diff --git a/.gitmodules b/.gitmodules index 925b6f41..1d92d62c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "timeboost-proto/protos"] path = timeboost-proto/protos url = https://github.com/EspressoSystems/timeboost-proto.git -[submodule "contracts/lib/openzeppelin-contracts-upgradeable"] - path = contracts/lib/openzeppelin-contracts-upgradeable - url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable [submodule "contracts"] path = contracts url = https://github.com/EspressoSystems/timeboost-contracts.git diff --git a/Cargo.lock b/Cargo.lock index 4bbcc384..6850b4b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1594,15 +1594,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "anvil" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e29d3ca6c44fd6bc23219f6441303e329ee44580d3cee160df9c5d205e8fc83" -dependencies = [ - "thiserror 2.0.16", -] - [[package]] name = "anyhow" version = "1.0.99" diff --git a/contracts b/contracts index d367ab38..88d521cc 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit d367ab389a735182f245abfc217121e3bb2a9a85 +Subproject commit 88d521cca14cd60c6a4fec621233ce5ab1fbbea8 diff --git a/diff.txt b/diff.txt new file mode 100644 index 00000000..b73fdaa4 --- /dev/null +++ b/diff.txt @@ -0,0 +1,126 @@ +diff --git a/contracts b/contracts +--- a/contracts ++++ b/contracts +@@ -1 +1 @@ +-Subproject commit 88d521cca14cd60c6a4fec621233ce5ab1fbbea8 ++Subproject commit 88d521cca14cd60c6a4fec621233ce5ab1fbbea8-dirty +diff --git a/timeboost-contract/src/committee.rs b/timeboost-contract/src/committee.rs +index be5ac003..4d1d9a46 100644 +--- a/timeboost-contract/src/committee.rs ++++ b/timeboost-contract/src/committee.rs +@@ -47,7 +47,7 @@ mod tests { + let provider_arc = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider_arc); + +- let manager = CommitteeManager::new(provider_arc, contract_addr); ++ let manager = CommitteeManager::new(provider_arc.clone(), contract_addr); + + let rng = &mut rand::rng(); + let members = (0..3) +@@ -83,7 +83,7 @@ mod tests { + let provider_arc = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider_arc); + +- let manager = CommitteeManager::new(provider_arc, contract_addr); ++ let manager = CommitteeManager::new(provider_arc.clone(), contract_addr); + + let rng = &mut rand::rng(); + let first_members = (0..2) +@@ -154,7 +154,7 @@ mod tests { + let provider_arc = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider_arc); + +- let manager = CommitteeManager::new(provider_arc, contract_addr); ++ let manager = CommitteeManager::new(provider_arc.clone(), contract_addr); + + let rng = &mut rand::rng(); + let members = (0..2) +@@ -185,7 +185,7 @@ mod tests { + let provider_arc = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider_arc); + +- let manager = CommitteeManager::new(provider_arc, contract_addr); ++ let manager = CommitteeManager::new(provider_arc.clone(), contract_addr); + + // create multiple committees + let rng = &mut rand::rng(); +@@ -230,8 +230,7 @@ mod tests { + async fn test_committee_manager_creation() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); +- let provider_arc = Arc::new(provider); +- let manager = CommitteeManager::new(provider_arc, contract_addr); ++ let manager = CommitteeManager::new(Arc::new(provider), contract_addr); + + // manager should be created successfully + assert_eq!(manager.contract_addr, contract_addr); +diff --git a/timeboost-contract/src/events.rs b/timeboost-contract/src/events.rs +index a8c19dc8..1bd7527a 100644 +--- a/timeboost-contract/src/events.rs ++++ b/timeboost-contract/src/events.rs +@@ -134,9 +134,9 @@ mod tests { + let _manager = provider_arc.default_signer_address(); + + // create event monitor & get initial block number +- let contract = KeyManager::new(contract_addr, &provider_arc); +- let monitor = KeyManagerEventMonitor::new(&provider_arc, contract_addr); + let initial_block = provider_arc.get_block_number().await.unwrap(); ++ let contract = KeyManager::new(contract_addr, &provider_arc); ++ let monitor = KeyManagerEventMonitor::new(provider_arc.clone(), contract_addr); + + // create a committee to trigger CommitteeCreated event + let rng = &mut rand::rng(); +@@ -176,12 +176,12 @@ mod tests { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider_arc = Arc::new(provider); ++ let initial_block = provider_arc.get_block_number().await.unwrap(); + + let contract = KeyManager::new(contract_addr, &provider_arc); + + // create event monitor and get initial block number +- let monitor = KeyManagerEventMonitor::new(&provider_arc, contract_addr); +- let initial_block = provider_arc.get_block_number().await.unwrap(); ++ let monitor = KeyManagerEventMonitor::new(provider_arc.clone(), contract_addr); + + // set threshold encryption key to trigger ThresholdEncryptionKeyUpdated event + let test_key = b"test_threshold_encryption_key_32_bytes!".to_vec(); +@@ -220,7 +220,7 @@ mod tests { + let manager = provider_arc.default_signer_address(); + + // create event monitor +- let monitor = KeyManagerEventMonitor::new(&provider_arc, contract_addr); ++ let monitor = KeyManagerEventMonitor::new(provider_arc.clone(), contract_addr); + + // get initial block number + let initial_block = provider_arc.get_block_number().await.unwrap(); +@@ -283,7 +283,7 @@ mod tests { + let contract = KeyManager::new(contract_addr, &provider_arc); + + // create event monitor +- let monitor = KeyManagerEventMonitor::new(&provider_arc, contract_addr); ++ let monitor = KeyManagerEventMonitor::new(provider_arc.clone(), contract_addr); + + // get initial block number + let initial_block = provider_arc.get_block_number().await.unwrap(); +diff --git a/timeboost/src/event_monitor.rs b/timeboost/src/event_monitor.rs +index 1bc378a8..6b1fa638 100644 +--- a/timeboost/src/event_monitor.rs ++++ b/timeboost/src/event_monitor.rs +@@ -6,6 +6,7 @@ use anyhow::Result; + use timeboost_contract::KeyManagerEventMonitor; + use tokio::time::sleep; + use tracing::{error, info}; ++use std::sync::Arc; + + use crate::conf::EventMonitoringConfig; + +@@ -47,7 +48,7 @@ impl EventMonitor

