Skip to content

Commit d112833

Browse files
authored
refactor: extract eth2wrap (#214)
* Use `fetch_genesis_time` * Add `fetch_slots_config` * Remove clippy overrides * Add `fetch_fork_config` * Move `eth2api` to actual crate * Consistent error case naming * Consistent erro case names * Reuse `fetch_slots_config` * Extract Beacon node setup * Rename test * Test `fetch_genesis_time` * Test `fetch_slots_config` - Fixes deserialization issues * Test `fetch_fork_config` - Fix serialization issues * Apply clippy suggestions * Do not lose error information * Reuse containers when possible * Inline variable * Prefer `LazyLock`
1 parent 7b9aed4 commit d112833

File tree

10 files changed

+470
-226
lines changed

10 files changed

+470
-226
lines changed

Cargo.lock

Lines changed: 116 additions & 103 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/app/src/deadline/mod.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
// TODO: Requires `eth2client`
2-
#![allow(unreachable_code)]
3-
#![allow(unused_variables)]
4-
#![allow(clippy::diverging_sub_expression)]
5-
61
use pluto_core::types::{Duty, DutyType};
7-
use std::time;
2+
use pluto_eth2api::{EthBeaconNodeApiClient, EthBeaconNodeApiClientError};
83

94
/// Defines the fraction of the slot duration to use as a margin.
105
/// This is to consider network delays and other factors that may affect the
@@ -16,16 +11,19 @@ pub type DeadlineFunc = Box<dyn Fn(Duty) -> Option<chrono::DateTime<chrono::Utc>
1611

1712
/// Error type for deadline-related operations.
1813
#[derive(Debug, thiserror::Error)]
19-
pub enum DeadlineError {}
14+
pub enum DeadlineError {
15+
/// Beacon client API error.
16+
#[error("Beacon client error: {0}")]
17+
BeaconClientError(#[from] EthBeaconNodeApiClientError),
18+
}
2019

2120
type Result<T> = std::result::Result<T, DeadlineError>;
2221

2322
/// Create a function that provides duty deadline or [`None`] if the duty never
2423
/// deadlines.
25-
pub fn new_duty_deadline_func() -> Result<DeadlineFunc> {
26-
let genesis_time: chrono::DateTime<chrono::Utc> = todo!("Fetch genesis time from eth2 client");
27-
28-
let slot_duration: time::Duration = todo!("Fetch slot duration from eth2 client");
24+
pub async fn new_duty_deadline_func(eth2_cl: &EthBeaconNodeApiClient) -> Result<DeadlineFunc> {
25+
let genesis_time = eth2_cl.fetch_genesis_time().await?;
26+
let (slot_duration, _) = eth2_cl.fetch_slots_config().await?;
2927

3028
#[allow(
3129
clippy::arithmetic_side_effects,

crates/app/src/eth2wrap/eth2api.rs

Lines changed: 0 additions & 39 deletions
This file was deleted.

crates/app/src/eth2wrap/mod.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,3 @@ pub mod version;
33

44
/// Cache of Validators retrieved from the Beacon node
55
pub mod valcache;
6-
7-
/// Extensions module to the Eth2Api crate
8-
///
9-
/// Includes additional data types and functions to reduce the boilerplate when
10-
/// interacting with `eth2api`.
11-
pub mod eth2api;

crates/app/src/eth2wrap/valcache.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use crate::eth2wrap::eth2api::{EthBeaconNodeApiClientError, ValidatorIndex, ValidatorStatusExt};
21
use pluto_core::types::PubKey;
32
use pluto_eth2api::{
4-
EthBeaconNodeApiClient, GetStateValidatorsResponseResponse,
3+
EthBeaconNodeApiClient, EthBeaconNodeApiClientError, GetStateValidatorsResponseResponse,
54
GetStateValidatorsResponseResponseDatum, PostStateValidatorsRequest,
6-
PostStateValidatorsRequestPath, PostStateValidatorsResponse, ValidatorRequestBody,
5+
PostStateValidatorsRequestPath, PostStateValidatorsResponse, ValidatorIndex,
6+
ValidatorRequestBody,
77
};
88
use std::{collections::HashMap, sync::Arc};
99
use tokio::sync::RwLock;
@@ -13,9 +13,9 @@ type Result<T> = std::result::Result<T, ValidatorCacheError>;
1313
/// Errors that can occur when interacting with the validator cache.
1414
#[derive(Debug, thiserror::Error)]
1515
pub enum ValidatorCacheError {
16-
/// Beacon client API error.
17-
#[error("Beacon client error: {0}")]
18-
BeaconClientError(#[from] EthBeaconNodeApiClientError),
16+
/// Beacon Node API client error.
17+
#[error("Beacon Node API client error: {0}")]
18+
EthBeaconNodeApiClientError(#[from] EthBeaconNodeApiClientError),
1919
}
2020

2121
/// Active validators as [`PubKey`] indexed by their validator index.

crates/eth2api/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ reqwest.workspace = true
1616
serde_json.workspace = true
1717
serde_with.workspace = true
1818
serde.workspace = true
19+
thiserror.workspace = true
20+
chrono.workspace = true
21+
hex.workspace = true
1922
validator.workspace = true
2023

2124
[dev-dependencies]

crates/eth2api/src/extensions.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use crate::{
2+
ConsensusVersion, EthBeaconNodeApiClient, GetGenesisRequest, GetGenesisResponse,
3+
GetSpecRequest, GetSpecResponse, ValidatorStatus,
4+
};
5+
use chrono::{DateTime, Utc};
6+
use std::{collections::HashMap, time};
7+
8+
/// Error that can occur when using the
9+
/// [`EthBeaconNodeApiClient`].
10+
#[derive(Debug, thiserror::Error)]
11+
pub enum EthBeaconNodeApiClientError {
12+
/// Underlying error from [`EthBeaconNodeApiClient`] when
13+
/// making a request.
14+
#[error("Request error: {0}")]
15+
RequestError(#[from] anyhow::Error),
16+
17+
/// Unexpected response, e.g, got an error when an Ok response was expected
18+
#[error("Unexpected response")]
19+
UnexpectedResponse,
20+
21+
/// Unexpected type in response
22+
#[error("Unexpected type in response")]
23+
UnexpectedType,
24+
25+
/// Zero slot duration or slots per epoch in network spec
26+
#[error("Zero slot duration or slots per epoch in network spec")]
27+
ZeroSlotDurationOrSlotsPerEpoch,
28+
}
29+
30+
/// Type alias for validator index.
31+
pub type ValidatorIndex = u64;
32+
33+
const FORKS: [ConsensusVersion; 6] = [
34+
ConsensusVersion::Altair,
35+
ConsensusVersion::Bellatrix,
36+
ConsensusVersion::Capella,
37+
ConsensusVersion::Deneb,
38+
ConsensusVersion::Electra,
39+
ConsensusVersion::Fulu,
40+
];
41+
42+
/// The schedule of given fork, containing the fork version and the epoch at
43+
/// which it activates.
44+
#[derive(Debug, Clone, PartialEq, Eq)]
45+
pub struct ForkSchedule {
46+
/// The fork version, as a 4-byte array.
47+
pub version: [u8; 4],
48+
/// The epoch at which the fork activates.
49+
pub epoch: u64,
50+
}
51+
52+
impl ValidatorStatus {
53+
/// Returns true if the validator is in one of the active states.
54+
pub fn is_active(&self) -> bool {
55+
matches!(
56+
self,
57+
ValidatorStatus::ActiveOngoing
58+
| ValidatorStatus::ActiveExiting
59+
| ValidatorStatus::ActiveSlashed
60+
)
61+
}
62+
}
63+
64+
impl EthBeaconNodeApiClient {
65+
/// Fetches the genesis time.
66+
pub async fn fetch_genesis_time(&self) -> Result<DateTime<Utc>, EthBeaconNodeApiClientError> {
67+
let genesis = match self.get_genesis(GetGenesisRequest {}).await? {
68+
GetGenesisResponse::Ok(genesis) => genesis,
69+
_ => return Err(EthBeaconNodeApiClientError::UnexpectedResponse),
70+
};
71+
72+
genesis
73+
.data
74+
.genesis_time
75+
.parse()
76+
.map_err(|_| EthBeaconNodeApiClientError::UnexpectedType)
77+
.and_then(|timestamp| {
78+
DateTime::from_timestamp(timestamp, 0)
79+
.ok_or(EthBeaconNodeApiClientError::UnexpectedType)
80+
})
81+
}
82+
83+
/// Fetches the slot duration and slots per epoch.
84+
pub async fn fetch_slots_config(
85+
&self,
86+
) -> Result<(time::Duration, u64), EthBeaconNodeApiClientError> {
87+
let spec = match self.get_spec(GetSpecRequest {}).await? {
88+
GetSpecResponse::Ok(spec) => spec,
89+
_ => return Err(EthBeaconNodeApiClientError::UnexpectedResponse),
90+
};
91+
92+
let slot_duration = spec
93+
.data
94+
.as_object()
95+
.and_then(|o| o.get("SECONDS_PER_SLOT"))
96+
.and_then(|v| v.as_str())
97+
.and_then(|s| s.parse::<u64>().ok())
98+
.ok_or(EthBeaconNodeApiClientError::UnexpectedType)
99+
.map(time::Duration::from_secs)?;
100+
101+
let slots_per_epoch = spec
102+
.data
103+
.as_object()
104+
.and_then(|o| o.get("SLOTS_PER_EPOCH"))
105+
.and_then(|v| v.as_str())
106+
.and_then(|s| s.parse::<u64>().ok())
107+
.ok_or(EthBeaconNodeApiClientError::UnexpectedType)?;
108+
109+
if slot_duration == time::Duration::ZERO || slots_per_epoch == 0 {
110+
return Err(EthBeaconNodeApiClientError::ZeroSlotDurationOrSlotsPerEpoch);
111+
}
112+
113+
Ok((slot_duration, slots_per_epoch))
114+
}
115+
116+
/// Fetches the fork schedule for all known forks.
117+
pub async fn fetch_fork_config(
118+
&self,
119+
) -> Result<HashMap<ConsensusVersion, ForkSchedule>, EthBeaconNodeApiClientError> {
120+
fn fetch_fork(
121+
fork: &ConsensusVersion,
122+
spec_data: &serde_json::Value,
123+
) -> Result<ForkSchedule, EthBeaconNodeApiClientError> {
124+
let version_field = format!("{}_FORK_VERSION", fork.to_string().to_uppercase());
125+
let version = spec_data
126+
.as_object()
127+
.and_then(|o| o.get(&version_field))
128+
.and_then(|f| f.as_str())
129+
.and_then(|hex| {
130+
let hex = hex.strip_prefix("0x").unwrap_or(hex);
131+
hex::decode(hex).ok()
132+
})
133+
.and_then(|bytes| bytes.try_into().ok())
134+
.ok_or(EthBeaconNodeApiClientError::UnexpectedType)?;
135+
136+
let epoch_field = format!("{}_FORK_EPOCH", fork.to_string().to_uppercase());
137+
let epoch = spec_data
138+
.as_object()
139+
.and_then(|o| o.get(&epoch_field))
140+
.and_then(|v| v.as_str())
141+
.and_then(|s| s.parse::<u64>().ok())
142+
.ok_or(EthBeaconNodeApiClientError::UnexpectedType)?;
143+
144+
Ok(ForkSchedule { version, epoch })
145+
}
146+
147+
let spec = match self.get_spec(GetSpecRequest {}).await? {
148+
GetSpecResponse::Ok(spec) => spec,
149+
_ => return Err(EthBeaconNodeApiClientError::UnexpectedResponse),
150+
};
151+
152+
let mut result = HashMap::new();
153+
for fork in FORKS.into_iter() {
154+
let fork_schedule = fetch_fork(&fork, &spec.data)?;
155+
result.insert(fork, fork_schedule);
156+
}
157+
158+
Ok(result)
159+
}
160+
}

0 commit comments

Comments
 (0)