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