Skip to content

Commit

Permalink
feat(engine): no_std engine types (#1268)
Browse files Browse the repository at this point in the history
* feat: support no_std in alloy-rpc-types-eth

* feat: derive_more error

* ci: add no-std check

* ci: update script

* fix: crate collections

* fix: fmt

* chore: remove unused hash_map re-export

* feat: converts engine rpc types to no-std

* feat: derive_more display + std feature

* fix: jwt feat

* fix: clippy warnings

* fix: clippy warnings

* fix: error source

* fix: remove tree

* fix: std

* fix: std

* fix: std

* fix: dep

* fix: fmt

* fix: ssz derive depends on std

* fix: missing deps jwt feat

* fix: raw duration

* fix: fmts

* touchups

* fix: last nits

* fix: remove core result

* fix: remove manual result imports

* fix: fmts

---------

Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
  • Loading branch information
refcell and mattsse authored Sep 12, 2024
1 parent 8546ecf commit e61a184
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 72 deletions.
22 changes: 12 additions & 10 deletions crates/rpc-types-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,34 @@ workspace = true

[dependencies]
# ethereum
alloy-serde.workspace = true
alloy-rpc-types-eth.workspace = true
alloy-rlp = { workspace = true, features = ["arrayvec", "derive"] }
alloy-primitives = { workspace = true, features = ["rlp", "serde"] }
alloy-consensus = { workspace = true, features = ["std"] }
alloy-rpc-types-eth.workspace = true
alloy-serde.workspace = true
alloy-consensus = { workspace = true, features = ["serde"] }
alloy-eips = { workspace = true, features = ["serde"] }

# misc
serde = { workspace = true, features = ["derive"] }
derive_more = { workspace = true, features = ["display"] }

# ssz
ethereum_ssz_derive = { workspace = true, optional = true }
ethereum_ssz = { workspace = true, optional = true }

serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true

# jsonrpsee
jsonrpsee-types = { version = "0.24", optional = true }

# jwt
jsonwebtoken = { version = "9.3.0", optional = true }
rand = { workspace = true, optional = true }
jsonwebtoken = { version = "9.3.0", optional = true }

[features]
default = ["jwt"]
default = ["jwt", "std"]
std = ["alloy-rpc-types-eth/std", "alloy-consensus/std", "derive_more/std"]
jwt = ["dep:jsonwebtoken", "dep:rand"]
jsonrpsee-types = ["dep:jsonrpsee-types"]
ssz = ["dep:ethereum_ssz", "dep:ethereum_ssz_derive", "alloy-eips/ssz"]
jsonrpsee-types = ["dep:jsonrpsee-types", "alloy-rpc-types-eth/jsonrpsee-types"]
ssz = ["std", "dep:ethereum_ssz", "dep:ethereum_ssz_derive", "alloy-eips/ssz"]
kzg = ["alloy-consensus/kzg"]

[dev-dependencies]
Expand Down
2 changes: 2 additions & 0 deletions crates/rpc-types-engine/src/cancun.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Contains types related to the Cancun hardfork that will be used by RPC to communicate with the
//! beacon consensus engine.

use alloc::vec::Vec;

use alloy_primitives::B256;

/// Fields introduced in `engine_newPayloadV3` that are not present in the `ExecutionPayload` RPC
Expand Down
11 changes: 7 additions & 4 deletions crates/rpc-types-engine/src/forkchoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,25 @@ impl ForkchoiceState {
///
/// These are considered hard RPC errors and are _not_ returned as [PayloadStatus] or
/// [PayloadStatusEnum::Invalid].
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, derive_more::Display)]
pub enum ForkchoiceUpdateError {
/// The forkchoice update has been processed, but the requested contained invalid
/// [PayloadAttributes](crate::PayloadAttributes).
///
/// This is returned as an error because the payload attributes are invalid and the payload is not valid, See <https://github.com/ethereum/execution-apis/blob/6709c2a795b707202e93c4f2867fa0bf2640a84f/src/engine/paris.md#engine_forkchoiceupdatedv1>
#[error("invalid payload attributes")]
#[display("invalid payload attributes")]
UpdatedInvalidPayloadAttributes,
/// The given [ForkchoiceState] is invalid or inconsistent.
#[error("invalid forkchoice state")]
#[display("invalid forkchoice state")]
InvalidState,
/// Thrown when a forkchoice final block does not exist in the database.
#[error("final block not available in database")]
#[display("final block not available in database")]
UnknownFinalBlock,
}

