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