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