#[cfg(feature = "std")]
impl std::error::Error for ForkchoiceUpdateError {}

#[cfg(feature = "jsonrpsee-types")]
impl From<ForkchoiceUpdateError> for jsonrpsee_types::error::ErrorObject<'static> {
fn from(value: ForkchoiceUpdateError) -> Self {
Expand Down
7 changes: 4 additions & 3 deletions crates/rpc-types-engine/src/identification.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
//! Client identification: <https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md>

use alloc::string::{String, ToString};
use core::str::FromStr;
use serde::{Deserialize, Serialize};
use std::str::FromStr;

/// This enum defines a standard for specifying a client with just two letters. Clients teams which
/// have a code reserved in this list MUST use this code when identifying themselves.
Expand Down Expand Up @@ -93,8 +94,8 @@ impl FromStr for ClientCode {
}
}

impl std::fmt::Display for ClientCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for ClientCode {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.as_str())
}
}
Expand Down
68 changes: 50 additions & 18 deletions crates/rpc-types-engine/src/jwt.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,54 @@
//! JWT (JSON Web Token) utilities for the Engine API.

use alloc::{format, string::String};
use alloy_primitives::hex;
use core::{str::FromStr, time::Duration};
use jsonwebtoken::{
decode, errors::ErrorKind, get_current_timestamp, Algorithm, DecodingKey, Validation,
};
use rand::Rng;
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use std::{
fs, io,
path::{Path, PathBuf},
str::FromStr,
time::Duration,
};
use thiserror::Error;

