diff --git a/Cargo.lock b/Cargo.lock index ce3b556c20..f2e1232748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5080,6 +5080,7 @@ version = "0.1.0" dependencies = [ "alloy-eips", "alloy-primitives", + "alloy-serde", "jsonrpsee", "kona-interop", "kona-protocol", diff --git a/crates/supervisor/core/src/rpc/server.rs b/crates/supervisor/core/src/rpc/server.rs index 45467f3ca6..cbeb318596 100644 --- a/crates/supervisor/core/src/rpc/server.rs +++ b/crates/supervisor/core/src/rpc/server.rs @@ -5,12 +5,12 @@ use alloy_eips::eip1898::BlockNumHash; use alloy_primitives::{B256, ChainId, map::HashMap}; use async_trait::async_trait; use jsonrpsee::{core::RpcResult, types::ErrorObject}; -use kona_interop::{ - DependencySet, DerivedIdPair, ExecutingDescriptor, SafetyLevel, SuperRootOutput, -}; +use kona_interop::{DependencySet, DerivedIdPair, ExecutingDescriptor, SafetyLevel}; use kona_protocol::BlockInfo; -use kona_supervisor_rpc::{SupervisorApiServer, SupervisorChainSyncStatus, SupervisorSyncStatus}; -use kona_supervisor_types::{HexChainId, SuperHead}; +use kona_supervisor_rpc::{ + SuperRootOutputRpc, SupervisorApiServer, SupervisorChainSyncStatus, SupervisorSyncStatus, +}; +use kona_supervisor_types::{HexStringU64, SuperHead}; use std::sync::Arc; use tracing::{trace, warn}; @@ -39,7 +39,7 @@ where { async fn cross_derived_to_source( &self, - chain_id_hex: HexChainId, + chain_id_hex: HexStringU64, derived: BlockNumHash, ) -> RpcResult { let chain_id = ChainId::from(chain_id_hex); @@ -71,7 +71,7 @@ where ) } - async fn local_unsafe(&self, chain_id_hex: HexChainId) -> RpcResult { + async fn local_unsafe(&self, chain_id_hex: HexStringU64) -> RpcResult { let chain_id = ChainId::from(chain_id_hex); crate::observe_rpc_call!( "local_unsafe", @@ -101,7 +101,7 @@ where ) } - async fn cross_safe(&self, chain_id_hex: HexChainId) -> RpcResult { + async fn cross_safe(&self, chain_id_hex: HexStringU64) -> RpcResult { let chain_id = ChainId::from(chain_id_hex); crate::observe_rpc_call!( "cross_safe", @@ -120,7 +120,7 @@ where ) } - async fn finalized(&self, chain_id_hex: HexChainId) -> RpcResult { + async fn finalized(&self, chain_id_hex: HexStringU64) -> RpcResult { let chain_id = ChainId::from(chain_id_hex); crate::observe_rpc_call!( "finalized", @@ -147,22 +147,27 @@ where ) } - async fn super_root_at_timestamp(&self, timestamp: u64) -> RpcResult { + async fn super_root_at_timestamp( + &self, + timestamp_hex: HexStringU64, + ) -> RpcResult { crate::observe_rpc_call!( "super_root_at_timestamp", async { - trace!(target: "supervisor_rpc", - %timestamp, - "Received super_root_at_timestamp request" - ); - - self.supervisor.super_root_at_timestamp(timestamp) - .await - .map_err(|err| { - warn!(target: "supervisor_rpc", %err, "Error from core supervisor super_root_at_timestamp"); - ErrorObject::from(err) - }) - }.await) + let timestamp = u64::from(timestamp_hex); + trace!(target: "supervisor_rpc", + %timestamp, + "Received super_root_at_timestamp request" + ); + + self.supervisor.super_root_at_timestamp(timestamp) + .await + .map_err(|err| { + warn!(target: "supervisor_rpc", %err, "Error from core supervisor super_root_at_timestamp"); + ErrorObject::from(err) + }) + }.await + ) } async fn check_access_list( @@ -175,19 +180,20 @@ where crate::observe_rpc_call!( "check_access_list", async { - trace!(target: "supervisor_rpc", - num_inbox_entries = inbox_entries.len(), - ?min_safety, - ?executing_descriptor, - "Received check_access_list request", - ); - self.supervisor - .check_access_list(inbox_entries, min_safety, executing_descriptor) - .map_err(|err| { - warn!(target: "supervisor_rpc", %err, "Error from core supervisor check_access_list"); - ErrorObject::from(err) - }) - }.await) + trace!(target: "supervisor_rpc", + num_inbox_entries = inbox_entries.len(), + ?min_safety, + ?executing_descriptor, + "Received check_access_list request", + ); + self.supervisor + .check_access_list(inbox_entries, min_safety, executing_descriptor) + .map_err(|err| { + warn!(target: "supervisor_rpc", %err, "Error from core supervisor check_access_list"); + ErrorObject::from(err) + }) + }.await + ) } async fn sync_status(&self) -> RpcResult { @@ -361,7 +367,7 @@ mod tests { async fn super_root_at_timestamp( &self, _timestamp: u64, - ) -> Result { + ) -> Result { unimplemented!() } } diff --git a/crates/supervisor/core/src/supervisor.rs b/crates/supervisor/core/src/supervisor.rs index 6ed1d8d579..25653e15d5 100644 --- a/crates/supervisor/core/src/supervisor.rs +++ b/crates/supervisor/core/src/supervisor.rs @@ -6,10 +6,11 @@ use alloy_rpc_client::RpcClient; use async_trait::async_trait; use core::fmt::Debug; use kona_interop::{ - ChainRootInfo, DependencySet, ExecutingDescriptor, OutputRootWithChain, SUPER_ROOT_VERSION, - SafetyLevel, SuperRoot, SuperRootOutput, + DependencySet, ExecutingDescriptor, OutputRootWithChain, SUPER_ROOT_VERSION, SafetyLevel, + SuperRoot, }; use kona_protocol::BlockInfo; +use kona_supervisor_rpc::{ChainRootInfoRpc, SuperRootOutputRpc}; use kona_supervisor_storage::{ ChainDb, ChainDbFactory, DerivationStorageReader, DerivationStorageWriter, FinalizedL1Storage, HeadRefStorageReader, LogStorageReader, LogStorageWriter, @@ -86,7 +87,7 @@ pub trait SupervisorService: Debug + Send + Sync { async fn super_root_at_timestamp( &self, timestamp: u64, - ) -> Result; + ) -> Result; /// Verifies if an access-list references only valid messages fn check_access_list( @@ -393,12 +394,12 @@ impl SupervisorService for Supervisor { async fn super_root_at_timestamp( &self, timestamp: u64, - ) -> Result { + ) -> Result { let mut chain_ids = self.config.dependency_set.dependencies.keys().collect::>(); // Sorting chain ids for deterministic super root hash chain_ids.sort(); - let mut chain_infos = Vec::::with_capacity(chain_ids.len()); + let mut chain_infos = Vec::::with_capacity(chain_ids.len()); let mut super_root_chains = Vec::::with_capacity(chain_ids.len()); let mut cross_safe_source = BlockNumHash::default(); @@ -413,7 +414,7 @@ impl SupervisorService for Supervisor { serde_json::to_string(&pending_output_v0).unwrap().as_bytes(), ); - chain_infos.push(ChainRootInfo { + chain_infos.push(ChainRootInfoRpc { chain_id: *id, canonical: canonical_root, pending: pending_output_v0_bytes, @@ -424,11 +425,9 @@ impl SupervisorService for Supervisor { let l2_block = managed_node.l2_block_ref_by_timestamp(timestamp).await?; let source = self - .get_db(*id)? - .derived_to_source(l2_block.id()) - .map_err(|err| { + .derived_to_source_block(*id, l2_block.id()) + .inspect_err(|err| { error!(target: "supervisor_service", %id, %err, "Failed to get derived to source block for chain"); - SpecError::from(err) })?; if cross_safe_source.number == 0 || cross_safe_source.number < source.number { @@ -437,10 +436,9 @@ impl SupervisorService for Supervisor { } let super_root = SuperRoot { timestamp, output_roots: super_root_chains }; - let super_root_hash = super_root.hash(); - Ok(SuperRootOutput { + Ok(SuperRootOutputRpc { cross_safe_derived_from: cross_safe_source, timestamp, super_root: super_root_hash, diff --git a/crates/supervisor/rpc/Cargo.toml b/crates/supervisor/rpc/Cargo.toml index 8e4d6f4095..8c5c35b7eb 100644 --- a/crates/supervisor/rpc/Cargo.toml +++ b/crates/supervisor/rpc/Cargo.toml @@ -23,16 +23,15 @@ kona-supervisor-types.workspace = true # jsonrpsee serde.workspace = true +serde_json.workspace = true jsonrpsee = { workspace = true, optional = true, features = ["macros", "server"] } # Alloy alloy-eips.workspace = true +alloy-serde.workspace = true alloy-primitives = { workspace = true, features = ["map", "rlp", "serde"] } op-alloy-consensus.workspace = true -#[dev-dependencies] -serde_json.workspace = true - [features] serde = [ "alloy-eips/serde", diff --git a/crates/supervisor/rpc/src/jsonrpsee.rs b/crates/supervisor/rpc/src/jsonrpsee.rs index ada988367e..693ee6223d 100644 --- a/crates/supervisor/rpc/src/jsonrpsee.rs +++ b/crates/supervisor/rpc/src/jsonrpsee.rs @@ -5,16 +5,15 @@ pub use jsonrpsee::{ types::{ErrorCode, ErrorObjectOwned}, }; -use crate::SupervisorSyncStatus; +use crate::{SuperRootOutputRpc, SupervisorSyncStatus}; use alloy_eips::BlockNumHash; use alloy_primitives::{B256, BlockHash, ChainId, map::HashMap}; use jsonrpsee::proc_macros::rpc; use kona_interop::{ DependencySet, DerivedIdPair, DerivedRefPair, ExecutingDescriptor, ManagedEvent, SafetyLevel, - SuperRootOutput, }; use kona_protocol::BlockInfo; -use kona_supervisor_types::{BlockSeal, HexChainId, OutputV0, Receipts, SubscriptionEvent}; +use kona_supervisor_types::{BlockSeal, HexStringU64, OutputV0, Receipts, SubscriptionEvent}; use serde::{Deserialize, Serialize}; /// Supervisor API for interop. @@ -28,7 +27,7 @@ pub trait SupervisorApi { #[method(name = "crossDerivedToSource")] async fn cross_derived_to_source( &self, - chain_id: HexChainId, + chain_id: HexStringU64, block_id: BlockNumHash, ) -> RpcResult; @@ -38,7 +37,7 @@ pub trait SupervisorApi { /// /// [`LocalUnsafe`]: SafetyLevel::LocalUnsafe #[method(name = "localUnsafe")] - async fn local_unsafe(&self, chain_id: HexChainId) -> RpcResult; + async fn local_unsafe(&self, chain_id: HexStringU64) -> RpcResult; /// Returns the [`CrossSafe`] block for given chain. /// @@ -46,7 +45,7 @@ pub trait SupervisorApi { /// /// [`CrossSafe`]: SafetyLevel::CrossSafe #[method(name = "crossSafe")] - async fn cross_safe(&self, chain_id: HexChainId) -> RpcResult; + async fn cross_safe(&self, chain_id: HexStringU64) -> RpcResult; /// Returns the [`Finalized`] block for the given chain. /// @@ -54,7 +53,7 @@ pub trait SupervisorApi { /// /// [`Finalized`]: SafetyLevel::Finalized #[method(name = "finalized")] - async fn finalized(&self, chain_id: HexChainId) -> RpcResult; + async fn finalized(&self, chain_id: HexStringU64) -> RpcResult; /// Returns the finalized L1 block that the supervisor is synced to. /// @@ -75,7 +74,10 @@ pub trait SupervisorApi { /// [`SuperRoot`]: kona_interop::SuperRoot /// [`ChainRootInfo`]: kona_interop::ChainRootInfo #[method(name = "superRootAtTimestamp")] - async fn super_root_at_timestamp(&self, timestamp: u64) -> RpcResult; + async fn super_root_at_timestamp( + &self, + timestamp: HexStringU64, + ) -> RpcResult; /// Verifies if an access-list references only valid messages w.r.t. locally configured minimum /// [`SafetyLevel`]. diff --git a/crates/supervisor/rpc/src/lib.rs b/crates/supervisor/rpc/src/lib.rs index 054a89018c..0ba13757a7 100644 --- a/crates/supervisor/rpc/src/lib.rs +++ b/crates/supervisor/rpc/src/lib.rs @@ -11,6 +11,8 @@ pub use jsonrpsee::SupervisorApiServer; pub use jsonrpsee::ManagedModeApiClient; pub mod response; -pub use response::{SupervisorChainSyncStatus, SupervisorSyncStatus}; +pub use response::{ + ChainRootInfoRpc, SuperRootOutputRpc, SupervisorChainSyncStatus, SupervisorSyncStatus, +}; pub use kona_protocol::BlockInfo; diff --git a/crates/supervisor/rpc/src/response.rs b/crates/supervisor/rpc/src/response.rs index 7a6ce100fc..4d9443e566 100644 --- a/crates/supervisor/rpc/src/response.rs +++ b/crates/supervisor/rpc/src/response.rs @@ -1,9 +1,10 @@ //! Supervisor RPC response types. use alloy_eips::BlockNumHash; -use alloy_primitives::{ChainId, map::HashMap}; +use alloy_primitives::{B256, Bytes, ChainId, map::HashMap}; use kona_protocol::BlockInfo; use kona_supervisor_types::SuperHead; +use serde::{Deserialize, Serialize, Serializer}; /// Describes superchain sync status. /// @@ -80,10 +81,66 @@ impl From for SupervisorChainSyncStatus { } } +/// This is same as [`kona_interop::ChainRootInfo`] but with [`u64`] serializeing as a valid hex +/// string. +/// +/// Required by +/// [`super_root_at_timestamp`](crate::jsonrpsee::SupervisorApiServer::super_root_at_timestamp) RPC +/// for marshalling and unmarshalling in GO implementation. Required for e2e tests. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChainRootInfoRpc { + /// The chain ID. + #[serde(rename = "chainID", with = "alloy_serde::quantity")] + pub chain_id: ChainId, + /// The canonical output root of the latest canonical block at a particular timestamp. + pub canonical: B256, + /// The pending output root. + /// + /// This is the output root preimage for the latest block at a particular timestamp prior to + /// validation of executing messages. If the original block was valid, this will be the + /// preimage of the output root from the `canonical` array. If it was invalid, it will be + /// the output root preimage from the optimistic block deposited transaction added to the + /// deposit-only block. + pub pending: Bytes, +} + +/// This is same as [`kona_interop::SuperRootOutput`] but with timestamp serializing as a valid hex +/// string. version is also serialized as an even length hex string. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SuperRootOutputRpc { + /// The Highest L1 Block that is cross-safe among all chains. + pub cross_safe_derived_from: BlockNumHash, + /// The timestamp of the super root. + #[serde(with = "alloy_serde::quantity")] + pub timestamp: u64, + /// The super root hash. + pub super_root: B256, + /// The version of the super root. + #[serde(serialize_with = "serialize_u8_as_hex")] + pub version: u8, + /// The chain root info for each chain in the dependency set. + /// It represents the state of the chain at or before the timestamp. + pub chains: Vec, +} + +/// Serializes a [u8] as a hex string. Ensure that the hex string has an even length. +/// +/// This is used to serialize the [`SuperRootOutputRpc`]'s version field as a hex string. +fn serialize_u8_as_hex(value: &u8, serializer: S) -> Result +where + S: Serializer, +{ + let hex_string = format!("0x{:02x}", value); + serializer.serialize_str(&hex_string) +} + #[cfg(test)] mod test { use super::*; use alloy_primitives::b256; + use kona_interop::SUPER_ROOT_VERSION; const CHAIN_STATUS: &str = r#" { @@ -242,4 +299,23 @@ mod test { } ) } + + #[test] + fn test_super_root_version_even_length_hex() { + let root = SuperRootOutputRpc { + cross_safe_derived_from: BlockNumHash::default(), + timestamp: 0, + super_root: B256::default(), + version: SUPER_ROOT_VERSION, + chains: vec![], + }; + let json = serde_json::to_string(&root).expect("should serialize"); + let v: serde_json::Value = serde_json::from_str(&json).expect("valid json"); + let version_field = + v.get("version").expect("version field present").as_str().expect("version is string"); + let hex_part = &version_field[2..]; // remove 0x + assert_eq!(hex_part.len() % 2, 0, "Hex string should have even length"); + // For SUPER_ROOT_VERSION = 1, should be 0x01 + assert_eq!(version_field, "0x01"); + } } diff --git a/crates/supervisor/types/src/chain_id.rs b/crates/supervisor/types/src/hex_string_u64.rs similarity index 56% rename from crates/supervisor/types/src/chain_id.rs rename to crates/supervisor/types/src/hex_string_u64.rs index 03aefaf786..7df7641804 100644 --- a/crates/supervisor/types/src/chain_id.rs +++ b/crates/supervisor/types/src/hex_string_u64.rs @@ -1,11 +1,9 @@ -use alloy_primitives::ChainId; - -/// A wrapper around `ChainId` that supports hex string (e.g. `"0x1"`) or numeric deserialization +/// A wrapper around `u64` that supports hex string (e.g. `"0x1"`) or numeric deserialization /// for RPC inputs. -#[derive(Debug, Clone, Copy)] -pub struct HexChainId(pub ChainId); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HexStringU64(pub u64); -impl serde::Serialize for HexChainId { +impl serde::Serialize for HexStringU64 { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -14,7 +12,7 @@ impl serde::Serialize for HexChainId { } } -impl<'de> serde::Deserialize<'de> for HexChainId { +impl<'de> serde::Deserialize<'de> for HexStringU64 { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -24,12 +22,18 @@ impl<'de> serde::Deserialize<'de> for HexChainId { } } -impl From for ChainId { - fn from(value: HexChainId) -> Self { +impl From for u64 { + fn from(value: HexStringU64) -> Self { value.0 } } +impl From for HexStringU64 { + fn from(value: u64) -> Self { + Self(value) + } +} + #[cfg(test)] mod tests { use super::*; @@ -37,23 +41,23 @@ mod tests { #[test] fn test_deserialize_from_hex_string() { let json = r#""0x1a""#; - let parsed: HexChainId = serde_json::from_str(json).expect("should parse hex string"); - let chain_id: ChainId = parsed.into(); + let parsed: HexStringU64 = serde_json::from_str(json).expect("should parse hex string"); + let chain_id: u64 = parsed.0; assert_eq!(chain_id, 0x1a); } #[test] fn test_serialize_to_hex() { - let value = HexChainId(26); + let value = HexStringU64(26); let json = serde_json::to_string(&value).expect("should serialize"); assert_eq!(json, r#""0x1a""#); } #[test] fn test_round_trip() { - let original = HexChainId(12345); + let original = HexStringU64(12345); let json = serde_json::to_string(&original).unwrap(); - let parsed: HexChainId = serde_json::from_str(&json).unwrap(); + let parsed: HexStringU64 = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.0, original.0); } } diff --git a/crates/supervisor/types/src/lib.rs b/crates/supervisor/types/src/lib.rs index 202cd4f023..55e27f33fb 100644 --- a/crates/supervisor/types/src/lib.rs +++ b/crates/supervisor/types/src/lib.rs @@ -18,9 +18,9 @@ pub use receipt::Receipts; mod access_list; pub use access_list::{Access, AccessListError, parse_access_list}; -mod chain_id; +mod hex_string_u64; mod types; -pub use chain_id::HexChainId; +pub use hex_string_u64::HexStringU64; pub use types::{BlockSeal, OutputV0, SubscriptionEvent}; diff --git a/tests/supervisor/rpc_test.go b/tests/supervisor/rpc_test.go index 330cd6b692..b53e2efffc 100644 --- a/tests/supervisor/rpc_test.go +++ b/tests/supervisor/rpc_test.go @@ -13,12 +13,15 @@ import ( "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" gethTypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// TODO: add test for dependencySetV1 after devstack support is added to the QueryAPI + func TestRPCLocalUnsafe(gt *testing.T) { t := devtest.ParallelT(gt) sys := presets.NewSimpleInterop(t) @@ -85,6 +88,43 @@ func TestRPCFinalized(gt *testing.T) { } } +func TestRPCFinalizedL1(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + t.Run("succeeds to get finalized L1 block", func(gt devtest.T) { + block, err := client.QueryAPI().FinalizedL1(context.Background()) + require.NoError(t, err) + assert.Greater(t, block.Number, uint64(0)) + assert.Less(t, block.Time, uint64(time.Now().Unix()+5)) + assert.Len(t, block.Hash, 32) + }) +} + +func TestRPCSuperRootAtTimestamp(gt *testing.T) { + t := devtest.ParallelT(gt) + sys := presets.NewSimpleInterop(t) + client := sys.Supervisor.Escape() + + t.Run("fails with invalid timestamp", func(gt devtest.T) { + _, err := client.QueryAPI().SuperRootAtTimestamp(context.Background(), 0) + require.Error(t, err) + }) + + t.Run("succeeds with valid timestamp", func(gt devtest.T) { + timeNow := uint64(time.Now().Unix()) + root, err := client.QueryAPI().SuperRootAtTimestamp(context.Background(), hexutil.Uint64(timeNow-90)) + require.NoError(t, err) + assert.Len(t, root.SuperRoot, 32) + assert.Len(t, root.Chains, 2) + + for _, chain := range root.Chains { + assert.Len(t, chain.Canonical, 32) + assert.Contains(t, []eth.ChainID{sys.L2ChainA.ChainID(), sys.L2ChainB.ChainID()}, chain.ChainID) + } + }) +} + func TestRPCAllSafeDerivedAt(gt *testing.T) { t := devtest.ParallelT(gt)