{ + + /// Monitor contract events and send them to the processing channel + async fn monitor_events(self) -> Result<()> { +- let event_monitor = KeyManagerEventMonitor::new(self.provider.clone(), self.contract_addr); ++ let event_monitor = KeyManagerEventMonitor::new(Arc::new(self.provider.clone()), self.contract_addr); + let mut last_processed_block = None; + let start_block_number = self.config.start_block_number; + loop { diff --git a/justfile b/justfile index 5de77dce..cd7787d9 100644 --- a/justfile +++ b/justfile @@ -12,9 +12,10 @@ build *ARGS: cargo build {{ARGS}} update-submodules: - git submodule update --remote --recursive - cd timeboost-proto && cargo build - cd ../contracts && forge build + git submodule update --init --remote --recursive + cargo build -p timeboost-proto + forge build + cargo build -p timeboost-contract build_release *ARGS: cargo build --release --workspace --all-targets {{ARGS}} diff --git a/timeboost-contract/src/committee.rs b/timeboost-contract/src/committee.rs new file mode 100644 index 00000000..5ba4fa1d --- /dev/null +++ b/timeboost-contract/src/committee.rs @@ -0,0 +1,238 @@ +//! Committee management and querying utilities +use crate::bindings::keymanager::KeyManager; +use alloy::{primitives::Address, providers::Provider}; +use anyhow::Result; +use std::sync::Arc; + +pub struct CommitteeManager

{ + provider: Arc

, + contract_addr: Address, +} + +impl CommitteeManager

{ + pub fn new(provider: Arc

, contract_addr: Address) -> Self { + Self { + provider, + contract_addr, + } + } + + pub async fn get_committees_for_startup( + &self, + current_id: u64, + previous_id: Option, + ) -> Result<(KeyManager::Committee, Option)> { + let contract = KeyManager::new(self.contract_addr, &self.provider); + + let current = contract.getCommitteeById(current_id).call().await?; + let previous = if let Some(prev_id) = previous_id { + Some(contract.getCommitteeById(prev_id).call().await?) + } else { + None + }; + + Ok((current, previous)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CommitteeMemberSol, KeyManager}; + use rand::prelude::*; + + #[tokio::test] + async fn test_get_committees_for_startup_current_only() { + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider); + + let manager = CommitteeManager::new(provider.clone(), contract_addr); + + let rng = &mut rand::rng(); + let members = (0..3) + .map(|_| CommitteeMemberSol::random()) + .collect::>(); + let timestamp = rng.random::(); + + // create the committee + contract + .setNextCommittee(timestamp, members.clone()) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + let (current, previous) = manager.get_committees_for_startup(0, None).await.unwrap(); + + // verify current committee + assert_eq!(current.id, 0); + assert_eq!(current.effectiveTimestamp, timestamp); + assert_eq!(current.members.len(), 3); + + // verify no previous committee + assert!(previous.is_none()); + } + + #[tokio::test] + async fn test_get_committees_for_startup_with_previous() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider); + + let manager = CommitteeManager::new(provider.clone(), contract_addr); + + let rng = &mut rand::rng(); + let first_members = (0..2) + .map(|_| CommitteeMemberSol::random()) + .collect::>(); + let first_timestamp = rng.random::(); + + contract + .setNextCommittee(first_timestamp, first_members.clone()) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // create second committee (will be current) + let second_members = (0..3) + .map(|_| CommitteeMemberSol::random()) + .collect::>(); + let second_timestamp = first_timestamp + 1000; // Ensure different timestamp + + contract + .setNextCommittee(second_timestamp, second_members.clone()) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // test getting both current and previous committees + let (current, previous) = manager + .get_committees_for_startup(1, Some(0)) + .await + .unwrap(); + + // verify current committee (id=1) + assert_eq!(current.id, 1); + assert_eq!(current.effectiveTimestamp, second_timestamp); + assert_eq!(current.members.len(), 3); + + // verify previous committee (id=0) + assert!(previous.is_some()); + let prev = previous.unwrap(); + assert_eq!(prev.id, 0); + assert_eq!(prev.effectiveTimestamp, first_timestamp); + assert_eq!(prev.members.len(), 2); + } + + #[tokio::test] + async fn test_get_committees_for_startup_nonexistent_committee() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + + let manager = CommitteeManager::new(Arc::new(provider), contract_addr); + + let result = manager.get_committees_for_startup(999, None).await; + + // should return an error + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_get_committees_for_startup_nonexistent_previous() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider); + + let manager = CommitteeManager::new(provider.clone(), contract_addr); + + let rng = &mut rand::rng(); + let members = (0..2) + .map(|_| CommitteeMemberSol::random()) + .collect::>(); + let timestamp = rng.random::(); + + contract + .setNextCommittee(timestamp, members) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // try to get current committee with non-existent previous + let result = manager.get_committees_for_startup(0, Some(999)).await; + + // should return an error because previous committee doesn't exist + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_get_committees_for_startup_multiple_committees() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider); + + let manager = CommitteeManager::new(provider.clone(), contract_addr); + + // create multiple committees + let rng = &mut rand::rng(); + let base_timestamp = rng.random::(); + + for i in 0..5 { + let members = (0..2) + .map(|_| CommitteeMemberSol::random()) + .collect::>(); + let timestamp = base_timestamp + (i as u64 * 1000); + + contract + .setNextCommittee(timestamp, members) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + } + + // test getting committee 3 with previous committee 2 + let (current, previous) = manager + .get_committees_for_startup(3, Some(2)) + .await + .unwrap(); + + // verify current committee (id=3) + assert_eq!(current.id, 3); + assert_eq!(current.effectiveTimestamp, base_timestamp + 3000); + assert_eq!(current.members.len(), 2); + + // verify previous committee (id=2) + assert!(previous.is_some()); + let prev = previous.unwrap(); + assert_eq!(prev.id, 2); + assert_eq!(prev.effectiveTimestamp, base_timestamp + 2000); + assert_eq!(prev.members.len(), 2); + } + + #[tokio::test] + async fn test_committee_manager_creation() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let manager = CommitteeManager::new(Arc::new(provider), contract_addr); + + // manager should be created successfully + assert_eq!(manager.contract_addr, contract_addr); + } +} diff --git a/timeboost-contract/src/events.rs b/timeboost-contract/src/events.rs new file mode 100644 index 00000000..d1efa2d8 --- /dev/null +++ b/timeboost-contract/src/events.rs @@ -0,0 +1,331 @@ +//! Event monitoring and filtering for KeyManager contract +use crate::bindings::keymanager::KeyManager; +use alloy::{ + primitives::Address, + providers::Provider, + rpc::types::{BlockNumberOrTag, Filter}, + sol_types::SolEvent, +}; +use anyhow::Result; +use std::sync::Arc; + +pub struct KeyManagerEventMonitor

{ + provider: Arc

, + contract_addr: Address, +} + +impl KeyManagerEventMonitor

{ + pub fn new(provider: Arc

, contract_addr: Address) -> Self { + Self { + provider, + contract_addr, + } + } + + pub async fn get_committee_created_events( + &self, + from_block: BlockNumberOrTag, + to_block: BlockNumberOrTag, + ) -> Result> { + let filter = Filter::new() + .address(self.contract_addr) + .from_block(from_block) + .to_block(to_block) + .event(KeyManager::CommitteeCreated::SIGNATURE); + + let logs = self.provider.get_logs(&filter).await?; + let events = logs + .into_iter() + .filter_map(|log| { + KeyManager::CommitteeCreated::decode_log(&log.into()) + .ok() + .map(|decoded_log| decoded_log.data) + }) + .collect(); + + Ok(events) + } + + pub async fn get_threshold_encryption_key_updated_events( + &self, + from_block: BlockNumberOrTag, + to_block: BlockNumberOrTag, + ) -> Result> { + let filter = Filter::new() + .address(self.contract_addr) + .from_block(from_block) + .to_block(to_block) + .event(KeyManager::ThresholdEncryptionKeyUpdated::SIGNATURE); + + let logs = self.provider.get_logs(&filter).await?; + let events = logs + .into_iter() + .filter_map(|log| { + KeyManager::ThresholdEncryptionKeyUpdated::decode_log(&log.into()) + .ok() + .map(|decoded_log| decoded_log.data) + }) + .collect(); + + Ok(events) + } + + pub async fn get_manager_changed_events( + &self, + from_block: BlockNumberOrTag, + to_block: BlockNumberOrTag, + ) -> Result> { + let filter = Filter::new() + .address(self.contract_addr) + .from_block(from_block) + .to_block(to_block) + .event(KeyManager::ManagerChanged::SIGNATURE); + + let logs = self.provider.get_logs(&filter).await?; + let events = logs + .into_iter() + .filter_map(|log| { + KeyManager::ManagerChanged::decode_log(&log.into()) + .ok() + .map(|decoded_log| decoded_log.data) + }) + .collect(); + + Ok(events) + } + + pub async fn get_committees_pruned_events( + &self, + from_block: BlockNumberOrTag, + to_block: BlockNumberOrTag, + ) -> Result> { + let filter = Filter::new() + .address(self.contract_addr) + .from_block(from_block) + .to_block(to_block) + .event(KeyManager::CommitteesPruned::SIGNATURE); + + let logs = self.provider.get_logs(&filter).await?; + let events = logs + .into_iter() + .filter_map(|log| { + KeyManager::CommitteesPruned::decode_log(&log.into()) + .ok() + .map(|decoded_log| decoded_log.data) + }) + .collect(); + + Ok(events) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CommitteeMemberSol, KeyManager}; + use alloy::{primitives::Address, providers::WalletProvider, rpc::types::BlockNumberOrTag}; + use rand::prelude::*; + + #[tokio::test] + async fn test_committee_created_event_monitoring() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider = Arc::new(provider); + let _manager = provider.default_signer_address(); + + // create event monitor & get initial block number + let initial_block = provider.get_block_number().await.unwrap(); + let contract = KeyManager::new(contract_addr, &provider); + let monitor = KeyManagerEventMonitor::new(provider.clone(), contract_addr); + + // create a committee to trigger CommitteeCreated event + let rng = &mut rand::rng(); + let members = (0..3) + .map(|_| CommitteeMemberSol::random()) + .collect::>(); + let timestamp = rng.random::(); + + // set next committee to trigger CommitteeCreated event + let _tx_receipt = contract + .setNextCommittee(timestamp, members.clone()) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // query for CommitteeCreated events + let final_block = provider.get_block_number().await.unwrap(); + let events = monitor + .get_committee_created_events( + BlockNumberOrTag::Number(initial_block), + BlockNumberOrTag::Number(final_block), + ) + .await + .unwrap(); + + // verify we got the event + assert_eq!(events.len(), 1); + let event = &events[0]; + assert_eq!(event.id, 0); // first committee should have id 0 + } + + #[tokio::test] + async fn test_threshold_encryption_key_updated_event_monitoring() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider = Arc::new(provider); + let initial_block = provider.get_block_number().await.unwrap(); + + let contract = KeyManager::new(contract_addr, &provider); + + // create event monitor and get initial block number + let monitor = KeyManagerEventMonitor::new(provider.clone(), contract_addr); + + // set threshold encryption key to trigger ThresholdEncryptionKeyUpdated event + let test_key = b"test_threshold_encryption_key_32_bytes!".to_vec(); + let _tx_receipt = contract + .setThresholdEncryptionKey(test_key.clone().into()) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // query for ThresholdEncryptionKeyUpdated events + let final_block = provider.get_block_number().await.unwrap(); + let events = monitor + .get_threshold_encryption_key_updated_events( + BlockNumberOrTag::Number(initial_block), + BlockNumberOrTag::Number(final_block), + ) + .await + .unwrap(); + + // verify we got the event + assert_eq!(events.len(), 1); + let event = &events[0]; + assert_eq!(event.thresholdEncryptionKey, test_key); + } + + #[tokio::test] + async fn test_manager_changed_event_monitoring() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider); + + let manager = provider.default_signer_address(); + + // create event monitor + let monitor = KeyManagerEventMonitor::new(provider.clone(), contract_addr); + + // get initial block number + let initial_block = provider.get_block_number().await.unwrap(); + + // create a new manager address + let new_manager = Address::random(); + + // change manager to trigger ManagerChanged event + let _tx_receipt = contract + .setManager(new_manager) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // get the block number after the transaction + let final_block = provider.get_block_number().await.unwrap(); + + // query for ManagerChanged events + let events = monitor + .get_manager_changed_events( + BlockNumberOrTag::Number(initial_block), + BlockNumberOrTag::Number(final_block), + ) + .await + .unwrap(); + + // verify we got the event + assert_eq!(events.len(), 1); + let event = &events[0]; + assert_eq!(event.oldManager, manager); + assert_eq!(event.newManager, new_manager); + } + + #[tokio::test] + async fn test_no_events_in_empty_range() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + + // create event monitor + let monitor = KeyManagerEventMonitor::new(Arc::new(provider), contract_addr); + + // query for events in a range where no events occurred + let events = monitor + .get_committee_created_events(BlockNumberOrTag::Number(0), BlockNumberOrTag::Number(0)) + .await + .unwrap(); + + // verify we got no events + assert_eq!(events.len(), 0); + } + + #[tokio::test] + async fn test_multiple_events_monitoring() { + // setup test chain and deploy contract + let (provider, contract_addr) = crate::init_test_chain().await.unwrap(); + let provider = Arc::new(provider); + let contract = KeyManager::new(contract_addr, &provider); + + // create event monitor + let monitor = KeyManagerEventMonitor::new(provider.clone(), contract_addr); + + // get initial block number + let initial_block = provider.get_block_number().await.unwrap(); + + // create multiple committees to trigger multiple CommitteeCreated events + let rng = &mut rand::rng(); + let base_timestamp = rng.random::(); + for i in 0..3 { + let members = (0..2) + .map(|_| CommitteeMemberSol::random()) + .collect::>(); + // ensure timestamps are far enough apart to avoid validation errors + let timestamp = base_timestamp + (i as u64 * 1000); + + contract + .setNextCommittee(timestamp, members) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + } + + // get the final block number + let final_block = provider.get_block_number().await.unwrap(); + + // query for all CommitteeCreated events + let events = monitor + .get_committee_created_events( + BlockNumberOrTag::Number(initial_block), + BlockNumberOrTag::Number(final_block), + ) + .await + .unwrap(); + + // verify we got all 3 events + assert_eq!(events.len(), 3); + + // verify the committee IDs are sequential + let mut committee_ids: Vec = events.iter().map(|e| e.id).collect(); + committee_ids.sort(); + assert_eq!(committee_ids, vec![0, 1, 2]); + } +} diff --git a/timeboost-contract/src/lib.rs b/timeboost-contract/src/lib.rs index ee80e083..ba17e8bc 100644 --- a/timeboost-contract/src/lib.rs +++ b/timeboost-contract/src/lib.rs @@ -11,10 +11,14 @@ use anyhow::Result; // Include the generated contract bindings // The build script auto-detects contracts and generates bindings in src/bindings/ mod bindings; +pub mod committee; pub mod deployer; +pub mod events; pub mod provider; mod sol_types; +pub use committee::*; +pub use events::*; pub use sol_types::*; use timeboost_types::TestProviderWithWallet; diff --git a/timeboost/src/binaries/timeboost.rs b/timeboost/src/binaries/timeboost.rs index d7fb322d..649c3559 100644 --- a/timeboost/src/binaries/timeboost.rs +++ b/timeboost/src/binaries/timeboost.rs @@ -13,7 +13,7 @@ use timeboost_types::{KeyStore, ThresholdKeyCell}; use tokio::select; use tokio::signal; use tokio::task::spawn; -use tracing::info; +use tracing::{error, info}; use clap::Parser; use timeboost::config::{CERTIFIER_PORT_OFFSET, DECRYPTER_PORT_OFFSET, NodeConfig}; @@ -224,7 +224,23 @@ async fn main() -> Result<()> { .chain_config(node_config.chain.clone()) .build(); - let timeboost = Timeboost::new(config).await?; + let timeboost = Timeboost::new(config.clone()).await?; + + let event_monitor_handle = config.event_monitoring().enabled().then(|| { + let provider = + ProviderBuilder::new().connect_http(node_config.chain.parent.rpc_url.clone()); + let event_monitor = timeboost::event_monitor::EventMonitor::new( + provider, + node_config.chain.parent.key_manager_contract, + config.event_monitoring().clone(), + ); + + spawn(async move { + if let Err(e) = event_monitor.start_monitoring().await { + error!("Event monitoring failed: {}", e); + } + }) + }); let mut grpc = { let addr = node_config.net.internal.address.to_string(); @@ -295,14 +311,31 @@ async fn main() -> Result<()> { } #[cfg(not(feature = "until"))] - select! { - _ = timeboost.go() => bail!("timeboost shutdown unexpectedly"), - _ = &mut grpc => bail!("grpc api shutdown unexpectedly"), - _ = &mut api => bail!("api service shutdown unexpectedly"), - _ = signal::ctrl_c() => { - warn!("received ctrl-c; shutting down"); - api.abort(); - grpc.abort(); + if let Some(mut event_handle) = event_monitor_handle { + select! { + _ = timeboost.go() => bail!("timeboost shutdown unexpectedly"), + _ = &mut grpc => bail!("grpc api shutdown unexpectedly"), + _ = &mut api => bail!("api service shutdown unexpectedly"), + _ = &mut event_handle => { + bail!("event monitor shutdown unexpectedly") + }, + _ = signal::ctrl_c() => { + warn!("received ctrl-c; shutting down"); + api.abort(); + grpc.abort(); + event_handle.abort(); + } + } + } else { + select! { + _ = timeboost.go() => bail!("timeboost shutdown unexpectedly"), + _ = &mut grpc => bail!("grpc api shutdown unexpectedly"), + _ = &mut api => bail!("api service shutdown unexpectedly"), + _ = signal::ctrl_c() => { + warn!("received ctrl-c; shutting down"); + api.abort(); + grpc.abort(); + } } } diff --git a/timeboost/src/conf.rs b/timeboost/src/conf.rs index 229cd564..800ef007 100644 --- a/timeboost/src/conf.rs +++ b/timeboost/src/conf.rs @@ -58,6 +58,10 @@ pub struct TimeboostConfig { /// Chain configuration pub(crate) chain_config: ChainConfig, + + /// Event monitoring configuration + #[builder(default)] + pub(crate) event_monitoring: EventMonitoringConfig, } impl TimeboostConfig { @@ -95,4 +99,39 @@ impl TimeboostConfig { .committee(self.sailfish_committee.committee().clone()) .build() } + + pub fn event_monitoring(&self) -> &EventMonitoringConfig { + &self.event_monitoring + } +} + +#[derive(Debug, Clone, Builder)] +pub struct EventMonitoringConfig { + /// Enable event monitoring for contract events + #[builder(default = true)] + pub(crate) enabled: bool, + + /// Polling interval for checking new events (in seconds) + #[builder(default = 5)] + pub(crate) poll_interval_seconds: u64, + + /// The start block number to monitor events from + #[builder(default = 0)] + pub(crate) start_block_number: u64, +} + +impl Default for EventMonitoringConfig { + fn default() -> Self { + Self { + enabled: true, + poll_interval_seconds: 5, + start_block_number: 0, + } + } +} + +impl EventMonitoringConfig { + pub fn enabled(&self) -> bool { + self.enabled + } } diff --git a/timeboost/src/event_monitor.rs b/timeboost/src/event_monitor.rs new file mode 100644 index 00000000..117f8a86 --- /dev/null +++ b/timeboost/src/event_monitor.rs @@ -0,0 +1,103 @@ +//! Event monitoring for contract events that trigger committee transitions +use std::time::Duration; + +use alloy::{primitives::Address, providers::Provider, rpc::types::BlockNumberOrTag}; +use anyhow::Result; +use std::sync::Arc; +use timeboost_contract::KeyManagerEventMonitor; +use tokio::time::sleep; +use tracing::{error, info}; + +use crate::conf::EventMonitoringConfig; + +/// Event monitor that watches for contract events and triggers committee transitions +pub struct EventMonitor

{ + provider: P, + contract_addr: Address, + config: EventMonitoringConfig, +} + +impl EventMonitor

{ + pub fn new(provider: P, contract_addr: Address, config: EventMonitoringConfig) -> Self { + Self { + provider, + contract_addr, + config, + } + } + + /// Start monitoring contract events in the background + pub async fn start_monitoring(self) -> Result<()> { + if !self.config.enabled { + info!("Event monitoring is disabled"); + return Ok(()); + } + + let monitor_task = tokio::spawn(async move { + if let Err(e) = self.monitor_events().await { + error!("Event monitoring failed: {}", e); + } + }); + + if let Err(e) = monitor_task.await { + error!("Event monitor task panicked: {}", e); + } + + Ok(()) + } + + /// Monitor contract events and send them to the processing channel + async fn monitor_events(self) -> Result<()> { + let event_monitor = + KeyManagerEventMonitor::new(Arc::new(self.provider.clone()), self.contract_addr); + let mut last_processed_block = None; + let start_block_number = self.config.start_block_number; + loop { + match self.get_latest_block_number().await { + Ok(latest_block) => { + let from_block = if let Some(last_block) = last_processed_block { + BlockNumberOrTag::Number(last_block + 1) + } else { + BlockNumberOrTag::Number(start_block_number) + }; + + // Get committee created events since last check + match event_monitor + .get_committee_created_events(from_block, BlockNumberOrTag::Latest) + .await + { + Ok(events) => { + for event in events { + info!( + committee_id = event.id, + "New committee created event detected" + ); + + // TODO: process event + } + + last_processed_block = Some(latest_block); + } + Err(e) => { + error!("Failed to get committee created events: {}", e); + } + } + } + Err(e) => { + error!("Failed to get latest block number: {}", e); + } + } + + // Wait before next poll + sleep(Duration::from_secs(self.config.poll_interval_seconds)).await; + } + } + + /// Get the latest block number from the provider + async fn get_latest_block_number(&self) -> Result { + self.provider + .get_block_number() + .await + .map_err(|e| anyhow::anyhow!("Failed to get latest block number: {}", e)) + } +} diff --git a/timeboost/src/lib.rs b/timeboost/src/lib.rs index 9f627fff..a6c08707 100644 --- a/timeboost/src/lib.rs +++ b/timeboost/src/lib.rs @@ -27,6 +27,7 @@ use crate::api::internal::GrpcServer; use crate::forwarder::nitro_forwarder::NitroForwarder; pub mod api; +pub mod event_monitor; pub mod forwarder; pub mod metrics;