diff --git a/Cargo.toml b/Cargo.toml index 1c5b70c4..ae540ffb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,10 @@ members = [ "nomos-da/reed-solomon", "nomos-da/kzg", "nomos-da/full-replication", - "nomos-http-api", "nomos-cli", "nomos-utils", "nodes/nomos-node", + "nodes/nomos-node-api", "nodes/mixnode", "simulations", "consensus-engine", diff --git a/consensus-engine/Cargo.toml b/consensus-engine/Cargo.toml index 1c3ab499..3723c8cb 100644 --- a/consensus-engine/Cargo.toml +++ b/consensus-engine/Cargo.toml @@ -17,11 +17,16 @@ thiserror = "1" fraction = { version = "0.13" } nomos-utils = { path = "../nomos-utils", optional = true } +utoipa = { version = "4.0", optional = true } +serde_json = { version = "1.0", optional = true } + [features] default = [] serde = ["dep:serde", "nomos-utils/serde"] simulation = [] +openapi = ["dep:utoipa", "serde_json"] + [dev-dependencies] proptest = "1.2.0" proptest-state-machine = "0.1.0" diff --git a/consensus-engine/src/lib.rs b/consensus-engine/src/lib.rs index abf35cfa..a904607d 100644 --- a/consensus-engine/src/lib.rs +++ b/consensus-engine/src/lib.rs @@ -5,6 +5,12 @@ mod types; pub use overlay::Overlay; pub use types::*; +/// Re-export of the OpenAPI types +#[cfg(feature = "openapi")] +pub mod openapi { + pub use crate::types::BlockId; +} + #[derive(Clone, Debug, PartialEq)] pub struct Carnot { id: NodeId, diff --git a/consensus-engine/src/types/block_id.rs b/consensus-engine/src/types/block_id.rs index 3b542d49..91e29b60 100644 --- a/consensus-engine/src/types/block_id.rs +++ b/consensus-engine/src/types/block_id.rs @@ -1,4 +1,6 @@ +/// The block id #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct BlockId(pub(crate) [u8; 32]); #[cfg(feature = "serde")] diff --git a/nomos-http-api/Cargo.toml b/nodes/nomos-node-api/Cargo.toml similarity index 63% rename from nomos-http-api/Cargo.toml rename to nodes/nomos-node-api/Cargo.toml index 03e31bd6..c8252875 100644 --- a/nomos-http-api/Cargo.toml +++ b/nodes/nomos-node-api/Cargo.toml @@ -1,9 +1,10 @@ [package] -name = "nomos-http-api" +name = "nomos-node-api" version = "0.1.0" edition = "2021" [features] +default = ["axum"] axum = ["dep:axum", "dep:hyper", "utoipa-swagger-ui/axum"] [dependencies] @@ -11,13 +12,20 @@ async-trait = "0.1" overwatch-rs = { git = "https://github.com/logos-co/Overwatch", rev = "2f70806" } overwatch-derive = { git = "https://github.com/logos-co/Overwatch", rev = "ac28d01" } tracing = "0.1" -utoipa = "4.0" -utoipa-swagger-ui = { version = "4.0" } # axum related dependencies axum = { version = "0.6", optional = true } hyper = { version = "0.14", features = ["full"], optional = true } +nomos-core = { path = "../../nomos-core" } +nomos-da = { path = "../../nomos-services/data-availability" } +nomos-mempool = { path = "../../nomos-services/mempool", features = ["mock", "libp2p", "openapi"] } +full-replication = { path = "../../nomos-da/full-replication", features = ["openapi"] } +serde = { version = "1", features = ["derive"] } +tokio = { version = "1.33", default-features = false, features = ["sync"] } +utoipa = "4.0" +utoipa-swagger-ui = { version = "4.0" } + [dev-dependencies] axum = "0.6" hyper = { version = "0.14", features = ["full"] } diff --git a/nodes/nomos-node-api/src/http/backend/axum.rs b/nodes/nomos-node-api/src/http/backend/axum.rs new file mode 100644 index 00000000..1d572600 --- /dev/null +++ b/nodes/nomos-node-api/src/http/backend/axum.rs @@ -0,0 +1,179 @@ +use std::{fmt::Debug, hash::Hash, net::SocketAddr, sync::Arc}; + +use axum::{extract::State, response::IntoResponse, routing, Json, Router, Server}; +use full_replication::Blob; +use hyper::StatusCode; +use nomos_core::{da::blob, tx::Transaction}; +use nomos_mempool::{openapi::Status, MempoolMetrics}; +use overwatch_rs::overwatch::handle::OverwatchHandle; +use serde::{Deserialize, Serialize}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::{ + http::{cl::*, da::*}, + Backend, +}; + +#[derive(Clone)] +pub struct AxumBackendSettings { + pub addr: SocketAddr, + pub handle: OverwatchHandle, +} + +pub struct AxumBackend { + settings: Arc, + _cl: core::marker::PhantomData, +} + +#[derive(OpenApi)] +#[openapi( + paths( + da_metrics, + da_status, + ), + components( + schemas(Status, MempoolMetrics) + ), + tags( + (name = "da", description = "data availibility related APIs") + ) +)] +struct ApiDoc; + +type Store = Arc; + +#[async_trait::async_trait] +impl Backend for AxumBackend +where + ClTransaction: Transaction + + Clone + + Debug + + Hash + + Serialize + + for<'de> Deserialize<'de> + + Send + + Sync + + 'static, + ::Hash: + Serialize + for<'de> Deserialize<'de> + std::cmp::Ord + Debug + Send + Sync + 'static, +{ + type Error = hyper::Error; + type Settings = AxumBackendSettings; + + async fn new(settings: Self::Settings) -> Result + where + Self: Sized, + { + Ok(Self { + settings: Arc::new(settings), + _cl: core::marker::PhantomData, + }) + } + + async fn serve(self) -> Result<(), Self::Error> { + let store = self.settings.clone(); + let app = Router::new() + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/da/metrics", routing::get(da_metrics)) + .route("/da/status", routing::post(da_status)) + .route("/cl/metrics", routing::get(cl_metrics::)) + .route("/cl/status", routing::post(cl_status::)) + .with_state(store); + + Server::bind(&self.settings.addr) + .serve(app.into_make_service()) + .await + } +} + +#[utoipa::path( + get, + path = "/da/metrics", + responses( + (status = 200, description = "Get the mempool metrics of the da service", body = MempoolMetrics), + (status = 500, description = "Internal server error", body = String), + ) +)] +async fn da_metrics(State(store): State) -> impl IntoResponse { + match da_mempool_metrics(&store.handle).await { + Ok(metrics) => (StatusCode::OK, Json(metrics)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } +} + +#[utoipa::path( + post, + path = "/da/status", + responses( + (status = 200, description = "Query the mempool status of the da service", body = Vec<::Hash>), + (status = 500, description = "Internal server error", body = String), + ) +)] +async fn da_status( + State(store): State, + Json(items): Json::Hash>>, +) -> impl IntoResponse { + match da_mempool_status(&store.handle, items).await { + Ok(status) => (StatusCode::OK, Json(status)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } +} + +#[utoipa::path( + get, + path = "/cl/metrics", + responses( + (status = 200, description = "Get the mempool metrics of the cl service", body = MempoolMetrics), + (status = 500, description = "Internal server error", body = String), + ) +)] +async fn cl_metrics(State(store): State) -> impl IntoResponse +where + T: Transaction + + Clone + + Debug + + Hash + + Serialize + + for<'de> Deserialize<'de> + + Send + + Sync + + 'static, + ::Hash: std::cmp::Ord + Debug + Send + Sync + 'static, +{ + match cl_mempool_metrics::(&store.handle).await { + Ok(metrics) => (StatusCode::OK, Json(metrics)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } +} + +#[utoipa::path( + post, + path = "/cl/status", + responses( + (status = 200, description = "Query the mempool status of the cl service", body = Vec<::Hash>), + (status = 500, description = "Internal server error", body = String), + ) +)] +async fn cl_status( + State(store): State, + Json(items): Json::Hash>>, +) -> impl IntoResponse +where + T: Transaction + + Clone + + Debug + + Hash + + Serialize + + serde::de::DeserializeOwned + + Send + + Sync + + 'static, + ::Hash: + Serialize + serde::de::DeserializeOwned + std::cmp::Ord + Debug + Send + Sync + 'static, +{ + match cl_mempool_status::(&store.handle, items).await { + Ok(status) => (StatusCode::OK, Json(status)).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } +} diff --git a/nomos-http-api/src/http/backend/mod.rs b/nodes/nomos-node-api/src/http/backend/mod.rs similarity index 100% rename from nomos-http-api/src/http/backend/mod.rs rename to nodes/nomos-node-api/src/http/backend/mod.rs diff --git a/nodes/nomos-node-api/src/http/cl.rs b/nodes/nomos-node-api/src/http/cl.rs new file mode 100644 index 00000000..add0cddd --- /dev/null +++ b/nodes/nomos-node-api/src/http/cl.rs @@ -0,0 +1,73 @@ +use core::{fmt::Debug, hash::Hash}; + +use nomos_core::tx::Transaction; +use nomos_mempool::{ + backend::mockpool::MockPool, + network::adapters::libp2p::Libp2pAdapter, + openapi::{MempoolMetrics, Status}, + MempoolMsg, MempoolService, Transaction as TxDiscriminant, +}; +use serde::{Deserialize, Serialize}; +use tokio::sync::oneshot; + +type ClMempoolService = MempoolService< + Libp2pAdapter::Hash>, + MockPool::Hash>, + TxDiscriminant, +>; + +pub async fn cl_mempool_metrics( + handle: &overwatch_rs::overwatch::handle::OverwatchHandle, +) -> Result +where + T: Transaction + + Clone + + Debug + + Hash + + Serialize + + for<'de> Deserialize<'de> + + Send + + Sync + + 'static, + ::Hash: std::cmp::Ord + Debug + Send + Sync + 'static, +{ + let relay = handle.relay::>().connect().await?; + let (sender, receiver) = oneshot::channel(); + relay + .send(MempoolMsg::Metrics { + reply_channel: sender, + }) + .await + .map_err(|(e, _)| e)?; + + Ok(receiver.await?) +} + +pub async fn cl_mempool_status( + handle: &overwatch_rs::overwatch::handle::OverwatchHandle, + items: Vec<::Hash>, +) -> Result, super::DynError> +where + T: Transaction + + Clone + + Debug + + Hash + + Serialize + + for<'de> Deserialize<'de> + + Send + + Sync + + 'static, + ::Hash: std::cmp::Ord + Debug + Send + Sync + 'static, +{ + let relay = handle.relay::>().connect().await?; + let (sender, receiver) = oneshot::channel(); + relay + .send(MempoolMsg::Status { + items, + reply_channel: sender, + }) + .await + .map_err(|(e, _)| e)?; + + Ok(receiver.await?) +} diff --git a/nodes/nomos-node-api/src/http/da.rs b/nodes/nomos-node-api/src/http/da.rs new file mode 100644 index 00000000..22d45c0e --- /dev/null +++ b/nodes/nomos-node-api/src/http/da.rs @@ -0,0 +1,47 @@ +use full_replication::{Blob, Certificate}; +use nomos_core::da::blob; +use nomos_mempool::{ + backend::mockpool::MockPool, + network::adapters::libp2p::Libp2pAdapter, + openapi::{MempoolMetrics, Status}, + Certificate as CertDiscriminant, MempoolMsg, MempoolService, +}; +use tokio::sync::oneshot; + +type DaMempoolService = MempoolService< + Libp2pAdapter::Hash>, + MockPool::Hash>, + CertDiscriminant, +>; + +pub async fn da_mempool_metrics( + handle: &overwatch_rs::overwatch::handle::OverwatchHandle, +) -> Result { + let relay = handle.relay::().connect().await?; + let (sender, receiver) = oneshot::channel(); + relay + .send(MempoolMsg::Metrics { + reply_channel: sender, + }) + .await + .map_err(|(e, _)| e)?; + + Ok(receiver.await.unwrap()) +} + +pub async fn da_mempool_status( + handle: &overwatch_rs::overwatch::handle::OverwatchHandle, + items: Vec<::Hash>, +) -> Result, super::DynError> { + let relay = handle.relay::().connect().await?; + let (sender, receiver) = oneshot::channel(); + relay + .send(MempoolMsg::Status { + items, + reply_channel: sender, + }) + .await + .map_err(|(e, _)| e)?; + + Ok(receiver.await.unwrap()) +} diff --git a/nodes/nomos-node-api/src/http/mod.rs b/nodes/nomos-node-api/src/http/mod.rs new file mode 100644 index 00000000..22f67af6 --- /dev/null +++ b/nodes/nomos-node-api/src/http/mod.rs @@ -0,0 +1,5 @@ +pub type DynError = Box; + +pub mod backend; +pub mod cl; +pub mod da; diff --git a/nomos-http-api/src/lib.rs b/nodes/nomos-node-api/src/lib.rs similarity index 100% rename from nomos-http-api/src/lib.rs rename to nodes/nomos-node-api/src/lib.rs diff --git a/nomos-http-api/tests/todo.rs b/nodes/nomos-node-api/tests/todo.rs similarity index 99% rename from nomos-http-api/tests/todo.rs rename to nodes/nomos-node-api/tests/todo.rs index 73b95026..484bb111 100644 --- a/nomos-http-api/tests/todo.rs +++ b/nodes/nomos-node-api/tests/todo.rs @@ -6,7 +6,7 @@ use std::{ use axum::{routing, Router, Server}; use hyper::Error; -use nomos_http_api::{ApiService, ApiServiceSettings, Backend}; +use nomos_node_api::{ApiService, ApiServiceSettings, Backend}; use overwatch_derive::Services; use overwatch_rs::{overwatch::OverwatchRunner, services::handle::ServiceHandle}; use utoipa::{ diff --git a/nomos-da/full-replication/Cargo.toml b/nomos-da/full-replication/Cargo.toml index 39825ac7..f9422d9f 100644 --- a/nomos-da/full-replication/Cargo.toml +++ b/nomos-da/full-replication/Cargo.toml @@ -3,10 +3,15 @@ name = "full-replication" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +openapi = ["dep:utoipa", "serde_json"] [dependencies] blake2 = { version = "0.10" } bytes = { version = "1.3", features = ["serde"] } nomos-core = { path = "../../nomos-core" } -serde = { version = "1.0", features = ["derive"] } \ No newline at end of file +serde = { version = "1.0", features = ["derive"] } + +utoipa = { version = "4.0", optional = true } +serde_json = { version = "1.0", optional = true } diff --git a/nomos-da/full-replication/src/lib.rs b/nomos-da/full-replication/src/lib.rs index fd515231..762c5357 100644 --- a/nomos-da/full-replication/src/lib.rs +++ b/nomos-da/full-replication/src/lib.rs @@ -16,6 +16,12 @@ use bytes::Bytes; use nomos_core::wire; use serde::{Deserialize, Serialize}; +/// Re-export the types for OpenAPI +#[cfg(feature = "openapi")] +pub mod openapi { + pub use super::{Attestation, Certificate}; +} + #[derive(Debug, Clone)] pub struct FullReplication { certificate_strategy: CertificateStrategy, @@ -110,6 +116,7 @@ impl blob::Blob for Blob { } #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Attestation { blob: [u8; 32], voter: [u8; 32], @@ -135,6 +142,7 @@ impl attestation::Attestation for Attestation { } #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Certificate { attestations: Vec, } diff --git a/nomos-http-api/src/http/backend/axum.rs b/nomos-http-api/src/http/backend/axum.rs deleted file mode 100644 index efdd26e9..00000000 --- a/nomos-http-api/src/http/backend/axum.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::{net::SocketAddr, sync::Arc}; - -use axum::{Router, Server}; -use utoipa::OpenApi; -use utoipa_swagger_ui::SwaggerUi; - -use crate::Backend; - -#[derive(Clone)] -pub struct AxumBackendSettings { - pub addr: SocketAddr, -} - -pub struct AxumBackend { - settings: Arc, -} - -#[derive(OpenApi)] -#[openapi(paths(), components(), tags())] -struct ApiDoc; - -#[async_trait::async_trait] -impl Backend for AxumBackend { - type Error = hyper::Error; - type Settings = AxumBackendSettings; - - async fn new(settings: Self::Settings) -> Result - where - Self: Sized, - { - Ok(Self { - settings: Arc::new(settings), - }) - } - - async fn serve(self) -> Result<(), Self::Error> { - let store = self.settings.clone(); - let app = Router::new() - .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) - .with_state(store); - - Server::bind(&self.settings.addr) - .serve(app.into_make_service()) - .await - } -} diff --git a/nomos-http-api/src/http/mod.rs b/nomos-http-api/src/http/mod.rs deleted file mode 100644 index fceb1419..00000000 --- a/nomos-http-api/src/http/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod backend; diff --git a/nomos-services/mempool/Cargo.toml b/nomos-services/mempool/Cargo.toml index fa3b0ebf..30834d8a 100644 --- a/nomos-services/mempool/Cargo.toml +++ b/nomos-services/mempool/Cargo.toml @@ -21,6 +21,9 @@ tokio = { version = "1", features = ["sync", "macros"] } tokio-stream = "0.1" chrono = "0.4" +utoipa = { version = "4.0", optional = true } +serde_json = { version = "1", optional = true } + [dev-dependencies] nomos-log = { path = "../log" } overwatch-derive = { git = "https://github.com/logos-co/Overwatch", rev = "ac28d01" } @@ -31,3 +34,6 @@ blake2 = "0.10" default = [] mock = ["linked-hash-map", "nomos-network/mock", "rand", "nomos-core/mock"] libp2p = ["nomos-network/libp2p"] + +# enable to help generate OpenAPI +openapi = ["dep:utoipa", "serde_json"] diff --git a/nomos-services/mempool/src/backend/mod.rs b/nomos-services/mempool/src/backend/mod.rs index c97f2c15..727ef2a8 100644 --- a/nomos-services/mempool/src/backend/mod.rs +++ b/nomos-services/mempool/src/backend/mod.rs @@ -50,9 +50,22 @@ pub trait MemPool { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub enum Status { + /// Unknown status Unknown, + /// Pending status Pending, + /// Rejected status Rejected, + /// Accepted status + /// + /// The block id of the block that contains the item + #[cfg_attr( + feature = "openapi", + schema( + example = "e.g. 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + ) + )] InBlock { block: BlockId }, } diff --git a/nomos-services/mempool/src/lib.rs b/nomos-services/mempool/src/lib.rs index fe4bc9dd..ca887d8b 100644 --- a/nomos-services/mempool/src/lib.rs +++ b/nomos-services/mempool/src/lib.rs @@ -1,6 +1,12 @@ pub mod backend; pub mod network; +/// Re-export for OpenAPI +#[cfg(feature = "openapi")] +pub mod openapi { + pub use super::{backend::Status, MempoolMetrics}; +} + // std use std::{ fmt::{Debug, Error, Formatter}, @@ -43,6 +49,8 @@ where _d: PhantomData, } +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[derive(serde::Serialize, serde::Deserialize)] pub struct MempoolMetrics { pub pending_items: usize, pub last_item_timestamp: u64,