From 91caec29a3271ea0700f8efb5fadb04f4dba7117 Mon Sep 17 00:00:00 2001 From: itschaindev Date: Fri, 1 Aug 2025 12:24:55 +0200 Subject: [PATCH 1/3] reset managed node after rewinding db --- .../supervisor/core/src/l1_watcher/watcher.rs | 8 ++- crates/supervisor/core/src/reorg/handler.rs | 11 ++- crates/supervisor/core/src/reorg/task.rs | 69 +++++++++++++++++-- crates/supervisor/core/src/supervisor.rs | 8 ++- crates/supervisor/core/src/syncnode/node.rs | 5 ++ .../supervisor/core/src/syncnode/resetter.rs | 4 +- 6 files changed, 91 insertions(+), 14 deletions(-) diff --git a/crates/supervisor/core/src/l1_watcher/watcher.rs b/crates/supervisor/core/src/l1_watcher/watcher.rs index 0c9d69a1fa..6e25d2120c 100644 --- a/crates/supervisor/core/src/l1_watcher/watcher.rs +++ b/crates/supervisor/core/src/l1_watcher/watcher.rs @@ -212,7 +212,10 @@ where #[cfg(test)] mod tests { use super::*; - use crate::SupervisorError; + use crate::{ + SupervisorError, + syncnode::{Client, resetter::Resetter}, + }; use alloy_primitives::B256; use alloy_transport::mock::*; use kona_supervisor_storage::{ChainDb, FinalizedL1Storage, StorageError}; @@ -242,7 +245,8 @@ mod tests { fn mock_reorg_handler() -> ReorgHandler { let chain_dbs_map: HashMap> = HashMap::new(); - ReorgHandler::new(mock_rpc_client(), chain_dbs_map) + let resetters: HashMap>> = HashMap::new(); + ReorgHandler::new(mock_rpc_client(), chain_dbs_map, resetters) } #[tokio::test] diff --git a/crates/supervisor/core/src/reorg/handler.rs b/crates/supervisor/core/src/reorg/handler.rs index 37d8ad412f..7400809ac5 100644 --- a/crates/supervisor/core/src/reorg/handler.rs +++ b/crates/supervisor/core/src/reorg/handler.rs @@ -1,4 +1,8 @@ -use crate::{SupervisorError, reorg::task::ReorgTask}; +use crate::{ + SupervisorError, + reorg::task::ReorgTask, + syncnode::{Client, resetter::Resetter}, +}; use alloy_primitives::ChainId; use alloy_rpc_client::RpcClient; use derive_more::Constructor; @@ -15,6 +19,8 @@ pub struct ReorgHandler { rpc_client: RpcClient, /// Per chain dbs. chain_dbs: HashMap>, + /// Per chain resetters. + resetters: HashMap>>, } impl ReorgHandler @@ -32,8 +38,9 @@ where let mut handles = Vec::with_capacity(self.chain_dbs.len()); for (chain_id, chain_db) in &self.chain_dbs { + let resetter = Arc::clone(&self.resetters[chain_id]); let reorg_task = - ReorgTask::new(*chain_id, Arc::clone(chain_db), self.rpc_client.clone()); + ReorgTask::new(*chain_id, Arc::clone(chain_db), self.rpc_client.clone(), resetter); let handle = tokio::spawn(async move { reorg_task.process_chain_reorg().await }); handles.push(handle); } diff --git a/crates/supervisor/core/src/reorg/task.rs b/crates/supervisor/core/src/reorg/task.rs index cece69d00f..3d91866642 100644 --- a/crates/supervisor/core/src/reorg/task.rs +++ b/crates/supervisor/core/src/reorg/task.rs @@ -1,4 +1,7 @@ -use crate::SupervisorError; +use crate::{ + SupervisorError, + syncnode::{ManagedNodeClient, resetter::Resetter}, +}; use alloy_eips::BlockNumHash; use alloy_primitives::{B256, ChainId}; use alloy_rpc_client::RpcClient; @@ -10,15 +13,17 @@ use tracing::{debug, info, trace, warn}; /// Handles reorg for a single chain #[derive(Debug, Constructor)] -pub(crate) struct ReorgTask { +pub(crate) struct ReorgTask { chain_id: ChainId, db: Arc, rpc_client: RpcClient, + resetter: Arc>, } -impl ReorgTask +impl ReorgTask where DB: DbReader + StorageRewinder + Send + Sync + 'static, + C: ManagedNodeClient + Send + Sync + 'static, { /// Processes reorg for a single chain pub(crate) async fn process_chain_reorg(&self) -> Result<(), SupervisorError> { @@ -44,6 +49,23 @@ where ); })?; + debug!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + "Calling resetter to reset the node after reorg" + ); + + // Reset the node after rewinding the DB. + self.resetter.reset().await.map_err(|err| { + warn!( + target: "supervisor::reorg_handler", + chain_id = %self.chain_id, + %err, + "Failed to reset node after reorg" + ); + SupervisorError::from(err) + })?; + Ok(()) } @@ -129,14 +151,17 @@ where #[cfg(test)] mod tests { use super::*; + use crate::syncnode::{ClientError, ManagedNodeClient}; use alloy_rpc_types_eth::Header; use alloy_transport::mock::*; + use async_trait::async_trait; + use jsonrpsee::core::client::Subscription; use kona_interop::{DerivedRefPair, SafetyLevel}; use kona_protocol::BlockInfo; use kona_supervisor_storage::{ DerivationStorageReader, HeadRefStorageReader, LogStorageReader, StorageError, }; - use kona_supervisor_types::{Log, SuperHead}; + use kona_supervisor_types::{BlockSeal, Log, OutputV0, Receipts, SubscriptionEvent, SuperHead}; use mockall::mock; mock!( @@ -172,6 +197,30 @@ mod tests { pub chain_db {} ); + mock! { + #[derive(Debug)] + pub Client {} + + #[async_trait] + impl ManagedNodeClient for Client { + async fn chain_id(&self) -> Result; + async fn subscribe_events(&self) -> Result, ClientError>; + async fn fetch_receipts(&self, block_hash: B256) -> Result; + async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result; + async fn pending_output_v0_at_timestamp(&self, timestamp: u64) -> Result; + async fn l2_block_ref_by_timestamp(&self, timestamp: u64) -> Result; + async fn block_ref_by_number(&self, block_number: u64) -> Result; + async fn reset_pre_interop(&self) -> Result<(), ClientError>; + async fn reset(&self, unsafe_id: BlockNumHash, cross_unsafe_id: BlockNumHash, local_safe_id: BlockNumHash, cross_safe_id: BlockNumHash, finalised_id: BlockNumHash) -> Result<(), ClientError>; + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ClientError>; + async fn provide_l1(&self, block_info: BlockInfo) -> Result<(), ClientError>; + async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn update_cross_unsafe(&self, cross_unsafe_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn update_cross_safe(&self, source_block_id: BlockNumHash, derived_block_id: BlockNumHash) -> Result<(), ClientError>; + async fn reset_ws_client(&self); + } + } + #[tokio::test] async fn test_find_rewind_target_without_reorg() { let mut mock_db = MockDb::new(); @@ -208,7 +257,9 @@ mod tests { // Mock RPC response asserter.push_success(&latest_source); - let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + let resetter = + Arc::new(Resetter::new(Arc::new(MockClient::new()), Arc::new(MockDb::new()))); + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client, resetter); let rewind_target = reorg_task.find_rewind_target().await; // Should succeed since the latest source block is still canonical @@ -342,7 +393,9 @@ mod tests { // Finally returning the correct block asserter.push_success(&finalized_source); - let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client); + let resetter = + Arc::new(Resetter::new(Arc::new(MockClient::new()), Arc::new(MockDb::new()))); + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client, resetter); let rewind_target = reorg_task.find_rewind_target().await; // Should succeed since the latest source block is still canonical @@ -389,7 +442,9 @@ mod tests { asserter.push_success(&canonical_block); asserter.push_success(&non_canonical_block); - let reorg_task = ReorgTask::new(1, Arc::new(MockDb::new()), rpc_client); + let resetter = + Arc::new(Resetter::new(Arc::new(MockClient::new()), Arc::new(MockDb::new()))); + let reorg_task = ReorgTask::new(1, Arc::new(MockDb::new()), rpc_client, resetter); let result = reorg_task.is_block_canonical(100, canonical_hash).await; assert!(result.is_ok()); diff --git a/crates/supervisor/core/src/supervisor.rs b/crates/supervisor/core/src/supervisor.rs index 9864c2fd1b..a5baa38de7 100644 --- a/crates/supervisor/core/src/supervisor.rs +++ b/crates/supervisor/core/src/supervisor.rs @@ -296,12 +296,18 @@ impl Supervisor { .map(|chain_id| (*chain_id, self.database_factory.get_db(*chain_id).unwrap())) .collect(); + let resetters = self + .managed_nodes + .iter() + .map(|(chain_id, managed_node)| (*chain_id, managed_node.resetter())) + .collect(); + let l1_watcher = L1Watcher::new( l1_rpc.clone(), self.database_factory.clone(), senders, self.cancel_token.clone(), - ReorgHandler::new(l1_rpc, chain_dbs_map), + ReorgHandler::new(l1_rpc, chain_dbs_map, resetters), ); tokio::spawn(async move { diff --git a/crates/supervisor/core/src/syncnode/node.rs b/crates/supervisor/core/src/syncnode/node.rs index e465abd505..858f96210c 100644 --- a/crates/supervisor/core/src/syncnode/node.rs +++ b/crates/supervisor/core/src/syncnode/node.rs @@ -63,6 +63,11 @@ where let chain_id = self.client.chain_id().await?; Ok(chain_id) } + + /// Returns the [`Resetter`] for the [`ManagedNode`]. + pub(crate) fn resetter(&self) -> Arc> { + self.resetter.clone() + } } #[async_trait] diff --git a/crates/supervisor/core/src/syncnode/resetter.rs b/crates/supervisor/core/src/syncnode/resetter.rs index 43444559e6..1bd0eac64f 100644 --- a/crates/supervisor/core/src/syncnode/resetter.rs +++ b/crates/supervisor/core/src/syncnode/resetter.rs @@ -9,7 +9,7 @@ use tokio::sync::Mutex; use tracing::{debug, error, info}; #[derive(Debug)] -pub(super) struct Resetter { +pub(crate) struct Resetter { client: Arc, db_provider: Arc, reset_guard: Mutex<()>, @@ -21,7 +21,7 @@ where C: ManagedNodeClient + Send + Sync + 'static, { /// Creates a new [`Resetter`] with the specified client. - pub(super) fn new(client: Arc, db_provider: Arc) -> Self { + pub(crate) fn new(client: Arc, db_provider: Arc) -> Self { Self { client, db_provider, reset_guard: Mutex::new(()) } } From 22aaaa858a7f4d229e496e58d296eed6be4a40b6 Mon Sep 17 00:00:00 2001 From: itschaindev Date: Fri, 1 Aug 2025 14:00:10 +0200 Subject: [PATCH 2/3] swap resetter with managed node controller --- .../supervisor/core/src/l1_watcher/watcher.rs | 9 +-- crates/supervisor/core/src/reorg/handler.rs | 24 ++++--- crates/supervisor/core/src/reorg/task.rs | 64 +++++++------------ crates/supervisor/core/src/supervisor.rs | 12 ++-- crates/supervisor/core/src/syncnode/node.rs | 5 -- 5 files changed, 48 insertions(+), 66 deletions(-) diff --git a/crates/supervisor/core/src/l1_watcher/watcher.rs b/crates/supervisor/core/src/l1_watcher/watcher.rs index 6e25d2120c..6392c9160a 100644 --- a/crates/supervisor/core/src/l1_watcher/watcher.rs +++ b/crates/supervisor/core/src/l1_watcher/watcher.rs @@ -212,10 +212,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{ - SupervisorError, - syncnode::{Client, resetter::Resetter}, - }; + use crate::{SupervisorError, syncnode::ManagedNodeController}; use alloy_primitives::B256; use alloy_transport::mock::*; use kona_supervisor_storage::{ChainDb, FinalizedL1Storage, StorageError}; @@ -245,8 +242,8 @@ mod tests { fn mock_reorg_handler() -> ReorgHandler { let chain_dbs_map: HashMap> = HashMap::new(); - let resetters: HashMap>> = HashMap::new(); - ReorgHandler::new(mock_rpc_client(), chain_dbs_map, resetters) + let managed_nodes: HashMap> = HashMap::new(); + ReorgHandler::new(mock_rpc_client(), chain_dbs_map, managed_nodes) } #[tokio::test] diff --git a/crates/supervisor/core/src/reorg/handler.rs b/crates/supervisor/core/src/reorg/handler.rs index 7400809ac5..574b6ab977 100644 --- a/crates/supervisor/core/src/reorg/handler.rs +++ b/crates/supervisor/core/src/reorg/handler.rs @@ -1,8 +1,4 @@ -use crate::{ - SupervisorError, - reorg::task::ReorgTask, - syncnode::{Client, resetter::Resetter}, -}; +use crate::{SupervisorError, reorg::task::ReorgTask, syncnode::ManagedNodeController}; use alloy_primitives::ChainId; use alloy_rpc_client::RpcClient; use derive_more::Constructor; @@ -19,8 +15,8 @@ pub struct ReorgHandler { rpc_client: RpcClient, /// Per chain dbs. chain_dbs: HashMap>, - /// Per chain resetters. - resetters: HashMap>>, + /// Per chain managed nodes + managed_nodes: HashMap>, } impl ReorgHandler @@ -38,9 +34,17 @@ where let mut handles = Vec::with_capacity(self.chain_dbs.len()); for (chain_id, chain_db) in &self.chain_dbs { - let resetter = Arc::clone(&self.resetters[chain_id]); - let reorg_task = - ReorgTask::new(*chain_id, Arc::clone(chain_db), self.rpc_client.clone(), resetter); + let managed_node = self.managed_nodes.get(chain_id).ok_or( + SupervisorError::Initialise("no managed node found for chain".to_string()), + )?; + + let reorg_task = ReorgTask::new( + *chain_id, + Arc::clone(chain_db), + self.rpc_client.clone(), + Arc::clone(managed_node), + ); + let handle = tokio::spawn(async move { reorg_task.process_chain_reorg().await }); handles.push(handle); } diff --git a/crates/supervisor/core/src/reorg/task.rs b/crates/supervisor/core/src/reorg/task.rs index 3d91866642..5209cd6e9b 100644 --- a/crates/supervisor/core/src/reorg/task.rs +++ b/crates/supervisor/core/src/reorg/task.rs @@ -1,7 +1,4 @@ -use crate::{ - SupervisorError, - syncnode::{ManagedNodeClient, resetter::Resetter}, -}; +use crate::{SupervisorError, syncnode::ManagedNodeController}; use alloy_eips::BlockNumHash; use alloy_primitives::{B256, ChainId}; use alloy_rpc_client::RpcClient; @@ -13,17 +10,16 @@ use tracing::{debug, info, trace, warn}; /// Handles reorg for a single chain #[derive(Debug, Constructor)] -pub(crate) struct ReorgTask { +pub(crate) struct ReorgTask { chain_id: ChainId, db: Arc, rpc_client: RpcClient, - resetter: Arc>, + managed_node: Arc, } -impl ReorgTask +impl ReorgTask where DB: DbReader + StorageRewinder + Send + Sync + 'static, - C: ManagedNodeClient + Send + Sync + 'static, { /// Processes reorg for a single chain pub(crate) async fn process_chain_reorg(&self) -> Result<(), SupervisorError> { @@ -49,14 +45,14 @@ where ); })?; - debug!( + trace!( target: "supervisor::reorg_handler", chain_id = %self.chain_id, "Calling resetter to reset the node after reorg" ); // Reset the node after rewinding the DB. - self.resetter.reset().await.map_err(|err| { + self.managed_node.reset().await.map_err(|err| { warn!( target: "supervisor::reorg_handler", chain_id = %self.chain_id, @@ -151,17 +147,16 @@ where #[cfg(test)] mod tests { use super::*; - use crate::syncnode::{ClientError, ManagedNodeClient}; + use crate::syncnode::{ManagedNodeController, ManagedNodeError}; use alloy_rpc_types_eth::Header; use alloy_transport::mock::*; use async_trait::async_trait; - use jsonrpsee::core::client::Subscription; use kona_interop::{DerivedRefPair, SafetyLevel}; use kona_protocol::BlockInfo; use kona_supervisor_storage::{ DerivationStorageReader, HeadRefStorageReader, LogStorageReader, StorageError, }; - use kona_supervisor_types::{BlockSeal, Log, OutputV0, Receipts, SubscriptionEvent, SuperHead}; + use kona_supervisor_types::{BlockSeal, Log, SuperHead}; use mockall::mock; mock!( @@ -197,29 +192,19 @@ mod tests { pub chain_db {} ); - mock! { + mock! ( #[derive(Debug)] - pub Client {} + pub ManagedNode {} #[async_trait] - impl ManagedNodeClient for Client { - async fn chain_id(&self) -> Result; - async fn subscribe_events(&self) -> Result, ClientError>; - async fn fetch_receipts(&self, block_hash: B256) -> Result; - async fn output_v0_at_timestamp(&self, timestamp: u64) -> Result; - async fn pending_output_v0_at_timestamp(&self, timestamp: u64) -> Result; - async fn l2_block_ref_by_timestamp(&self, timestamp: u64) -> Result; - async fn block_ref_by_number(&self, block_number: u64) -> Result; - async fn reset_pre_interop(&self) -> Result<(), ClientError>; - async fn reset(&self, unsafe_id: BlockNumHash, cross_unsafe_id: BlockNumHash, local_safe_id: BlockNumHash, cross_safe_id: BlockNumHash, finalised_id: BlockNumHash) -> Result<(), ClientError>; - async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ClientError>; - async fn provide_l1(&self, block_info: BlockInfo) -> Result<(), ClientError>; - async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ClientError>; - async fn update_cross_unsafe(&self, cross_unsafe_block_id: BlockNumHash) -> Result<(), ClientError>; - async fn update_cross_safe(&self, source_block_id: BlockNumHash, derived_block_id: BlockNumHash) -> Result<(), ClientError>; - async fn reset_ws_client(&self); + impl ManagedNodeController for ManagedNode { + async fn reset(&self) -> Result<(), ManagedNodeError>; + async fn update_finalized(&self, finalized_block_id: BlockNumHash) -> Result<(), ManagedNodeError>; + async fn update_cross_unsafe(&self, cross_unsafe_block_id: BlockNumHash) -> Result<(), ManagedNodeError>; + async fn update_cross_safe(&self, source_block_id: BlockNumHash, derived_block_id: BlockNumHash) -> Result<(), ManagedNodeError>; + async fn invalidate_block(&self, seal: BlockSeal) -> Result<(), ManagedNodeError>; } - } + ); #[tokio::test] async fn test_find_rewind_target_without_reorg() { @@ -257,9 +242,8 @@ mod tests { // Mock RPC response asserter.push_success(&latest_source); - let resetter = - Arc::new(Resetter::new(Arc::new(MockClient::new()), Arc::new(MockDb::new()))); - let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client, resetter); + let managed_node = Arc::new(MockManagedNode::new()); + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client, managed_node); let rewind_target = reorg_task.find_rewind_target().await; // Should succeed since the latest source block is still canonical @@ -393,9 +377,8 @@ mod tests { // Finally returning the correct block asserter.push_success(&finalized_source); - let resetter = - Arc::new(Resetter::new(Arc::new(MockClient::new()), Arc::new(MockDb::new()))); - let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client, resetter); + let managed_node = Arc::new(MockManagedNode::new()); + let reorg_task = ReorgTask::new(1, Arc::new(mock_db), rpc_client, managed_node); let rewind_target = reorg_task.find_rewind_target().await; // Should succeed since the latest source block is still canonical @@ -442,9 +425,8 @@ mod tests { asserter.push_success(&canonical_block); asserter.push_success(&non_canonical_block); - let resetter = - Arc::new(Resetter::new(Arc::new(MockClient::new()), Arc::new(MockDb::new()))); - let reorg_task = ReorgTask::new(1, Arc::new(MockDb::new()), rpc_client, resetter); + let managed_node = Arc::new(MockManagedNode::new()); + let reorg_task = ReorgTask::new(1, Arc::new(MockDb::new()), rpc_client, managed_node); let result = reorg_task.is_block_canonical(100, canonical_hash).await; assert!(result.is_ok()); diff --git a/crates/supervisor/core/src/supervisor.rs b/crates/supervisor/core/src/supervisor.rs index a5baa38de7..47a3b13c99 100644 --- a/crates/supervisor/core/src/supervisor.rs +++ b/crates/supervisor/core/src/supervisor.rs @@ -30,7 +30,9 @@ use crate::{ l1_watcher::L1Watcher, reorg::ReorgHandler, safety_checker::{CrossSafePromoter, CrossUnsafePromoter}, - syncnode::{Client, ManagedNode, ManagedNodeClient, ManagedNodeDataProvider}, + syncnode::{ + Client, ManagedNode, ManagedNodeClient, ManagedNodeController, ManagedNodeDataProvider, + }, }; /// Defines the service for the Supervisor core logic. @@ -296,10 +298,12 @@ impl Supervisor { .map(|chain_id| (*chain_id, self.database_factory.get_db(*chain_id).unwrap())) .collect(); - let resetters = self + let managed_nodes = self .managed_nodes .iter() - .map(|(chain_id, managed_node)| (*chain_id, managed_node.resetter())) + .map(|(chain_id, managed_node)| { + (*chain_id, managed_node.clone() as Arc) + }) .collect(); let l1_watcher = L1Watcher::new( @@ -307,7 +311,7 @@ impl Supervisor { self.database_factory.clone(), senders, self.cancel_token.clone(), - ReorgHandler::new(l1_rpc, chain_dbs_map, resetters), + ReorgHandler::new(l1_rpc, chain_dbs_map, managed_nodes), ); tokio::spawn(async move { diff --git a/crates/supervisor/core/src/syncnode/node.rs b/crates/supervisor/core/src/syncnode/node.rs index 858f96210c..e465abd505 100644 --- a/crates/supervisor/core/src/syncnode/node.rs +++ b/crates/supervisor/core/src/syncnode/node.rs @@ -63,11 +63,6 @@ where let chain_id = self.client.chain_id().await?; Ok(chain_id) } - - /// Returns the [`Resetter`] for the [`ManagedNode`]. - pub(crate) fn resetter(&self) -> Arc> { - self.resetter.clone() - } } #[async_trait] From c7357150f2aaaa59fa9e2b2de187b7c48ed5dea5 Mon Sep 17 00:00:00 2001 From: itschaindev Date: Fri, 1 Aug 2025 14:02:03 +0200 Subject: [PATCH 3/3] revert resetter visibility --- crates/supervisor/core/src/syncnode/resetter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/supervisor/core/src/syncnode/resetter.rs b/crates/supervisor/core/src/syncnode/resetter.rs index 1bd0eac64f..43444559e6 100644 --- a/crates/supervisor/core/src/syncnode/resetter.rs +++ b/crates/supervisor/core/src/syncnode/resetter.rs @@ -9,7 +9,7 @@ use tokio::sync::Mutex; use tracing::{debug, error, info}; #[derive(Debug)] -pub(crate) struct Resetter { +pub(super) struct Resetter { client: Arc, db_provider: Arc, reset_guard: Mutex<()>, @@ -21,7 +21,7 @@ where C: ManagedNodeClient + Send + Sync + 'static, { /// Creates a new [`Resetter`] with the specified client. - pub(crate) fn new(client: Arc, db_provider: Arc) -> Self { + pub(super) fn new(client: Arc, db_provider: Arc) -> Self { Self { client, db_provider, reset_guard: Mutex::new(()) } }