diff --git a/offchain/Cargo.lock b/offchain/Cargo.lock index d0298bc93..71877c7d7 100644 --- a/offchain/Cargo.lock +++ b/offchain/Cargo.lock @@ -476,6 +476,25 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "authority-claimer" +version = "1.0.0" +dependencies = [ + "async-trait", + "clap", + "eth-tx-manager", + "ethers", + "http-server", + "rollups-events", + "rusoto_core", + "serde", + "serde_json", + "snafu", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "auto_impl" version = "0.5.0" diff --git a/offchain/Cargo.toml b/offchain/Cargo.toml index d580f90b3..8b659bd3a 100644 --- a/offchain/Cargo.toml +++ b/offchain/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "advance-runner", + "authority-claimer", "contracts", "data", "dispatcher", @@ -38,6 +39,7 @@ diesel_migrations = "2.0" env_logger = "0.10" eth-tx-manager = "0.10" ethabi = "18.0" +ethers = "1.0" ethers-signers = "1.0" futures = "0.3" futures-util = "0.3" diff --git a/offchain/authority-claimer/Cargo.toml b/offchain/authority-claimer/Cargo.toml new file mode 100644 index 000000000..8cdc8cb51 --- /dev/null +++ b/offchain/authority-claimer/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "authority-claimer" +license = "Apache-2.0" +version = "1.0.0" +edition = "2021" + +[[bin]] +name = "cartesi-rollups-authority-claimer" +path = "src/main.rs" +test = false + +[dependencies] +http-server = { path = "../http-server" } +rollups-events = { path = "../rollups-events" } + +async-trait.workspace = true +clap = { workspace = true, features = ["derive"] } +eth-tx-manager.workspace = true +ethers.workspace = true +rusoto_core.workspace = true +serde.workspace = true +serde_json.workspace = true +snafu.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing.workspace = true diff --git a/offchain/authority-claimer/src/checker.rs b/offchain/authority-claimer/src/checker.rs new file mode 100644 index 000000000..54069dfe6 --- /dev/null +++ b/offchain/authority-claimer/src/checker.rs @@ -0,0 +1,44 @@ +use async_trait::async_trait; +use rollups_events::RollupsClaim; +use snafu::Snafu; +use std::fmt::Debug; + +#[async_trait] +pub trait DuplicateChecker: Debug { + type Error: snafu::Error; + + async fn is_duplicated_rollups_claim( + &self, + rollups_claim: &RollupsClaim, + ) -> Result; +} + +// ------------------------------------------------------------------------------------------------ +// DefaultDuplicateChecker +// ------------------------------------------------------------------------------------------------ + +#[derive(Debug)] +pub struct DefaultDuplicateChecker; + +#[derive(Debug, Snafu)] +pub enum DefaultDuplicateCheckerError { + Todo, +} + +impl DefaultDuplicateChecker { + pub fn new() -> Result { + todo!() + } +} + +#[async_trait] +impl DuplicateChecker for DefaultDuplicateChecker { + type Error = DefaultDuplicateCheckerError; + + async fn is_duplicated_rollups_claim( + &self, + _rollups_claim: &RollupsClaim, + ) -> Result { + todo!() + } +} diff --git a/offchain/authority-claimer/src/claimer.rs b/offchain/authority-claimer/src/claimer.rs new file mode 100644 index 000000000..02dd6e8a1 --- /dev/null +++ b/offchain/authority-claimer/src/claimer.rs @@ -0,0 +1,85 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +use async_trait::async_trait; +use snafu::ResultExt; +use tracing::trace; + +use crate::{ + checker::DuplicateChecker, listener::BrokerListener, + sender::TransactionSender, +}; + +/// The `AuthorityClaimer` starts an event loop that waits for claim messages +/// from the broker, and then sends the claims to the blockchain. +/// +/// It uses a `BrokerListener` for listening for messages from the broker. +/// +/// It also uses a `TransactionSender` that interacts with the blockchain and +/// effectively submits the claims. +#[async_trait] +pub trait AuthorityClaimer { + async fn start( + &self, + broker_listener: L, + duplicate_checker: C, + transaction_sender: S, + ) -> Result<(), AuthorityClaimerError> + where + L: BrokerListener + Send + Sync, + C: DuplicateChecker + Send + Sync, + S: TransactionSender + Send, + { + trace!("Starting the authority claimer loop"); + let mut transaction_sender = transaction_sender; + loop { + let rollups_claim = broker_listener + .listen() + .await + .context(BrokerListenerSnafu)?; + trace!("Got a claim from the broker: {:?}", rollups_claim); + + let should_send_rollups_claim = !duplicate_checker + .is_duplicated_rollups_claim(&rollups_claim) + .await + .context(DuplicateCheckerSnafu)?; + + if should_send_rollups_claim { + transaction_sender = transaction_sender + .send_rollups_claim(rollups_claim) + .await + .context(TransactionSenderSnafu)? + } + } + } +} + +#[derive(Debug, snafu::Snafu)] +pub enum AuthorityClaimerError< + L: BrokerListener + 'static, + C: DuplicateChecker + 'static, + S: TransactionSender + 'static, +> { + #[snafu(display("broker listener error"))] + BrokerListenerError { source: L::Error }, + + #[snafu(display("duplicate checker error"))] + DuplicateCheckerError { source: C::Error }, + + #[snafu(display("transaction sender error"))] + TransactionSenderError { source: S::Error }, +} + +// ------------------------------------------------------------------------------------------------ +// DefaultAuthorityClaimer +// ------------------------------------------------------------------------------------------------ + +pub struct DefaultAuthorityClaimer; + +impl DefaultAuthorityClaimer { + pub fn new() -> Self { + Self + } +} + +impl AuthorityClaimer for DefaultAuthorityClaimer {} diff --git a/offchain/authority-claimer/src/config/cli.rs b/offchain/authority-claimer/src/config/cli.rs new file mode 100644 index 000000000..e5012f5d3 --- /dev/null +++ b/offchain/authority-claimer/src/config/cli.rs @@ -0,0 +1,138 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +use clap::{command, Parser}; +use eth_tx_manager::{ + config::{TxEnvCLIConfig as TxManagerCLIConfig, TxManagerConfig}, + Priority, +}; +use rollups_events::{BrokerCLIConfig, BrokerConfig}; +use rusoto_core::Region; +use snafu::ResultExt; +use std::{fs, path::PathBuf, str::FromStr}; + +use crate::config::{ + error::{ + AuthorityClaimerConfigError, InvalidRegionSnafu, MnemonicFileSnafu, + TxManagerSnafu, TxSigningConfigError, TxSigningSnafu, + }, + json::{read_json_file, DappDeployment}, + AuthorityClaimerConfig, TxSigningConfig, +}; + +// ------------------------------------------------------------------------------------------------ +// AuthorityClaimerCLI +// ------------------------------------------------------------------------------------------------ + +#[derive(Clone, Parser)] +#[command(name = "rd_config")] +#[command(about = "Configuration for rollups authority claimer")] +pub(crate) struct AuthorityClaimerCLI { + #[command(flatten)] + tx_manager_config: TxManagerCLIConfig, + + #[command(flatten)] + tx_signing_config: TxSigningCLIConfig, + + #[command(flatten)] + broker_config: BrokerCLIConfig, + + /// Path to a file with the deployment json of the dapp + #[arg(long, env, default_value = "./dapp_deployment.json")] + dapp_deployment_file: PathBuf, +} + +impl TryFrom for AuthorityClaimerConfig { + type Error = AuthorityClaimerConfigError; + + fn try_from(cli_config: AuthorityClaimerCLI) -> Result { + let tx_manager_config = + TxManagerConfig::initialize(cli_config.tx_manager_config) + .context(TxManagerSnafu)?; + + let tx_signing_config = + TxSigningConfig::try_from(cli_config.tx_signing_config) + .context(TxSigningSnafu)?; + + let broker_config = BrokerConfig::from(cli_config.broker_config); + + let dapp_deployment = + read_json_file::(cli_config.dapp_deployment_file)?; + let dapp_address = dapp_deployment.dapp_address; + let dapp_deploy_block_hash = dapp_deployment.dapp_deploy_block_hash; + + Ok(AuthorityClaimerConfig { + tx_manager_config, + tx_signing_config, + broker_config, + dapp_address, + dapp_deploy_block_hash, + tx_manager_priority: Priority::Normal, + }) + } +} + +// ------------------------------------------------------------------------------------------------ +// TxSigningConfig +// ------------------------------------------------------------------------------------------------ + +#[derive(Debug, Clone, Parser)] +#[command(name = "tx_signing_config")] +#[command( + about = "Configuration for signing authentication for the transactions" +)] +pub(crate) struct TxSigningCLIConfig { + /// Signer mnemonic, overrides `tx_signing_mnemonic_file` and `tx_signing_aws_kms_*` + #[arg(long, env)] + tx_signing_mnemonic: Option, + + /// Signer mnemonic file path, overrides `tx_signing_aws_kms_*` + #[arg(long, env)] + tx_signing_mnemonic_file: Option, + + /// Mnemonic account index + #[arg(long, env)] + tx_signing_mnemonic_account_index: Option, + + /// AWS KMS signer key-id + #[arg(long, env)] + tx_signing_aws_kms_key_id: Option, + + /// AWS KMS signer region + #[arg(long, env)] + tx_signing_aws_kms_region: Option, +} + +impl TryFrom for TxSigningConfig { + type Error = TxSigningConfigError; + + fn try_from(cli: TxSigningCLIConfig) -> Result { + let account_index = cli.tx_signing_mnemonic_account_index; + if let Some(mnemonic) = cli.tx_signing_mnemonic { + Ok(TxSigningConfig::Mnemonic { + mnemonic, + account_index, + }) + } else if let Some(path) = cli.tx_signing_mnemonic_file { + let mnemonic = fs::read_to_string(path.clone()) + .context(MnemonicFileSnafu { path })? + .trim() + .to_string(); + Ok(TxSigningConfig::Mnemonic { + mnemonic, + account_index, + }) + } else { + match (cli.tx_signing_aws_kms_key_id, cli.tx_signing_aws_kms_region) + { + (None, _) => Err(TxSigningConfigError::MissingConfiguration), + (Some(_), None) => Err(TxSigningConfigError::MissingRegion), + (Some(key_id), Some(region)) => { + let region = Region::from_str(®ion) + .context(InvalidRegionSnafu)?; + Ok(TxSigningConfig::Aws { key_id, region }) + } + } + } + } +} diff --git a/offchain/authority-claimer/src/config/error.rs b/offchain/authority-claimer/src/config/error.rs new file mode 100644 index 000000000..567029719 --- /dev/null +++ b/offchain/authority-claimer/src/config/error.rs @@ -0,0 +1,52 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +use eth_tx_manager::config::Error as TxManagerConfigError; +use rusoto_core::region::ParseRegionError; +use snafu::Snafu; +use std::path::PathBuf; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum AuthorityClaimerConfigError { + #[snafu(display("TxManager configuration error: {}", source))] + TxManagerError { source: TxManagerConfigError }, + + #[snafu(display("TxSigning configuration error: {}", source))] + TxSigningError { source: TxSigningConfigError }, + + #[snafu(display("Read file error ({})", path.display()))] + ReadFileError { + path: PathBuf, + source: std::io::Error, + }, + + #[snafu(display("Json parse error ({})", path.display()))] + JsonParseError { + path: PathBuf, + source: serde_json::Error, + }, +} + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum TxSigningConfigError { + #[snafu(display("Missing auth configuration"))] + MissingConfiguration, + + #[snafu(display( + "Could not read mnemonic file at path `{}`: {}", + path, + source + ))] + MnemonicFileError { + path: String, + source: std::io::Error, + }, + + #[snafu(display("Missing AWS region"))] + MissingRegion, + + #[snafu(display("Invalid AWS region"))] + InvalidRegion { source: ParseRegionError }, +} diff --git a/offchain/authority-claimer/src/config/json.rs b/offchain/authority-claimer/src/config/json.rs new file mode 100644 index 000000000..234f0a422 --- /dev/null +++ b/offchain/authority-claimer/src/config/json.rs @@ -0,0 +1,29 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +use rollups_events::{Address, Hash}; +use serde::{de::DeserializeOwned, Deserialize}; +use snafu::ResultExt; +use std::{fs::File, io::BufReader, path::PathBuf}; + +use crate::config::error::{ + AuthorityClaimerConfigError, JsonParseSnafu, ReadFileSnafu, +}; + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct DappDeployment { + #[serde(rename = "address")] + pub dapp_address: Address, + + #[serde(rename = "blockHash")] + pub dapp_deploy_block_hash: Hash, +} + +pub(crate) fn read_json_file( + path: PathBuf, +) -> Result { + let file = + File::open(&path).context(ReadFileSnafu { path: path.clone() })?; + let reader = BufReader::new(file); + serde_json::from_reader(reader).context(JsonParseSnafu { path }) +} diff --git a/offchain/authority-claimer/src/config/mod.rs b/offchain/authority-claimer/src/config/mod.rs new file mode 100644 index 000000000..728429973 --- /dev/null +++ b/offchain/authority-claimer/src/config/mod.rs @@ -0,0 +1,55 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +mod cli; +mod error; +mod json; + +pub use error::{AuthorityClaimerConfigError, TxSigningConfigError}; + +use cli::AuthorityClaimerCLI; +use eth_tx_manager::{config::TxManagerConfig, Priority}; +use http_server::HttpServerConfig; +use rollups_events::{Address, BrokerConfig, Hash}; +use rusoto_core::Region; + +#[derive(Debug, Clone)] +pub struct Config { + pub authority_claimer_config: AuthorityClaimerConfig, + pub http_server_config: HttpServerConfig, +} + +#[derive(Debug, Clone)] +pub struct AuthorityClaimerConfig { + pub tx_manager_config: TxManagerConfig, + pub tx_signing_config: TxSigningConfig, + pub broker_config: BrokerConfig, + pub dapp_address: Address, + pub dapp_deploy_block_hash: Hash, + pub tx_manager_priority: Priority, +} + +#[derive(Debug, Clone)] +pub enum TxSigningConfig { + Mnemonic { + mnemonic: String, + account_index: Option, + }, + + Aws { + key_id: String, + region: Region, + }, +} + +impl Config { + pub fn new() -> Result { + let (http_server_config, authority_claimer_cli) = + HttpServerConfig::parse::("authority_claimer"); + let authority_claimer_config = authority_claimer_cli.try_into()?; + Ok(Self { + authority_claimer_config, + http_server_config, + }) + } +} diff --git a/offchain/authority-claimer/src/lib.rs b/offchain/authority-claimer/src/lib.rs new file mode 100644 index 000000000..decfcc3ed --- /dev/null +++ b/offchain/authority-claimer/src/lib.rs @@ -0,0 +1,72 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pub mod checker; +pub mod claimer; +pub mod config; +pub mod listener; +pub mod metrics; +pub mod sender; + +use config::Config; +use rollups_events::DAppMetadata; +use snafu::Error; +use tracing::trace; + +use crate::{ + checker::DefaultDuplicateChecker, + claimer::{AuthorityClaimer, DefaultAuthorityClaimer}, + listener::DefaultBrokerListener, + metrics::AuthorityClaimerMetrics, + sender::DefaultTransactionSender, +}; + +pub async fn run(config: Config) -> Result<(), Box> { + tracing::info!(?config, "starting authority-claimer"); + + // Creating the metrics and health server. + let metrics = AuthorityClaimerMetrics::new(); + let http_server_handle = + http_server::start(config.http_server_config, metrics.clone().into()); + + let dapp_address = config.authority_claimer_config.dapp_address; + let dapp_metadata = DAppMetadata { + chain_id: config.authority_claimer_config.tx_manager_config.chain_id, + dapp_address, + }; + + // Creating the broker listener. + trace!("Creating the broker listener"); + let broker_listener = DefaultBrokerListener::new( + config.authority_claimer_config.broker_config.clone(), + dapp_metadata.clone(), + metrics.clone(), + ) + .map_err(Box::new)?; + + // Creating the duplicate checker. + trace!("Creating the duplicate checker"); + let duplicate_checker = DefaultDuplicateChecker::new().map_err(Box::new)?; + + // Creating the transaction sender. + trace!("Creating the transaction sender sender"); + let transaction_sender = + DefaultTransactionSender::new(dapp_metadata, metrics) + .map_err(Box::new)?; + + // Creating the claimer loop. + let authority_claimer = DefaultAuthorityClaimer::new(); + let claimer_handle = authority_claimer.start( + broker_listener, + duplicate_checker, + transaction_sender, + ); + + // Starting the HTTP server and the claimer loop. + tokio::select! { + ret = http_server_handle => { ret.map_err(Box::new)? } + ret = claimer_handle => { ret.map_err(Box::new)? } + }; + + unreachable!() +} diff --git a/offchain/authority-claimer/src/listener.rs b/offchain/authority-claimer/src/listener.rs new file mode 100644 index 000000000..1e676472f --- /dev/null +++ b/offchain/authority-claimer/src/listener.rs @@ -0,0 +1,62 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +use async_trait::async_trait; +use rollups_events::{BrokerConfig, DAppMetadata, RollupsClaim}; +use snafu::Snafu; +use std::fmt::Debug; + +use crate::metrics::AuthorityClaimerMetrics; + +/// The `BrokerListener` listens for new claims from the broker. +/// +/// The `listen` function should preferably yield to other processes while +/// waiting for new messages (instead of busy-waiting). +#[async_trait] +pub trait BrokerListener: Debug { + type Error: snafu::Error; + + async fn listen(&self) -> Result; +} + +// ------------------------------------------------------------------------------------------------ +// DefaultBrokerListener +// ------------------------------------------------------------------------------------------------ + +#[derive(Debug)] +pub struct DefaultBrokerListener; + +#[derive(Debug, Snafu)] +pub enum DefaultBrokerListenerError { + Todo, +} + +impl DefaultBrokerListener { + pub fn new( + _broker_config: BrokerConfig, + _dapp_metadata: DAppMetadata, + _metrics: AuthorityClaimerMetrics, + ) -> Result { + todo!() + } +} + +#[async_trait] +impl BrokerListener for DefaultBrokerListener { + type Error = DefaultBrokerListenerError; + + async fn listen(&self) -> Result { + todo!() + } +} + +// impl Stream for BrokerListener { +// type Item = u32; +// +// fn poll_next( +// self: Pin<&mut Self>, +// cx: &mut Context<'_>, +// ) -> Poll> { +// todo!() +// } +// } diff --git a/offchain/authority-claimer/src/main.rs b/offchain/authority-claimer/src/main.rs new file mode 100644 index 000000000..3febe0c7f --- /dev/null +++ b/offchain/authority-claimer/src/main.rs @@ -0,0 +1,20 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +use authority_claimer::config::Config; +use std::error::Error; +use tracing_subscriber::filter::{EnvFilter, LevelFilter}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Settin up the logging environment. + let env_filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + + // Getting the configuration. + let config = Config::new().map_err(Box::new)?; + + authority_claimer::run(config).await +} diff --git a/offchain/authority-claimer/src/metrics.rs b/offchain/authority-claimer/src/metrics.rs new file mode 100644 index 000000000..3ffd67a66 --- /dev/null +++ b/offchain/authority-claimer/src/metrics.rs @@ -0,0 +1,34 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +use http_server::{CounterRef, FamilyRef, Registry}; +use rollups_events::DAppMetadata; + +const METRICS_PREFIX: &str = "cartesi_rollups_authority_claimer"; + +fn prefixed_metrics(name: &str) -> String { + format!("{}_{}", METRICS_PREFIX, name) +} + +#[derive(Debug, Clone, Default)] +pub struct AuthorityClaimerMetrics { + pub claims_sent: FamilyRef, +} + +impl AuthorityClaimerMetrics { + pub fn new() -> Self { + Self::default() + } +} + +impl From for Registry { + fn from(metrics: AuthorityClaimerMetrics) -> Self { + let mut registry = Registry::default(); + registry.register( + prefixed_metrics("claims_sent"), + "Counts the number of claims sent", + metrics.claims_sent, + ); + registry + } +} diff --git a/offchain/authority-claimer/src/sender.rs b/offchain/authority-claimer/src/sender.rs new file mode 100644 index 000000000..e3816eaf9 --- /dev/null +++ b/offchain/authority-claimer/src/sender.rs @@ -0,0 +1,58 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +use async_trait::async_trait; +use rollups_events::{DAppMetadata, RollupsClaim}; +use snafu::Snafu; +use std::fmt::Debug; + +use crate::metrics::AuthorityClaimerMetrics; + +/// The `TransactionSender` sends claims to the blockchain. +/// +/// It should wait for N blockchain confirmations. +#[async_trait] +pub trait TransactionSender: Sized + Debug { + type Error: snafu::Error; + + /// The `send_rollups_claim` function consumes the `TransactionSender` + /// object and then returns it to avoid that processes use the transaction + /// sender concurrently. + async fn send_rollups_claim( + self, + rollups_claim: RollupsClaim, + ) -> Result; +} + +// ------------------------------------------------------------------------------------------------ +// DefaultTransactionSender +// ------------------------------------------------------------------------------------------------ + +#[derive(Debug, Clone)] +pub struct DefaultTransactionSender; + +#[derive(Debug, Snafu)] +pub enum DefaultTransactionSenderError { + Todo, +} + +impl DefaultTransactionSender { + pub fn new( + _dapp_metadata: DAppMetadata, + _metrics: AuthorityClaimerMetrics, + ) -> Result { + todo!() + } +} + +#[async_trait] +impl TransactionSender for DefaultTransactionSender { + type Error = DefaultTransactionSenderError; + + async fn send_rollups_claim( + self, + _rollups_claim: RollupsClaim, + ) -> Result { + todo!() + } +} diff --git a/offchain/http-server/src/config.rs b/offchain/http-server/src/config.rs index c32595753..7a32ac82d 100644 --- a/offchain/http-server/src/config.rs +++ b/offchain/http-server/src/config.rs @@ -5,7 +5,7 @@ use clap::{ value_parser, Arg, Command, CommandFactory, FromArgMatches, Parser, }; -#[derive(Debug, Parser)] +#[derive(Debug, Clone, Parser)] pub struct HttpServerConfig { pub(crate) port: u16, } @@ -14,6 +14,9 @@ impl HttpServerConfig { /// Returns the HTTP server config and the app's config after parsing /// it from the command line and/or environment variables. /// + /// The parameter `service` must be a lowercase string that + /// uses underlines as spaces. + /// /// The parametric type `C` must be a struct that derives `Parser`. pub fn parse( service: &'static str, diff --git a/offchain/http-server/src/lib.rs b/offchain/http-server/src/lib.rs index edca9f547..f6c36f36c 100644 --- a/offchain/http-server/src/lib.rs +++ b/offchain/http-server/src/lib.rs @@ -13,6 +13,9 @@ pub use prometheus_client::metrics::counter::Counter as CounterRef; pub use prometheus_client::metrics::family::Family as FamilyRef; // End of metrics to re-export. +// Re-exporting hyper error. +pub use hyper::Error as HttpServerError; + use axum::{routing::get, Router}; use prometheus_client::encoding::text::encode; use std::{