diff --git a/Cargo.lock b/Cargo.lock index e8f9e75..a9eb1a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1981,7 +1981,10 @@ name = "ethlambda-rpc" version = "0.1.0" dependencies = [ "axum", + "ethlambda-storage", "prometheus", + "serde", + "serde_json", "thiserror 2.0.17", "tokio", "tracing", @@ -2015,6 +2018,7 @@ dependencies = [ "ethereum-types", "ethereum_ssz", "ethereum_ssz_derive", + "hex", "leansig", "serde", "ssz_types", diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 0b68239..4587651 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -8,7 +8,6 @@ use std::{ use clap::Parser; use ethlambda_p2p::{Bootnode, parse_enrs, start_p2p}; -use ethlambda_rpc::metrics::start_prometheus_metrics_api; use ethlambda_types::primitives::H256; use ethlambda_types::{ genesis::Genesis, @@ -20,6 +19,7 @@ use tracing::{error, info}; use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt}; use ethlambda_blockchain::BlockChain; +use ethlambda_storage::Store; const ASCII_ART: &str = r#" _ _ _ _ _ @@ -91,9 +91,10 @@ async fn main() { read_validator_keys(&validators_path, &validator_keys_dir, &options.node_id); let genesis_state = State::from_genesis(&genesis, validators); + let store = Store::from_genesis(genesis_state); let (p2p_tx, p2p_rx) = tokio::sync::mpsc::unbounded_channel(); - let blockchain = BlockChain::spawn(genesis_state, p2p_tx, validator_keys); + let blockchain = BlockChain::spawn(store.clone(), p2p_tx, validator_keys); let p2p_handle = tokio::spawn(start_p2p( node_p2p_key, @@ -103,7 +104,9 @@ async fn main() { p2p_rx, )); - start_prometheus_metrics_api(metrics_socket).await.unwrap(); + ethlambda_rpc::start_rpc_server(metrics_socket, store) + .await + .unwrap(); info!("Node initialized"); diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index efd58ef..9f35821 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -8,7 +8,7 @@ use ethlambda_types::{ block::{BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, primitives::TreeHash, signature::ValidatorSecretKey, - state::{Checkpoint, State}, + state::Checkpoint, }; use spawned_concurrency::tasks::{ CallResponse, CastResponse, GenServer, GenServerHandle, send_after, @@ -40,12 +40,11 @@ pub const SECONDS_PER_SLOT: u64 = 4; impl BlockChain { pub fn spawn( - genesis_state: State, + store: Store, p2p_tx: mpsc::UnboundedSender, validator_keys: HashMap, ) -> BlockChain { - let genesis_time = genesis_state.config.genesis_time; - let store = Store::from_genesis(genesis_state); + let genesis_time = store.config().genesis_time; let key_manager = key_manager::KeyManager::new(validator_keys); let handle = BlockChainServer { store, diff --git a/crates/common/types/Cargo.toml b/crates/common/types/Cargo.toml index 556d439..c841756 100644 --- a/crates/common/types/Cargo.toml +++ b/crates/common/types/Cargo.toml @@ -12,6 +12,7 @@ version.workspace = true [dependencies] thiserror.workspace = true serde.workspace = true +hex.workspace = true ethereum-types.workspace = true leansig.workspace = true diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index 185cbe6..88bcf3c 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -1,3 +1,4 @@ +use serde::Serialize; use ssz_derive::{Decode, Encode}; use ssz_types::typenum::U1048576; use tree_hash_derive::TreeHash; @@ -136,7 +137,7 @@ pub struct BlockWithAttestation { /// /// Headers are smaller than full blocks. They're useful for tracking the chain /// without storing everything. -#[derive(Debug, Clone, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Encode, Decode, TreeHash)] pub struct BlockHeader { /// The slot in which the block was proposed pub slot: u64, diff --git a/crates/common/types/src/state.rs b/crates/common/types/src/state.rs index f46d7ff..4b4038e 100644 --- a/crates/common/types/src/state.rs +++ b/crates/common/types/src/state.rs @@ -18,7 +18,7 @@ use crate::{ pub type ValidatorRegistryLimit = U4096; /// The main consensus state object -#[derive(Debug, Clone, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Encode, Decode, TreeHash)] pub struct State { /// The chain's configuration parameters pub config: ChainConfig, @@ -62,14 +62,22 @@ pub type JustificationValidators = ssz_types::BitList>; /// Represents a validator's static metadata and operational interface. -#[derive(Debug, Clone, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, Serialize, Encode, Decode, TreeHash)] pub struct Validator { /// XMSS one-time signature public key. + #[serde(serialize_with = "serialize_pubkey_hex")] pub pubkey: ValidatorPubkeyBytes, /// Validator index in the registry. pub index: u64, } +fn serialize_pubkey_hex(pubkey: &ValidatorPubkeyBytes, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&hex::encode(pubkey)) +} + impl Validator { pub fn get_pubkey(&self) -> Result { // TODO: make this unfallible by moving check to the constructor diff --git a/crates/net/rpc/Cargo.toml b/crates/net/rpc/Cargo.toml index 24895cd..6053906 100644 --- a/crates/net/rpc/Cargo.toml +++ b/crates/net/rpc/Cargo.toml @@ -15,3 +15,6 @@ tokio.workspace = true prometheus.workspace = true thiserror.workspace = true tracing.workspace = true +ethlambda-storage.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index e144883..d1bb391 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -1 +1,45 @@ +use std::net::SocketAddr; + +use axum::{Json, Router, response::IntoResponse, routing::get}; +use ethlambda_storage::Store; + pub mod metrics; + +pub async fn start_rpc_server(address: SocketAddr, store: Store) -> Result<(), std::io::Error> { + let metrics_router = metrics::start_prometheus_metrics_api(); + + // Create stateful routes first, then convert to stateless by applying state + let api_routes = Router::new() + .route("/lean/v0/states/finalized", get(get_latest_finalized_state)) + .route( + "/lean/v0/checkpoints/justified", + get(get_latest_justified_state), + ) + .with_state(store); + + // Merge stateless routers + let app = Router::new().merge(metrics_router).merge(api_routes); + + // Start the axum app + let listener = tokio::net::TcpListener::bind(address).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn get_latest_finalized_state( + axum::extract::State(store): axum::extract::State, +) -> impl IntoResponse { + let finalized = store.latest_finalized(); + let state = store + .get_state(&finalized.root) + .expect("finalized state exists"); + Json(state) +} + +async fn get_latest_justified_state( + axum::extract::State(store): axum::extract::State, +) -> impl IntoResponse { + let checkpoint = store.latest_justified(); + Json(checkpoint) +} diff --git a/crates/net/rpc/src/metrics.rs b/crates/net/rpc/src/metrics.rs index 62a85d1..2af9545 100644 --- a/crates/net/rpc/src/metrics.rs +++ b/crates/net/rpc/src/metrics.rs @@ -1,19 +1,11 @@ -use std::net::SocketAddr; - use axum::{Router, http::HeaderValue, response::IntoResponse, routing::get}; use thiserror::Error; use tracing::warn; -pub async fn start_prometheus_metrics_api(address: SocketAddr) -> Result<(), std::io::Error> { - let app = Router::new() +pub fn start_prometheus_metrics_api() -> Router { + Router::new() .route("/metrics", get(get_metrics)) - .route("/health", get(get_health)); - - // Start the axum app - let listener = tokio::net::TcpListener::bind(address).await?; - axum::serve(listener, app).await?; - - Ok(()) + .route("/lean/v0/health", get(get_health)) } pub(crate) async fn get_health() -> impl IntoResponse {