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 ACCESS_LIST_TRANSACTION_FIELD_COUNT, ACCESS_LIST_TRANSACTION_TYPE, AccessList,
16 AccessListEntries, AccessListEntry, AccessListStorageKeyItems, AccessListStorageKeys,
17 AccessListTransactionDecodeError, AccessListTransactionDecodeErrorCategory,
18 AccessListTransactionField, AccessListTransactionTo, BLOB_TRANSACTION_FIELD_COUNT,
19 BLOB_TRANSACTION_TYPE, BlobTransactionDecodeError, BlobTransactionDecodeErrorCategory,
20 BlobTransactionField, BlobVersionedHashItems, BlobVersionedHashes,
21 DYNAMIC_FEE_TRANSACTION_FIELD_COUNT, DYNAMIC_FEE_TRANSACTION_TYPE,
22 DynamicFeeTransactionDecodeError, DynamicFeeTransactionDecodeErrorCategory,
23 DynamicFeeTransactionField, DynamicFeeTransactionTo, EIP_2718_MAX_TYPED_PREFIX,
24 EIP_2718_RESERVED_PREFIX, EIP_2718_SCALAR_PREFIX_START, EIP_2718_TYPED_ZERO_PREFIX,
25 InvalidSignatureYParity, LEGACY_TRANSACTION_FIELD_COUNT, LEGACY_TRANSACTION_PREFIX_START,
26 LegacyTransactionDecodeError, LegacyTransactionDecodeErrorCategory, LegacyTransactionField,
27 LegacyTransactionTo, SignatureYParity, TransactionEncodeError, TransactionEncodeErrorCategory,
28 TransactionEnvelope, TransactionEnvelopeError, TransactionEnvelopeErrorCategory,
29 TypedTransactionEnvelope, UnvalidatedAccessListTransaction, UnvalidatedBlobTransaction,
30 UnvalidatedDynamicFeeTransaction, UnvalidatedLegacyTransaction, UnvalidatedTransaction,
31 decode_access_list_transaction, decode_blob_transaction, decode_dynamic_fee_transaction,
32 decode_legacy_transaction, decode_transaction_envelope, encode_access_list_transaction,
33 encode_blob_transaction, encode_dynamic_fee_transaction, encode_legacy_transaction,
34 encode_transaction, encoded_access_list_transaction_len, encoded_blob_transaction_len,
35 encoded_dynamic_fee_transaction_len, encoded_legacy_transaction_len, encoded_transaction_len,
36};
37
38#[non_exhaustive]
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub enum ProtocolError {
42 Feature(FeatureError),
44 Fork(ForkError),
46 InvalidStateTransition,
48}
49
50impl ProtocolError {
51 #[must_use]
53 pub const fn code(self) -> &'static str {
54 match self {
55 Self::Feature(error) => error.code(),
56 Self::Fork(error) => error.code(),
57 Self::InvalidStateTransition => "ETH_PROTOCOL_INVALID_STATE_TRANSITION",
58 }
59 }
60
61 #[must_use]
63 pub const fn message(self) -> &'static str {
64 match self {
65 Self::Feature(error) => error.message(),
66 Self::Fork(error) => error.message(),
67 Self::InvalidStateTransition => {
68 "validation state transition is not allowed from the current state"
69 }
70 }
71 }
72
73 #[must_use]
75 pub const fn category(self) -> ProtocolErrorCategory {
76 match self {
77 Self::Feature(_) => ProtocolErrorCategory::Feature,
78 Self::Fork(_) => ProtocolErrorCategory::Fork,
79 Self::InvalidStateTransition => ProtocolErrorCategory::State,
80 }
81 }
82}
83
84impl fmt::Display for ProtocolError {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 formatter.write_str(self.message())
87 }
88}
89
90#[cfg(feature = "std")]
91impl std::error::Error for ProtocolError {}
92
93#[non_exhaustive]
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum ProtocolErrorCategory {
97 Feature,
99 Fork,
101 State,
103}
104
105#[non_exhaustive]
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub enum FeatureError {
109 Disabled,
111 Unsupported,
113}
114
115impl FeatureError {
116 #[must_use]
118 pub const fn code(self) -> &'static str {
119 match self {
120 Self::Disabled => "ETH_FEATURE_DISABLED",
121 Self::Unsupported => "ETH_FEATURE_UNSUPPORTED",
122 }
123 }
124
125 #[must_use]
127 pub const fn message(self) -> &'static str {
128 match self {
129 Self::Disabled => "feature is disabled for this operation",
130 Self::Unsupported => "feature is not supported by this crate version",
131 }
132 }
133}
134
135impl fmt::Display for FeatureError {
136 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137 formatter.write_str(self.message())
138 }
139}
140
141#[cfg(feature = "std")]
142impl std::error::Error for FeatureError {}
143
144#[non_exhaustive]
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
147pub enum ForkError {
148 Unsupported,
150 Inactive,
152 MissingActivation,
154}
155
156impl ForkError {
157 #[must_use]
159 pub const fn code(self) -> &'static str {
160 match self {
161 Self::Unsupported => "ETH_FORK_UNSUPPORTED",
162 Self::Inactive => "ETH_FORK_INACTIVE",
163 Self::MissingActivation => "ETH_FORK_MISSING_ACTIVATION",
164 }
165 }
166
167 #[must_use]
169 pub const fn message(self) -> &'static str {
170 match self {
171 Self::Unsupported => "fork is not supported by this crate version",
172 Self::Inactive => "fork is not active for the validation context",
173 Self::MissingActivation => "fork activation data is incomplete",
174 }
175 }
176}
177
178impl fmt::Display for ForkError {
179 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
180 formatter.write_str(self.message())
181 }
182}
183
184#[cfg(feature = "std")]
185impl std::error::Error for ForkError {}
186
187#[derive(Clone, Copy, Debug, Eq, PartialEq)]
189pub enum ForkActivation {
190 BlockOnly {
192 activation_block: BlockNumber,
194 },
195 BlockAndTimestamp {
197 activation_block: BlockNumber,
199 activation_timestamp: UnixTimestamp,
201 },
202}
203
204#[derive(Clone, Copy, Debug, Eq, PartialEq)]
206pub struct ForkSpec {
207 pub chain_id: ChainId,
209 pub activation: ForkActivation,
211}
212
213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct ValidationContext {
216 pub fork: ForkSpec,
218 pub block_number: BlockNumber,
220 pub timestamp: UnixTimestamp,
222}
223
224impl ValidationContext {
225 #[must_use]
227 pub fn fork_is_active(self) -> bool {
228 match self.fork.activation {
229 ForkActivation::BlockOnly { activation_block } => self.block_number >= activation_block,
230 ForkActivation::BlockAndTimestamp {
231 activation_block,
232 activation_timestamp,
233 } => self.block_number >= activation_block && self.timestamp >= activation_timestamp,
234 }
235 }
236}
237
238#[derive(Clone, Copy, Debug, Eq, PartialEq)]
240pub struct Decoded;
241
242#[derive(Clone, Copy, Debug, Eq, PartialEq)]
244pub struct Canonical;
245
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
248pub struct ForkValidated;
249
250#[derive(Clone, Copy, Debug, Eq, PartialEq)]
252pub struct SenderRecovered;
253
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub struct Transaction<State> {
257 _state: PhantomData<State>,
258}
259
260impl Transaction<Decoded> {
261 #[must_use]
266 #[cfg(test)]
267 pub(crate) const fn decoded() -> Self {
268 Self {
269 _state: PhantomData,
270 }
271 }
272
273 #[must_use]
275 pub const fn into_canonical(self) -> Transaction<Canonical> {
276 Transaction {
277 _state: PhantomData,
278 }
279 }
280}
281
282impl Transaction<Canonical> {
283 #[must_use]
285 pub const fn into_fork_validated(self) -> Transaction<ForkValidated> {
286 Transaction {
287 _state: PhantomData,
288 }
289 }
290}
291
292impl Transaction<ForkValidated> {
293 #[must_use]
295 pub const fn into_sender_recovered(self) -> Transaction<SenderRecovered> {
296 Transaction {
297 _state: PhantomData,
298 }
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 extern crate std;
306 use std::string::ToString;
307
308 #[test]
309 fn activation_requires_block_and_time() {
310 let context = ValidationContext {
311 fork: ForkSpec {
312 chain_id: ChainId::new(1),
313 activation: ForkActivation::BlockAndTimestamp {
314 activation_block: BlockNumber::new(10),
315 activation_timestamp: UnixTimestamp::new(20),
316 },
317 },
318 block_number: BlockNumber::new(10),
319 timestamp: UnixTimestamp::new(19),
320 };
321 assert!(!context.fork_is_active());
322 }
323
324 #[test]
325 fn block_only_activation_ignores_timestamp() {
326 let context = ValidationContext {
327 fork: ForkSpec {
328 chain_id: ChainId::new(1),
329 activation: ForkActivation::BlockOnly {
330 activation_block: BlockNumber::new(10),
331 },
332 },
333 block_number: BlockNumber::new(10),
334 timestamp: UnixTimestamp::new(0),
335 };
336 assert!(context.fork_is_active());
337 }
338
339 #[test]
340 fn transaction_typestate_advances_in_order() {
341 let transaction = Transaction::decoded()
342 .into_canonical()
343 .into_fork_validated()
344 .into_sender_recovered();
345 assert_eq!(
346 transaction,
347 Transaction::<SenderRecovered> {
348 _state: PhantomData
349 }
350 );
351 }
352
353 #[test]
354 fn protocol_errors_have_stable_codes_and_categories() {
355 let error = ProtocolError::Fork(ForkError::Inactive);
356
357 assert_eq!(error.code(), "ETH_FORK_INACTIVE");
358 assert_eq!(
359 error.message(),
360 "fork is not active for the validation context"
361 );
362 assert_eq!(error.category(), ProtocolErrorCategory::Fork);
363 assert_eq!(
364 error.to_string(),
365 "fork is not active for the validation context"
366 );
367 }
368
369 #[test]
370 fn feature_errors_format_without_payloads() {
371 let error = ProtocolError::Feature(FeatureError::Unsupported);
372
373 assert_eq!(error.code(), "ETH_FEATURE_UNSUPPORTED");
374 assert_eq!(
375 FeatureError::Unsupported.to_string(),
376 "feature is not supported by this crate version"
377 );
378 }
379}