Skip to main content

eth_valkyoth_protocol/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3//! Fork-aware Ethereum protocol validation state.
4
5#[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, AccessList, AccessListEntries, AccessListEntry,
16    AccessListStorageKeyItems, AccessListStorageKeys, AccessListTransactionDecodeError,
17    AccessListTransactionDecodeErrorCategory, AccessListTransactionField, AccessListTransactionTo,
18    BLOB_TRANSACTION_FIELD_COUNT, BlobTransactionDecodeError, BlobTransactionDecodeErrorCategory,
19    BlobTransactionField, BlobVersionedHashItems, BlobVersionedHashes,
20    DYNAMIC_FEE_TRANSACTION_FIELD_COUNT, DynamicFeeTransactionDecodeError,
21    DynamicFeeTransactionDecodeErrorCategory, DynamicFeeTransactionField, DynamicFeeTransactionTo,
22    EIP_2718_MAX_TYPED_PREFIX, EIP_2718_RESERVED_PREFIX, EIP_2718_SCALAR_PREFIX_START,
23    EIP_2718_TYPED_ZERO_PREFIX, InvalidSignatureYParity, LEGACY_TRANSACTION_FIELD_COUNT,
24    LEGACY_TRANSACTION_PREFIX_START, LegacyTransactionDecodeError,
25    LegacyTransactionDecodeErrorCategory, LegacyTransactionField, LegacyTransactionTo,
26    SignatureYParity, TransactionEnvelope, TransactionEnvelopeError,
27    TransactionEnvelopeErrorCategory, TypedTransactionEnvelope, UnvalidatedAccessListTransaction,
28    UnvalidatedBlobTransaction, UnvalidatedDynamicFeeTransaction, UnvalidatedLegacyTransaction,
29    decode_access_list_transaction, decode_blob_transaction, decode_dynamic_fee_transaction,
30    decode_legacy_transaction, decode_transaction_envelope,
31};
32
33/// Protocol validation failure.
34#[non_exhaustive]
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum ProtocolError {
37    /// A feature is disabled or unsupported for the selected operation.
38    Feature(FeatureError),
39    /// Fork context is missing, unsupported, or inactive.
40    Fork(ForkError),
41    /// A validation state transition was attempted out of order.
42    InvalidStateTransition,
43}
44
45impl ProtocolError {
46    /// Stable machine-readable error code.
47    #[must_use]
48    pub const fn code(self) -> &'static str {
49        match self {
50            Self::Feature(error) => error.code(),
51            Self::Fork(error) => error.code(),
52            Self::InvalidStateTransition => "ETH_PROTOCOL_INVALID_STATE_TRANSITION",
53        }
54    }
55
56    /// Stable human-readable error message.
57    #[must_use]
58    pub const fn message(self) -> &'static str {
59        match self {
60            Self::Feature(error) => error.message(),
61            Self::Fork(error) => error.message(),
62            Self::InvalidStateTransition => {
63                "validation state transition is not allowed from the current state"
64            }
65        }
66    }
67
68    /// Stable high-level category for policy decisions.
69    #[must_use]
70    pub const fn category(self) -> ProtocolErrorCategory {
71        match self {
72            Self::Feature(_) => ProtocolErrorCategory::Feature,
73            Self::Fork(_) => ProtocolErrorCategory::Fork,
74            Self::InvalidStateTransition => ProtocolErrorCategory::State,
75        }
76    }
77}
78
79impl fmt::Display for ProtocolError {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        formatter.write_str(self.message())
82    }
83}
84
85#[cfg(feature = "std")]
86impl std::error::Error for ProtocolError {}
87
88/// Stable high-level protocol error categories.
89#[non_exhaustive]
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub enum ProtocolErrorCategory {
92    /// Feature support or configuration failure.
93    Feature,
94    /// Fork selection or activation failure.
95    Fork,
96    /// Validation state failure.
97    State,
98}
99
100/// Feature availability failure.
101#[non_exhaustive]
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum FeatureError {
104    /// The feature is intentionally not enabled for this build or context.
105    Disabled,
106    /// The feature is not implemented by this crate version.
107    Unsupported,
108}
109
110impl FeatureError {
111    /// Stable machine-readable error code.
112    #[must_use]
113    pub const fn code(self) -> &'static str {
114        match self {
115            Self::Disabled => "ETH_FEATURE_DISABLED",
116            Self::Unsupported => "ETH_FEATURE_UNSUPPORTED",
117        }
118    }
119
120    /// Stable human-readable error message.
121    #[must_use]
122    pub const fn message(self) -> &'static str {
123        match self {
124            Self::Disabled => "feature is disabled for this operation",
125            Self::Unsupported => "feature is not supported by this crate version",
126        }
127    }
128}
129
130impl fmt::Display for FeatureError {
131    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
132        formatter.write_str(self.message())
133    }
134}
135
136#[cfg(feature = "std")]
137impl std::error::Error for FeatureError {}
138
139/// Fork selection or activation failure.
140#[non_exhaustive]
141#[derive(Clone, Copy, Debug, Eq, PartialEq)]
142pub enum ForkError {
143    /// The selected fork is not supported by this crate version.
144    Unsupported,
145    /// The selected fork is not active for the supplied validation context.
146    Inactive,
147    /// Fork activation data is incomplete.
148    MissingActivation,
149}
150
151impl ForkError {
152    /// Stable machine-readable error code.
153    #[must_use]
154    pub const fn code(self) -> &'static str {
155        match self {
156            Self::Unsupported => "ETH_FORK_UNSUPPORTED",
157            Self::Inactive => "ETH_FORK_INACTIVE",
158            Self::MissingActivation => "ETH_FORK_MISSING_ACTIVATION",
159        }
160    }
161
162    /// Stable human-readable error message.
163    #[must_use]
164    pub const fn message(self) -> &'static str {
165        match self {
166            Self::Unsupported => "fork is not supported by this crate version",
167            Self::Inactive => "fork is not active for the validation context",
168            Self::MissingActivation => "fork activation data is incomplete",
169        }
170    }
171}
172
173impl fmt::Display for ForkError {
174    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
175        formatter.write_str(self.message())
176    }
177}
178
179#[cfg(feature = "std")]
180impl std::error::Error for ForkError {}
181
182/// Unambiguous fork activation rule.
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub enum ForkActivation {
185    /// Block number alone determines activation.
186    BlockOnly {
187        /// Activation block for this fork view.
188        activation_block: BlockNumber,
189    },
190    /// Both block number and timestamp must be satisfied.
191    BlockAndTimestamp {
192        /// Activation block for this fork view.
193        activation_block: BlockNumber,
194        /// Activation timestamp for timestamp-based forks.
195        activation_timestamp: UnixTimestamp,
196    },
197}
198
199/// Ethereum fork rules selected for a validation operation.
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub struct ForkSpec {
202    /// Chain being validated.
203    pub chain_id: ChainId,
204    /// Activation rule for this fork view.
205    pub activation: ForkActivation,
206}
207
208/// Validation context that must be explicit for consensus-sensitive operations.
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub struct ValidationContext {
211    /// Fork rules.
212    pub fork: ForkSpec,
213    /// Current block number.
214    pub block_number: BlockNumber,
215    /// Current block timestamp.
216    pub timestamp: UnixTimestamp,
217}
218
219impl ValidationContext {
220    /// Returns whether the configured fork is active for this context.
221    #[must_use]
222    pub fn fork_is_active(self) -> bool {
223        match self.fork.activation {
224            ForkActivation::BlockOnly { activation_block } => self.block_number >= activation_block,
225            ForkActivation::BlockAndTimestamp {
226                activation_block,
227                activation_timestamp,
228            } => self.block_number >= activation_block && self.timestamp >= activation_timestamp,
229        }
230    }
231}
232
233/// Raw wire input was accepted by the codec.
234#[derive(Clone, Copy, Debug, Eq, PartialEq)]
235pub struct Decoded;
236
237/// Canonical wire form and type-specific structure were checked.
238#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239pub struct Canonical;
240
241/// Fork-specific validity was checked.
242#[derive(Clone, Copy, Debug, Eq, PartialEq)]
243pub struct ForkValidated;
244
245/// Sender recovery succeeded.
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
247pub struct SenderRecovered;
248
249/// A transaction token whose validation state is tracked at compile time.
250#[derive(Clone, Copy, Debug, Eq, PartialEq)]
251pub struct Transaction<State> {
252    _state: PhantomData<State>,
253}
254
255impl Transaction<Decoded> {
256    /// Creates a token for a decoded transaction in internal tests.
257    ///
258    /// A public decoded-transaction entry point will be added only together
259    /// with the real codec output that proves the decoded state.
260    #[must_use]
261    #[cfg(test)]
262    pub(crate) const fn decoded() -> Self {
263        Self {
264            _state: PhantomData,
265        }
266    }
267
268    /// Advances to canonical form after canonical checks pass.
269    #[must_use]
270    pub const fn into_canonical(self) -> Transaction<Canonical> {
271        Transaction {
272            _state: PhantomData,
273        }
274    }
275}
276
277impl Transaction<Canonical> {
278    /// Advances after fork-specific validation passes.
279    #[must_use]
280    pub const fn into_fork_validated(self) -> Transaction<ForkValidated> {
281        Transaction {
282            _state: PhantomData,
283        }
284    }
285}
286
287impl Transaction<ForkValidated> {
288    /// Advances after sender recovery succeeds.
289    #[must_use]
290    pub const fn into_sender_recovered(self) -> Transaction<SenderRecovered> {
291        Transaction {
292            _state: PhantomData,
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    extern crate std;
301    use std::string::ToString;
302
303    #[test]
304    fn activation_requires_block_and_time() {
305        let context = ValidationContext {
306            fork: ForkSpec {
307                chain_id: ChainId::new(1),
308                activation: ForkActivation::BlockAndTimestamp {
309                    activation_block: BlockNumber::new(10),
310                    activation_timestamp: UnixTimestamp::new(20),
311                },
312            },
313            block_number: BlockNumber::new(10),
314            timestamp: UnixTimestamp::new(19),
315        };
316        assert!(!context.fork_is_active());
317    }
318
319    #[test]
320    fn block_only_activation_ignores_timestamp() {
321        let context = ValidationContext {
322            fork: ForkSpec {
323                chain_id: ChainId::new(1),
324                activation: ForkActivation::BlockOnly {
325                    activation_block: BlockNumber::new(10),
326                },
327            },
328            block_number: BlockNumber::new(10),
329            timestamp: UnixTimestamp::new(0),
330        };
331        assert!(context.fork_is_active());
332    }
333
334    #[test]
335    fn transaction_typestate_advances_in_order() {
336        let transaction = Transaction::decoded()
337            .into_canonical()
338            .into_fork_validated()
339            .into_sender_recovered();
340        assert_eq!(
341            transaction,
342            Transaction::<SenderRecovered> {
343                _state: PhantomData
344            }
345        );
346    }
347
348    #[test]
349    fn protocol_errors_have_stable_codes_and_categories() {
350        let error = ProtocolError::Fork(ForkError::Inactive);
351
352        assert_eq!(error.code(), "ETH_FORK_INACTIVE");
353        assert_eq!(
354            error.message(),
355            "fork is not active for the validation context"
356        );
357        assert_eq!(error.category(), ProtocolErrorCategory::Fork);
358        assert_eq!(
359            error.to_string(),
360            "fork is not active for the validation context"
361        );
362    }
363
364    #[test]
365    fn feature_errors_format_without_payloads() {
366        let error = ProtocolError::Feature(FeatureError::Unsupported);
367
368        assert_eq!(error.code(), "ETH_FEATURE_UNSUPPORTED");
369        assert_eq!(
370            FeatureError::Unsupported.to_string(),
371            "feature is not supported by this crate version"
372        );
373    }
374}