Conversation
|
We already have a WIP deadline module (I added it at some point to validate other module, not sure which one): https://github.com/NethermindEth/pluto/blob/3f1c5a31089b2764b57dfac60bcefa3742186d09/crates/app/src/deadline/mod.rs. If that code is no longer relevant then it should be removed in this PR. |
c615454 to
d71c885
Compare
crates/core/src/deadline.rs
Outdated
| // todo: uses hardcode beacon client for testing, should be refactored to use a | ||
| // real beacon client (testutils/beaconmock) |
There was a problem hiding this comment.
We can mock the client by mocking the underlying reqwest::Client (we'll probably need to adjust a bit the pluto_eth1wrap::EthClient constructors)
crates/core/src/deadline.rs
Outdated
| /// Clock trait for abstracting time operations. | ||
| trait Clock: Send + Sync { | ||
| /// Returns the current time. | ||
| fn now(&self) -> DateTime<Utc>; | ||
|
|
||
| /// Creates a sleep future that completes after the given duration. | ||
| fn sleep(&self, duration: std::time::Duration) -> BoxFuture<'static, ()>; | ||
| } | ||
|
|
||
| /// Real clock implementation using tokio::time. | ||
| struct RealClock; | ||
|
|
||
| impl Clock for RealClock { | ||
| fn now(&self) -> DateTime<Utc> { | ||
| Utc::now() | ||
| } | ||
|
|
||
| fn sleep(&self, duration: std::time::Duration) -> BoxFuture<'static, ()> { | ||
| tokio::time::sleep(duration).boxed() | ||
| } | ||
| } |
There was a problem hiding this comment.
For mocking time using this approach resulted in some challenges for qbft. We should explore tokio's builtins for mocking time: https://docs.rs/tokio/latest/tokio/time/fn.advance.html
crates/core/src/deadline.rs
Outdated
| fn secs_to_chrono(secs: u64) -> Result<chrono::Duration> { | ||
| let secs_i64 = i64::try_from(secs).map_err(|_| DeadlineError::ArithmeticOverflow)?; | ||
| chrono::Duration::try_seconds(secs_i64).ok_or(DeadlineError::DurationConversion) | ||
| } |
There was a problem hiding this comment.
There is a single usage for this function, prefer to inline it.
| /// It may only be called once and the returned channel should be used | ||
| /// by a single task. Multiple instances are required for different | ||
| /// components and use cases. | ||
| pub trait Deadliner: Send + Sync { |
There was a problem hiding this comment.
This is the first time we introduce BoxFuture (and for me, the first time using it). Can we do with async-trait if possible?
crates/core/src/deadline.rs
Outdated
| #[async_trait] | ||
| pub trait BeaconClientForDeadline { | ||
| /// Fetches the genesis time from the beacon node. | ||
| async fn fetch_genesis_time(&self) -> Result<DateTime<Utc>>; | ||
|
|
||
| /// Fetches the slot duration and slots per epoch from the beacon node. | ||
| async fn fetch_slots_config(&self) -> Result<(std::time::Duration, u64)>; | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl BeaconClientForDeadline for EthBeaconNodeApiClient { | ||
| async fn fetch_genesis_time(&self) -> Result<DateTime<Utc>> { | ||
| self.fetch_genesis_time() | ||
| .await | ||
| .map_err(DeadlineError::FetchGenesisTime) | ||
| } | ||
|
|
||
| async fn fetch_slots_config(&self) -> Result<(std::time::Duration, u64)> { | ||
| self.fetch_slots_config() | ||
| .await | ||
| .map_err(DeadlineError::FetchGenesisTime) | ||
| } | ||
| } |
There was a problem hiding this comment.
No need to create a trait for this behaviour. We can expect a concrete EthBeaconNodeApiClient as client and do the mocking in the client itself if needed.
crates/core/src/deadline.rs
Outdated
| // Use far-future sentinel date (9999-01-01) matching Go implementation | ||
| // This timestamp is a known constant and will never fail | ||
| let mut curr_deadline = | ||
| DateTime::from_timestamp(253402300799, 0).unwrap_or(DateTime::<Utc>::MAX_UTC); |
There was a problem hiding this comment.
No need to use the Go constant here, the sentinel is to always have a Duty available that will never resolve:
| // Use far-future sentinel date (9999-01-01) matching Go implementation | |
| // This timestamp is a known constant and will never fail | |
| let mut curr_deadline = | |
| DateTime::from_timestamp(253402300799, 0).unwrap_or(DateTime::<Utc>::MAX_UTC); | |
| // Use far-future sentinel date (9999-01-01) matching Go implementation | |
| // This timestamp is a known constant and will never fail | |
| let mut curr_deadline = DateTime::<Utc>::MAX_UTC; |
crates/core/src/deadline.rs
Outdated
| let Ok(deadline_opt) = deadline_func(duty.clone()) else { | ||
| continue; | ||
| }; | ||
|
|
||
| // Ignore duties that never expire | ||
| let Some(duty_deadline) = deadline_opt else { | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
We can inline some stuff:
| let Ok(deadline_opt) = deadline_func(duty.clone()) else { | |
| continue; | |
| }; | |
| // Ignore duties that never expire | |
| let Some(duty_deadline) = deadline_opt else { | |
| continue; | |
| }; | |
| // Ignore duties that never expire | |
| let Ok(Some(duty_deadline)) = deadline_func(duty.clone()) else { | |
| continue; | |
| }; | |
crates/core/src/deadline.rs
Outdated
| struct DeadlinerImpl { | ||
| cancel_token: CancellationToken, | ||
| input_tx: tokio::sync::mpsc::UnboundedSender<DeadlineInput>, | ||
| output_rx: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<Duty>>>>, |
There was a problem hiding this comment.
You can drop the Arc wrapper; it's not actually needed.
| output_rx: Arc<Mutex<Option<tokio::sync::mpsc::Receiver<Duty>>>>, | |
| output_rx: Mutex<Option<tokio::sync::mpsc::Receiver<Duty>>>, |
| } | ||
|
|
||
| /// Fake clock implementation for testing. | ||
| #[cfg(test)] |
There was a problem hiding this comment.
nit: I would move all these definitions to the tests module to avoid the repeated #[cfg(test)] attribute.
|
Generally good, I don't see any potential logic/concurrency issues, but:
I'd suggest exploring some alternative options before moving forward; my main concern is all the timing bookkeeping that we do since it has already introduced issues in QBFT (we also mocked time there). |
|
Claude finished @varex83's task in 2m 44s —— View job PR Review: feat: add deadline
SummaryThe PR implements duty deadline tracking, porting Go's deadliner to Rust. The structural approach is sound, the existing reviewer suggestions (sentinel value, inline patterns, Arc removal) appear to have been applied. However, there are a few remaining issues ranging from a critical correctness bug to style/design concerns that should be addressed before merge. Findings[Critical] Busy-loop when duties set is empty or all duties are non-expiring
This creates a tight infinite busy-loop: the sleep fires at Evidence: // All three occurrences of this pattern are affected:
let initial_duration = curr_deadline
.signed_duration_since(now)
.to_std()
.unwrap_or(std::time::Duration::ZERO); // fires immediately for MAX_UTC sentinelRecommendation: Use a large but representable duration as sentinel (e.g., [High]
|
| Component | Go behavior | Rust | Notes |
|---|---|---|---|
| Never-expire duties (Exit, BuilderRegistration) | returns nil |
returns Ok(None) |
✅ |
| Proposer/Randao deadline | slotDuration/3 + margin |
same | ✅ |
| SyncMessage deadline | 2*slotDuration/3 + margin |
same | ✅ |
| Attester/Aggregator deadline | 2*slotDuration + margin |
same | ✅ |
| Sentinel deadline (empty duties) | uses far-future time, loop suspends | overflows → zero duration, busy-loops | ❌ |
add() idempotency |
returns true if already tracked | same via HashSet::insert |
✅ |
Tests
Tests were not run (no toolchain available in this context), but the test structure covers: never-expire duties, per-duty-type deadline calculations, and end-to-end deadliner behavior. The main concern is the real-time dependency in test_deadliner (see High finding above).
| Branch: bohdan/deadline
Summary
Implements duty deadline tracking and notification functionality in Rust (Pluto). This adds the
Deadlinertrait and associated logic for scheduling duty deadlines, managing timers, and sending expired duties to a channel.Closes #274