diff --git a/Cargo.lock b/Cargo.lock index a1d53ea..c2a73df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4436,6 +4436,7 @@ dependencies = [ "eyre", "odyssey-node", "odyssey-wallet", + "odyssey-walltime", "reth-cli-util", "reth-node-builder", "reth-optimism-cli", @@ -4518,6 +4519,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "odyssey-walltime" +version = "0.0.0" +dependencies = [ + "futures", + "jsonrpsee", + "reth-chain-state", + "serde", + "tokio", +] + [[package]] name = "once_cell" version = "1.20.2" diff --git a/Cargo.toml b/Cargo.toml index 5fd4bb7..8877b33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/precompile", "crates/testing", "crates/wallet", + "crates/walltime", ] default-members = ["bin/odyssey/"] resolver = "2" @@ -137,6 +138,7 @@ strip = false odyssey-node = { path = "crates/node" } odyssey-precompile = { path = "crates/precompile" } odyssey-wallet = { path = "crates/wallet" } +odyssey-walltime = { path = "crates/walltime" } alloy = { version = "0.4", features = [ "contract", @@ -191,6 +193,7 @@ reth-tracing = { git = "https://github.com/paradigmxyz/reth.git", rev = "75dda1c reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth.git", rev = "75dda1c" } reth-network = { git = "https://github.com/paradigmxyz/reth.git", rev = "75dda1c" } reth-network-types = { git = "https://github.com/paradigmxyz/reth.git", rev = "75dda1c" } +reth-chain-state = { git = "https://github.com/paradigmxyz/reth.git", rev = "75dda1c" } # rpc jsonrpsee = "0.24" @@ -202,6 +205,7 @@ tracing = "0.1.0" serde = "1" serde_json = "1" thiserror = "1" +futures = "0.3" # misc-testing rstest = "0.18.2" diff --git a/bin/odyssey/Cargo.toml b/bin/odyssey/Cargo.toml index 06f23b4..e0db185 100644 --- a/bin/odyssey/Cargo.toml +++ b/bin/odyssey/Cargo.toml @@ -17,6 +17,7 @@ alloy-network.workspace = true alloy-primitives.workspace = true odyssey-node.workspace = true odyssey-wallet.workspace = true +odyssey-walltime.workspace = true eyre.workspace = true tracing.workspace = true reth-cli-util.workspace = true diff --git a/bin/odyssey/src/main.rs b/bin/odyssey/src/main.rs index 8a95eb2..d9f09c4 100644 --- a/bin/odyssey/src/main.rs +++ b/bin/odyssey/src/main.rs @@ -30,11 +30,12 @@ use clap::Parser; use eyre::Context; use odyssey_node::{chainspec::OdysseyChainSpecParser, node::OdysseyNode}; use odyssey_wallet::{OdysseyWallet, OdysseyWalletApiServer}; +use odyssey_walltime::{OdysseyWallTime, OdysseyWallTimeRpcApiServer}; use reth_node_builder::{engine_tree_config::TreeConfig, EngineNodeLauncher}; use reth_optimism_cli::Cli; use reth_optimism_node::{args::RollupArgs, node::OptimismAddOns}; use reth_optimism_rpc::sequencer::SequencerClient; -use reth_provider::providers::BlockchainProvider2; +use reth_provider::{providers::BlockchainProvider2, CanonStateSubscriptions}; use tracing::{info, warn}; #[global_allocator] @@ -93,6 +94,10 @@ fn main() { warn!(target: "reth::cli", "EXP0001 wallet not configured"); } + let walltime = OdysseyWallTime::spawn(ctx.provider().canonical_state_stream()); + ctx.modules.merge_configured(walltime.into_rpc())?; + info!(target: "reth::cli", "Walltime configured"); + Ok(()) }) .launch_with_fn(|builder| { diff --git a/crates/walltime/Cargo.toml b/crates/walltime/Cargo.toml new file mode 100644 index 0000000..ed5c377 --- /dev/null +++ b/crates/walltime/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "odyssey-walltime" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[dependencies] +reth-chain-state.workspace = true + +jsonrpsee = { workspace = true, features = ["server", "macros"] } +serde = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["sync"] } +futures.workspace = true + + +[dev-dependencies] +jsonrpsee = { workspace = true, features = ["server", "client", "macros"] } diff --git a/crates/walltime/src/lib.rs b/crates/walltime/src/lib.rs new file mode 100644 index 0000000..25626c8 --- /dev/null +++ b/crates/walltime/src/lib.rs @@ -0,0 +1,107 @@ +//! # Odyssey walltime +//! +//! Returns the current walltime and the chain's tip timestamps. + +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +use futures::{Stream, StreamExt}; +use jsonrpsee::{ + core::{async_trait, RpcResult}, + proc_macros::rpc, + types::{error::INTERNAL_ERROR_CODE, ErrorObject}, +}; +use reth_chain_state::CanonStateNotification; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::RwLock; + +/// The odyssey walltime endpoint. +#[derive(Debug, Clone)] +pub struct OdysseyWallTime { + inner: Arc, +} + +impl OdysseyWallTime { + /// Creates a new instance with the connected stream. + pub fn spawn(mut st: St) -> Self + where + St: Stream + Send + Unpin + 'static, + { + let walltime = Self { inner: Default::default() }; + let listener = walltime.clone(); + tokio::task::spawn(async move { + while let Some(notification) = st.next().await { + let tip = BlockTimeData { + wall_time_ms: notification.tip().timestamp, + block_timestamp: unix_epoch_ms(), + }; + *listener.inner.block_time_data.write().await = Some(tip); + } + }); + walltime + } + + /// Returns the currently tracked [`BlockTimeData`] if any. + async fn current_block_time(&self) -> Option { + *self.inner.block_time_data.read().await + } +} + +/// Implementation of the Odyssey `odyssey_getWallTimeData` endpoint. +#[derive(Debug, Default)] +struct OdysseyWallTimeInner { + /// Tracks the recent blocktime data + block_time_data: RwLock>, +} + +/// Data about the current time and the last block's. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct WallTimeData { + /// Wall time right now + current_wall_time_ms: u64, + /// Wall time of last block + last_block_wall_time_ms: u64, + /// Timestamp of last block (chain time) + last_block_timestamp: u64, +} + +/// Rpc endpoints +#[cfg_attr(not(test), rpc(server, namespace = "odyssey"))] +#[cfg_attr(test, rpc(server, client, namespace = "odyssey"))] +pub trait OdysseyWallTimeRpcApi { + /// Return the wall time and block timestamp of the latest block. + #[method(name = "getWallTimeData")] + async fn get_timedata(&self) -> RpcResult; +} + +#[async_trait] +impl OdysseyWallTimeRpcApiServer for OdysseyWallTime { + async fn get_timedata(&self) -> RpcResult { + let Some(current) = self.current_block_time().await else { + return Err(ErrorObject::owned(INTERNAL_ERROR_CODE, "node is not synced", None::<()>)); + }; + Ok(WallTimeData { + current_wall_time_ms: unix_epoch_ms(), + last_block_wall_time_ms: current.wall_time_ms, + last_block_timestamp: current.block_timestamp, + }) + } +} + +/// Time data about the last block. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct BlockTimeData { + /// Wall time of last block + wall_time_ms: u64, + /// Timestamp of last block (chain time) + block_timestamp: u64, +} + +/// Returns the current unix epoch in milliseconds. +pub fn unix_epoch_ms() -> u64 { + use std::time::SystemTime; + let now = SystemTime::now(); + now.duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_else(|err| panic!("Current time {now:?} is invalid: {err:?}")) + .as_millis() as u64 +}