diff --git a/finalizer/src/actor.rs b/finalizer/src/actor.rs index abf887b4..b215589c 100644 --- a/finalizer/src/actor.rs +++ b/finalizer/src/actor.rs @@ -970,6 +970,14 @@ impl< let length = self.canonical_state.get_epocher().current_length(); let _ = sender.send(ConsensusStateResponse::EpochLength(length)); } + ConsensusStateRequest::GetEpochBounds(epoch) => { + let bounds = self + .canonical_state + .get_epocher() + .epoch_bounds(Epoch::new(epoch)) + .map(|(first, last)| (first.get(), last.get())); + let _ = sender.send(ConsensusStateResponse::EpochBounds(bounds)); + } ConsensusStateRequest::GetDeposit(index) => { let deposit = self.canonical_state.get_deposit(index).cloned(); let _ = sender.send(ConsensusStateResponse::Deposit(deposit)); diff --git a/finalizer/src/ingress.rs b/finalizer/src/ingress.rs index 0d5e19c1..4dbb5795 100644 --- a/finalizer/src/ingress.rs +++ b/finalizer/src/ingress.rs @@ -279,6 +279,24 @@ impl, B: ConsensusBlock> FinalizerMailbox { length } + pub async fn get_epoch_bounds(&self, epoch: u64) -> Option<(u64, u64)> { + let (response, rx) = oneshot::channel(); + let request = ConsensusStateRequest::GetEpochBounds(epoch); + let _ = self + .sender + .clone() + .send(FinalizerMessage::QueryState { request, response }) + .await; + + let res = rx + .await + .expect("consensus state query response sender dropped"); + let ConsensusStateResponse::EpochBounds(bounds) = res else { + unreachable!("request and response variants must match"); + }; + bounds + } + pub async fn get_deposit( &self, index: usize, diff --git a/rpc/src/api.rs b/rpc/src/api.rs index a3536461..c0ed24e4 100644 --- a/rpc/src/api.rs +++ b/rpc/src/api.rs @@ -1,7 +1,7 @@ use crate::types::{ CheckpointInfoRes, CheckpointRes, DepositResponse, DepositTransactionResponse, - FinalizedHeaderRes, PendingWithdrawalResponse, PublicKeysResponse, StateProofResponse, - StateRootResponse, ValidatorAccountResponse, + EpochBoundsResponse, FinalizedHeaderRes, PendingWithdrawalResponse, PublicKeysResponse, + StateProofResponse, StateRootResponse, ValidatorAccountResponse, }; use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; @@ -57,6 +57,9 @@ pub trait SummitApi { #[method(name = "getEpochLength")] async fn get_epoch_length(&self) -> RpcResult; + #[method(name = "getEpochBounds")] + async fn get_epoch_bounds(&self, epoch: u64) -> RpcResult; + #[method(name = "getDeposit")] async fn get_deposit(&self, index: usize) -> RpcResult; diff --git a/rpc/src/error.rs b/rpc/src/error.rs index 8af18f35..6e6b3eeb 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -7,6 +7,7 @@ pub enum RpcError { ValidatorNotFound, DepositNotFound, WithdrawalNotFound, + EpochNotFound, InvalidPublicKey(String), GenesisPathError(String), IoError(String), @@ -26,6 +27,7 @@ impl From for ErrorObjectOwned { RpcError::FinalizedHeaderNotFound => { ErrorObjectOwned::owned(2003, "Finalized header not found", None::<()>) } + RpcError::EpochNotFound => ErrorObjectOwned::owned(2004, "Epoch not found", None::<()>), RpcError::ValidatorNotFound => { ErrorObjectOwned::owned(3000, "Validator not found", None::<()>) } diff --git a/rpc/src/server.rs b/rpc/src/server.rs index 0c7079e5..7016da8f 100644 --- a/rpc/src/server.rs +++ b/rpc/src/server.rs @@ -2,8 +2,8 @@ use crate::api::{SummitApiServer, SummitProofApiServer}; use crate::error::RpcError; use crate::types::{ CheckpointInfoRes, CheckpointRes, DepositResponse, DepositTransactionResponse, - FinalizedHeaderRes, PendingWithdrawalResponse, PublicKeysResponse, StateProofResponse, - StateRootResponse, ValidatorAccountResponse, + EpochBoundsResponse, FinalizedHeaderRes, PendingWithdrawalResponse, PublicKeysResponse, + StateProofResponse, StateRootResponse, ValidatorAccountResponse, }; use alloy_primitives::{Address, U256, hex::FromHex as _}; use async_trait::async_trait; @@ -289,6 +289,17 @@ impl SummitApiServer for SummitRpcServer { Ok(epoch_length) } + async fn get_epoch_bounds(&self, epoch: u64) -> RpcResult { + let bounds = self.finalizer_mailbox.get_epoch_bounds(epoch).await; + match bounds { + Some((first_height, last_height)) => Ok(EpochBoundsResponse { + first_height, + last_height, + }), + None => Err(RpcError::EpochNotFound.into()), + } + } + async fn get_deposit(&self, index: usize) -> RpcResult { let deposit = self.finalizer_mailbox.get_deposit(index).await; match deposit { diff --git a/rpc/src/types.rs b/rpc/src/types.rs index 6d723f1c..8477bf94 100644 --- a/rpc/src/types.rs +++ b/rpc/src/types.rs @@ -17,6 +17,6 @@ pub struct DepositTransactionResponse { } pub use summit_types::rpc::{ - CheckpointInfoRes, CheckpointRes, DepositResponse, FinalizedHeaderRes, + CheckpointInfoRes, CheckpointRes, DepositResponse, EpochBoundsResponse, FinalizedHeaderRes, PendingWithdrawalResponse, StateProofResponse, StateRootResponse, ValidatorAccountResponse, }; diff --git a/rpc/tests/utils.rs b/rpc/tests/utils.rs index 216027bb..b2c78399 100644 --- a/rpc/tests/utils.rs +++ b/rpc/tests/utils.rs @@ -127,6 +127,12 @@ pub fn create_test_finalizer_mailbox( ConsensusStateRequest::GetEpochLength => { let _ = response.send(ConsensusStateResponse::EpochLength(10)); } + ConsensusStateRequest::GetEpochBounds(epoch) => { + let first = epoch * 10; + let last = first + 9; + let _ = + response.send(ConsensusStateResponse::EpochBounds(Some((first, last)))); + } }, _ => {} } diff --git a/types/src/consensus_state_query.rs b/types/src/consensus_state_query.rs index dd142aca..0aeb60da 100644 --- a/types/src/consensus_state_query.rs +++ b/types/src/consensus_state_query.rs @@ -21,6 +21,7 @@ pub enum ConsensusStateRequest { GetMinimumStake, GetMaximumStake, GetEpochLength, + GetEpochBounds(u64), GetDeposit(usize), GetDepositCount, GetWithdrawal([u8; 32]), @@ -39,6 +40,7 @@ pub enum ConsensusStateResponse { MinimumStake(u64), MaximumStake(u64), EpochLength(u64), + EpochBounds(Option<(u64, u64)>), Deposit(Option), DepositCount(usize), Withdrawal(Option), @@ -218,4 +220,18 @@ impl ConsensusStateQuery { }; length } + + pub async fn get_epoch_bounds(&self, epoch: u64) -> Option<(u64, u64)> { + let (tx, rx) = oneshot::channel(); + let req = ConsensusStateRequest::GetEpochBounds(epoch); + let _ = self.sender.clone().send((req, tx)).await; + + let res = rx + .await + .expect("consensus state query response sender dropped"); + let ConsensusStateResponse::EpochBounds(bounds) = res else { + unreachable!("request and response variants must match"); + }; + bounds + } } diff --git a/types/src/dynamic_epocher.rs b/types/src/dynamic_epocher.rs index 1f3c70cd..5e8ca3dc 100644 --- a/types/src/dynamic_epocher.rs +++ b/types/src/dynamic_epocher.rs @@ -65,6 +65,16 @@ impl DynamicEpocher { inner.current_epoch = epoch; } + /// Returns the bounds (first height, last height) of a given epoch, + /// or `None` if the epoch is beyond `current_epoch + 1`. + pub fn epoch_bounds(&self, epoch: Epoch) -> Option<(Height, Height)> { + let inner = self.inner.read().unwrap(); + if epoch.get() > inner.current_epoch.get() + 1 { + return None; + } + Self::bounds(&inner.segments, epoch) + } + /// Registers a new epoch length, taking effect at `current_epoch + 2`. /// /// Returns an error if the target epoch is before the latest registered diff --git a/types/src/rpc.rs b/types/src/rpc.rs index 102b0f6c..c7e2d441 100644 --- a/types/src/rpc.rs +++ b/types/src/rpc.rs @@ -71,3 +71,9 @@ pub struct StateProofResponse { pub el_block_number: u64, pub proofs: Vec, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EpochBoundsResponse { + pub first_height: u64, + pub last_height: u64, +}