/// Errors returned by the [`JwtSecret`]
#[derive(Error, Debug)]
#[derive(Debug, derive_more::Display)]
pub enum JwtError {
/// An error encountered while decoding the hexadecimal string for the JWT secret.
#[error(transparent)]
JwtSecretHexDecodeError(#[from] hex::FromHexError),
#[display("{_0}")]
JwtSecretHexDecodeError(hex::FromHexError),

/// The JWT key length provided is invalid, expecting a specific length.
#[error("JWT key is expected to have a length of {0} digits. {1} digits key provided")]
#[display("JWT key is expected to have a length of {_0} digits. {_1} digits key provided")]
InvalidLength(usize, usize),

/// The signature algorithm used in the JWT is not supported. Only HS256 is supported.
#[error("unsupported signature algorithm. Only HS256 is supported")]
#[display("unsupported signature algorithm. Only HS256 is supported")]
UnsupportedSignatureAlgorithm,

/// The provided signature in the JWT is invalid.
#[error("provided signature is invalid")]
#[display("provided signature is invalid")]
InvalidSignature,

/// The "iat" (issued-at) claim in the JWT is not within the allowed ±60 seconds from the
/// current time.
#[error("IAT (issued-at) claim is not within ±60 seconds from the current time")]
#[display("IAT (issued-at) claim is not within ±60 seconds from the current time")]
InvalidIssuanceTimestamp,

/// The Authorization header is missing or invalid in the context of JWT validation.
#[error("Authorization header is missing or invalid")]
#[display("Authorization header is missing or invalid")]
MissingOrInvalidAuthorizationHeader,

/// An error occurred during JWT decoding.
#[error("JWT decoding error: {0}")]
#[display("JWT decoding error: {_0}")]
JwtDecodingError(String),

/// An error occurred while creating a directory to store the JWT.
#[error("failed to create dir {path:?}: {source}")]
#[display("failed to create dir {path:?}: {source}")]
#[cfg(feature = "std")]
CreateDir {
/// The source `io::Error`.
source: io::Error,
Expand All @@ -56,7 +57,8 @@ pub enum JwtError {
},

/// An error occurred while reading the JWT from a file.
#[error("failed to read from {path:?}: {source}")]
#[display("failed to read from {path:?}: {source}")]
#[cfg(feature = "std")]
Read {
/// The source `io::Error`.
source: io::Error,
Expand All @@ -65,7 +67,8 @@ pub enum JwtError {
},

/// An error occurred while writing the JWT to a file.
#[error("failed to write to {path:?}: {source}")]
#[display("failed to write to {path:?}: {source}")]
#[cfg(feature = "std")]
Write {
/// The source `io::Error`.
source: io::Error,
Expand All @@ -74,6 +77,25 @@ pub enum JwtError {
},
}

impl From<hex::FromHexError> for JwtError {
fn from(err: hex::FromHexError) -> Self {
Self::JwtSecretHexDecodeError(err)
}
}

#[cfg(feature = "std")]
impl std::error::Error for JwtError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::JwtSecretHexDecodeError(err) => Some(err),
Self::CreateDir { source, .. } => Some(source),
Self::Read { source, .. } => Some(source),
Self::Write { source, .. } => Some(source),
_ => None,
}
}
}

/// Length of the hex-encoded 256 bit secret key.
/// A 256-bit encoded string in Rust has a length of 64 digits because each digit represents 4 bits
/// of data. In hexadecimal representation, each digit can have 16 possible values (0-9 and A-F), so
Expand Down Expand Up @@ -161,6 +183,7 @@ impl JwtSecret {
/// Tries to load a [`JwtSecret`] from the specified file path.
/// I/O or secret validation errors might occur during read operations in the form of
/// a [`JwtError`].
#[cfg(feature = "std")]
pub fn from_file(fpath: &Path) -> Result<Self, JwtError> {
let hex = fs::read_to_string(fpath)
.map_err(|err| JwtError::Read { source: err, path: fpath.into() })?;
Expand All @@ -170,6 +193,7 @@ impl JwtSecret {

/// Creates a random [`JwtSecret`] and tries to store it at the specified path. I/O errors might
/// occur during write operations in the form of a [`JwtError`]
#[cfg(feature = "std")]
pub fn try_create_random(fpath: &Path) -> Result<Self, JwtError> {
if let Some(dir) = fpath.parent() {
// Create parent directory
Expand Down Expand Up @@ -234,8 +258,8 @@ impl JwtSecret {
}
}

impl std::fmt::Debug for JwtSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for JwtSecret {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("JwtSecretHash").field(&"{{}}").finish()
}
}
Expand All @@ -253,7 +277,8 @@ mod tests {
use super::*;
use assert_matches::assert_matches;
use jsonwebtoken::{encode, EncodingKey, Header};
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "std")]
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tempfile::tempdir;

#[test]
Expand Down Expand Up @@ -328,6 +353,7 @@ mod tests {
}

#[test]
#[cfg(feature = "std")]
fn validation_error_iat_out_of_window() {
let secret = JwtSecret::random();

Expand Down Expand Up @@ -403,6 +429,7 @@ mod tests {
}

#[test]
#[cfg(feature = "std")]
fn ephemeral_secret_created() {
let fpath: &Path = Path::new("secret0.hex");
assert!(fs::metadata(fpath).is_err());
Expand All @@ -412,6 +439,7 @@ mod tests {
}

#[test]
#[cfg(feature = "std")]
fn valid_secret_provided() {
let fpath = Path::new("secret1.hex");
assert!(fs::metadata(fpath).is_err());
Expand All @@ -431,6 +459,7 @@ mod tests {
}

#[test]
#[cfg(feature = "std")]
fn invalid_hex_provided() {
let fpath = Path::new("secret2.hex");
fs::write(fpath, "invalid hex").unwrap();
Expand All @@ -440,6 +469,7 @@ mod tests {
}

#[test]
#[cfg(feature = "std")]
fn provided_file_not_exists() {
let fpath = Path::new("secret3.hex");
let result = JwtSecret::from_file(fpath);
Expand All @@ -448,12 +478,14 @@ mod tests {
}

#[test]
#[cfg(feature = "std")]
fn provided_file_is_a_directory() {
let dir = tempdir().unwrap();
let result = JwtSecret::from_file(dir.path());
assert_matches!(result, Err(JwtError::Read {source: _,path}) if path == dir.into_path());
}

#[cfg(feature = "std")]
fn to_u64(time: SystemTime) -> u64 {
time.duration_since(UNIX_EPOCH).unwrap().as_secs()
}
Expand Down
20 changes: 15 additions & 5 deletions crates/rpc-types-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,29 @@
)]
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]

extern crate alloc;

mod cancun;
pub use cancun::*;

mod forkchoice;
pub use forkchoice::*;

mod identification;
pub use identification::*;

#[cfg(feature = "jwt")]
mod jwt;
pub mod payload;
mod transition;
#[cfg(feature = "jwt")]
pub use jwt::*;

pub use self::{cancun::*, forkchoice::*, identification::*, payload::*, transition::*};
pub mod payload;
pub use payload::*;

#[cfg(feature = "jwt")]
pub use self::jwt::*;
mod transition;
pub use transition::*;

#[doc(inline)]
pub use alloy_eips::eip6110::DepositRequest as DepositRequestV1;
Expand Down
Loading

0 comments on commit e61a184

Please sign in to comment.