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