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