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