From 6c66c352b39c68913538637fe5f70ce6bf7e4c2e Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:08:37 +0200 Subject: [PATCH 1/3] feat(core): add CachedValidatorsProvider trait for validatorapi Defines the `CachedValidatorsProvider` async trait in pluto-core's validatorapi module. Mirrors Charon's `app/eth2wrap.CachedValidatorsProvider` so the validator-API component can look up `validator_index -> DV root pubkey` (used by the selections / voluntary-exit / sync-committee submit handlers in follow-up PRs) without `pluto-core` depending on the application crate that owns the concrete per-epoch cache implementation. `Component` gains an `Arc` field threaded through both constructors as a required dependency, plus a private `fetch_active_validators` helper bounded by `UPSTREAM_REQUEST_TIMEOUT` that all submit handlers share. A `TestValidatorCache` test double lets unit tests supply the map directly without spinning up a real beacon node. The existing `crates/app/src/eth2wrap/valcache.rs` is intentionally untouched. It will impl this trait at the wiring layer (`pluto-app` already depends on `pluto-core`, so the dependency runs the right way without a new crate or any cross-crate moves). Test plan: - cargo +nightly fmt --all --check - cargo clippy -p pluto-core --all-targets --all-features -- -D warnings - cargo test -p pluto-core --all-features (430 passing; 2 new tests on `fetch_active_validators` covering the happy path and the provider-error -> 502 mapping) Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> --- crates/core/src/validatorapi/component.rs | 150 +++++++++++++++++- crates/core/src/validatorapi/mod.rs | 2 + .../core/src/validatorapi/validator_cache.rs | 35 ++++ 3 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 crates/core/src/validatorapi/validator_cache.rs diff --git a/crates/core/src/validatorapi/component.rs b/crates/core/src/validatorapi/component.rs index 969f92e7..29aa3af7 100644 --- a/crates/core/src/validatorapi/component.rs +++ b/crates/core/src/validatorapi/component.rs @@ -13,7 +13,7 @@ use pluto_eth2api::{ EthBeaconNodeApiClient, GetAttesterDutiesRequest, GetAttesterDutiesResponse, GetProposerDutiesRequest, GetProposerDutiesResponse, GetSyncCommitteeDutiesRequest, GetSyncCommitteeDutiesResponse, - spec::phase0::{BLSPubKey, Epoch, Root}, + spec::phase0::{BLSPubKey, Epoch, Root, ValidatorIndex}, }; use pluto_eth2util::signing::{self, DomainName, SigningError}; use tokio::time::error::Elapsed; @@ -32,6 +32,7 @@ use super::{ VersionedAttestation, VersionedProposal, VersionedSignedAggregateAndProof, VersionedSignedBlindedProposal, VersionedSignedProposal, }, + validator_cache::CachedValidatorsProvider, }; use crate::{ dutydb::{Error as DutyDbError, MemDB}, @@ -175,6 +176,14 @@ pub struct Component { /// Looks up the root pubkey for an `(slot, commIdx, valIdx)` triple. #[allow(dead_code, reason = "consumed by submit_attestations in later PRs")] pub_key_by_att_fn: Option, + /// Cluster's per-epoch active-validators lookup. Consumed by the + /// selections / voluntary-exit / sync-committee submit handlers to + /// translate validator-client-supplied `validator_index` values into + /// DV root public keys. Mirrors Go's `c.eth2Cl.ActiveValidators(ctx)`, + /// which is itself backed by `app/eth2wrap`'s per-epoch validator + /// cache. + #[allow(dead_code, reason = "consumed by submit_* handlers in later PRs")] + validator_cache: Arc, } impl Component { @@ -185,6 +194,7 @@ impl Component { share_idx: u64, pub_share_by_pubkey: HashMap, builder_enabled: bool, + validator_cache: Arc, ) -> Self { Self { eth2_cl, @@ -200,6 +210,7 @@ impl Component { await_agg_sig_db_fn: None, duty_def_fn: None, pub_key_by_att_fn: None, + validator_cache, } } @@ -212,6 +223,7 @@ impl Component { eth2_cl: Arc, dutydb: Arc, share_idx: u64, + validator_cache: Arc, ) -> Self { Self { eth2_cl, @@ -227,6 +239,7 @@ impl Component { await_agg_sig_db_fn: None, duty_def_fn: None, pub_key_by_att_fn: None, + validator_cache, } } @@ -352,6 +365,28 @@ impl Component { Ok(()) } + + /// Fetches the cluster's active validators through the per-epoch + /// [`CachedValidatorsProvider`], bounded by [`UPSTREAM_REQUEST_TIMEOUT`]. + /// Translates cache failures into `ApiError`s without leaking the + /// underlying error into the client-visible message. Mirrors Go's + /// `c.eth2Cl.ActiveValidators(ctx)`, which is itself implemented via + /// `app/eth2wrap`'s validator cache. + #[allow(dead_code, reason = "consumed by submit_* handlers in later PRs")] + async fn fetch_active_validators( + &self, + ) -> Result, ApiError> { + tokio::time::timeout( + UPSTREAM_REQUEST_TIMEOUT, + self.validator_cache.active_validators(), + ) + .await + .map_err(|_: Elapsed| upstream_timeout("active validators"))? + .map_err(|err| { + ApiError::new(StatusCode::BAD_GATEWAY, "active validators lookup failed") + .with_boxed_source(err) + }) + } } /// Errors returned by [`Component::verify_partial_sig`]. @@ -821,6 +856,36 @@ mod tests { validatorapi::types::AttestationDataOpts, }; + /// In-memory stand-in for the per-epoch validator cache. Tests supply + /// the `validator_index -> root pubkey` map up front instead of + /// running a real beacon-node mock through a `ValidatorCache`. + #[derive(Default)] + pub(super) struct TestValidatorCache(HashMap); + + impl TestValidatorCache { + pub(super) fn arc( + map: HashMap, + ) -> Arc { + Arc::new(Self(map)) + } + + pub(super) fn empty() -> Arc { + Self::arc(HashMap::new()) + } + } + + #[async_trait] + impl CachedValidatorsProvider for TestValidatorCache { + async fn active_validators( + &self, + ) -> Result< + HashMap, + super::super::validator_cache::CachedValidatorsError, + > { + Ok(self.0.clone()) + } + } + /// Schedules every duty with a deadline at `MAX_UTC`, so duties are /// `Scheduled` but never naturally expire. struct FarFutureCalculator; @@ -844,7 +909,8 @@ mod tests { let dutydb = Arc::new(MemDB::new(deadliner, evict_rx, &cancel)); let eth2_cl = Arc::new(EthBeaconNodeApiClient::with_base_url("http://127.0.0.1:0").unwrap()); - let component = Component::new_insecure(eth2_cl, Arc::clone(&dutydb), 1); + let component = + Component::new_insecure(eth2_cl, Arc::clone(&dutydb), 1, TestValidatorCache::empty()); (component, dutydb) } @@ -1086,7 +1152,8 @@ mod tests { let dutydb = Arc::new(MemDB::new(deadliner, trim_rx, &cancel)); let eth2_cl = Arc::new(EthBeaconNodeApiClient::with_base_url("http://127.0.0.1:0").unwrap()); - let component = Component::new_insecure(eth2_cl, Arc::clone(&dutydb), 1); + let component = + Component::new_insecure(eth2_cl, Arc::clone(&dutydb), 1, TestValidatorCache::empty()); // Start an await before any data is stored. let waiter = { @@ -1272,7 +1339,7 @@ mod tests { let dutydb = Arc::new(MemDB::new(deadliner, evict_rx, &cancel)); let eth2_cl = Arc::new(EthBeaconNodeApiClient::with_base_url("http://127.0.0.1:0").unwrap()); - Component::new(eth2_cl, dutydb, 1, map, false) + Component::new(eth2_cl, dutydb, 1, map, false, TestValidatorCache::empty()) } /// `Subscribe` invokes every registered subscriber, each receiving its @@ -1503,7 +1570,7 @@ mod tests { let (_evict_tx, evict_rx) = mpsc::channel(1); let dutydb = Arc::new(MemDB::new(deadliner, evict_rx, &cancel)); let eth2_cl = Arc::new(EthBeaconNodeApiClient::with_base_url(mock.uri()).unwrap()); - let component = Component::new(eth2_cl, dutydb, 1, map, false); + let component = Component::new(eth2_cl, dutydb, 1, map, false, TestValidatorCache::empty()); (component, mock) } @@ -1586,7 +1653,7 @@ mod tests { let dutydb = Arc::new(MemDB::new(deadliner, evict_rx, &cancel)); let eth2_cl = Arc::new(EthBeaconNodeApiClient::with_base_url("http://127.0.0.1:0").unwrap()); - let component = Component::new_insecure(eth2_cl, dutydb, 1); + let component = Component::new_insecure(eth2_cl, dutydb, 1, TestValidatorCache::empty()); component .verify_partial_sig( @@ -1599,4 +1666,75 @@ mod tests { .await .expect("insecure_test mode skips verification"); } + + // ==================================================================== + // CachedValidatorsProvider plumbing + // ==================================================================== + + /// `fetch_active_validators` returns whatever the registered + /// `CachedValidatorsProvider` yields, untouched. Mirrors Go's + /// `c.eth2Cl.ActiveValidators(ctx)` return shape. + #[tokio::test] + async fn fetch_active_validators_returns_cache_contents() { + let cancel = CancellationToken::new(); + let (deadliner, _deadliner_rx) = DeadlinerTask::start( + cancel.clone(), + "validatorapi-validator-cache-tests", + FarFutureCalculator, + ); + let (_evict_tx, evict_rx) = mpsc::channel(1); + let dutydb = Arc::new(MemDB::new(deadliner, evict_rx, &cancel)); + let eth2_cl = + Arc::new(EthBeaconNodeApiClient::with_base_url("http://127.0.0.1:0").unwrap()); + + let expected = HashMap::from([(1u64, dv_pubkey(0xA1)), (7u64, dv_pubkey(0xA7))]); + let component = Component::new_insecure( + eth2_cl, + dutydb, + 1, + TestValidatorCache::arc(expected.clone()), + ); + + let got = component + .fetch_active_validators() + .await + .expect("test cache always succeeds"); + assert_eq!(got, expected); + } + + /// A provider that surfaces a transport-style error is mapped to a 502 + /// without leaking the underlying error into the client-visible + /// message. + #[tokio::test] + async fn fetch_active_validators_maps_provider_error_to_502() { + struct FailingCache; + + #[async_trait] + impl CachedValidatorsProvider for FailingCache { + async fn active_validators( + &self, + ) -> Result< + HashMap, + super::super::validator_cache::CachedValidatorsError, + > { + Err("upstream unavailable".into()) + } + } + + let cancel = CancellationToken::new(); + let (deadliner, _deadliner_rx) = DeadlinerTask::start( + cancel.clone(), + "validatorapi-validator-cache-fail-tests", + FarFutureCalculator, + ); + let (_evict_tx, evict_rx) = mpsc::channel(1); + let dutydb = Arc::new(MemDB::new(deadliner, evict_rx, &cancel)); + let eth2_cl = + Arc::new(EthBeaconNodeApiClient::with_base_url("http://127.0.0.1:0").unwrap()); + let component = Component::new_insecure(eth2_cl, dutydb, 1, Arc::new(FailingCache)); + + let err = component.fetch_active_validators().await.unwrap_err(); + assert_eq!(err.status_code, StatusCode::BAD_GATEWAY); + assert_eq!(err.message, "active validators lookup failed"); + } } diff --git a/crates/core/src/validatorapi/mod.rs b/crates/core/src/validatorapi/mod.rs index 8442859c..28a1d680 100644 --- a/crates/core/src/validatorapi/mod.rs +++ b/crates/core/src/validatorapi/mod.rs @@ -9,6 +9,7 @@ pub mod handler; pub mod metrics; pub mod router; pub mod types; +pub mod validator_cache; #[cfg(test)] pub mod testutils; @@ -17,3 +18,4 @@ pub use component::Component; pub use error::ApiError; pub use handler::Handler; pub use router::new_router; +pub use validator_cache::{CachedValidatorsError, CachedValidatorsProvider}; diff --git a/crates/core/src/validatorapi/validator_cache.rs b/crates/core/src/validatorapi/validator_cache.rs new file mode 100644 index 00000000..34f22b5f --- /dev/null +++ b/crates/core/src/validatorapi/validator_cache.rs @@ -0,0 +1,35 @@ +//! Cluster-wide active-validators lookup consumed by submit handlers. +//! +//! Mirrors Charon's `app/eth2wrap.CachedValidatorsProvider` interface: +//! submit handlers that have to translate a validator-client-supplied +//! `validator_index` into the cluster's DV root public key consult this +//! trait. Defined here in `pluto-core` so the validator API does not need +//! to depend on the application crate that owns the concrete per-epoch +//! cache implementation. + +use std::collections::HashMap; + +use async_trait::async_trait; +use pluto_eth2api::spec::phase0::{BLSPubKey, ValidatorIndex}; + +/// Boxed error returned by [`CachedValidatorsProvider`] methods. Kept +/// opaque so the trait does not bind callers to any single backing +/// implementation's error type. +pub type CachedValidatorsError = Box; + +/// Provides the cluster's currently active validators, indexed by +/// validator index. Mirrors Go's `eth2Cl.ActiveValidators(ctx)`, which is +/// itself backed by `app/eth2wrap`'s per-epoch validator cache; the +/// validator-API [`Component`](super::Component) calls through this trait +/// so the cache is the single source of truth across duty handlers +/// without `pluto-core` depending on the cache crate. +/// +/// Implementations may populate the underlying cache on demand — callers +/// must not assume the call is non-blocking. +#[async_trait] +pub trait CachedValidatorsProvider: Send + Sync { + /// Returns the `validator_index -> DV root BLS public key` map. + async fn active_validators( + &self, + ) -> Result, CachedValidatorsError>; +} From fac7d5d445f9fb7c901f8b48daa70aeb534070ca Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <273991985+varex83agent@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:08:07 +0200 Subject: [PATCH 2/3] refactor(core): reuse eth2wrap valcache trait for validatorapi Move eth2wrap from pluto-app to pluto-core so validatorapi can consume the existing CachedValidatorsProvider trait and ValidatorCacheError directly. Drop the duplicate validatorapi::validator_cache module and make the trait async (with Send + Sync bounds) so cache lookups can populate on demand. Co-Authored-By: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> --- crates/app/src/eth2wrap/mod.rs | 3 - crates/core/src/eth2wrap/mod.rs | 10 +++ crates/{app => core}/src/eth2wrap/valcache.rs | 10 ++- crates/core/src/lib.rs | 3 + crates/core/src/validatorapi/component.rs | 75 ++++++++++--------- crates/core/src/validatorapi/mod.rs | 2 - .../core/src/validatorapi/validator_cache.rs | 35 --------- 7 files changed, 59 insertions(+), 79 deletions(-) create mode 100644 crates/core/src/eth2wrap/mod.rs rename crates/{app => core}/src/eth2wrap/valcache.rs (98%) delete mode 100644 crates/core/src/validatorapi/validator_cache.rs diff --git a/crates/app/src/eth2wrap/mod.rs b/crates/app/src/eth2wrap/mod.rs index 51970966..cd3ab232 100644 --- a/crates/app/src/eth2wrap/mod.rs +++ b/crates/app/src/eth2wrap/mod.rs @@ -1,5 +1,2 @@ /// Validate Beacon node versions pub mod version; - -/// Cache of Validators retrieved from the Beacon node -pub mod valcache; diff --git a/crates/core/src/eth2wrap/mod.rs b/crates/core/src/eth2wrap/mod.rs new file mode 100644 index 00000000..56e80b6b --- /dev/null +++ b/crates/core/src/eth2wrap/mod.rs @@ -0,0 +1,10 @@ +//! Wrappers around the upstream beacon-node client. +//! +//! Mirrors Charon's `app/eth2wrap` package: a layer that decorates the +//! raw beacon-node API with cluster-wide concerns (caching, error +//! mapping). Lives in `pluto-core` so downstream modules (e.g. the +//! validator API [`crate::validatorapi`]) can consume the wrappers +//! without depending on `pluto-app`. + +/// Cache of validators retrieved from the Beacon node. +pub mod valcache; diff --git a/crates/app/src/eth2wrap/valcache.rs b/crates/core/src/eth2wrap/valcache.rs similarity index 98% rename from crates/app/src/eth2wrap/valcache.rs rename to crates/core/src/eth2wrap/valcache.rs index 3ec4d6b8..a40855c0 100644 --- a/crates/app/src/eth2wrap/valcache.rs +++ b/crates/core/src/eth2wrap/valcache.rs @@ -1,4 +1,5 @@ -use pluto_core::types::PubKey; +use crate::types::PubKey; +use async_trait::async_trait; use pluto_eth2api::{ EthBeaconNodeApiClient, EthBeaconNodeApiClientError, GetStateValidatorsResponseResponse, GetStateValidatorsResponseResponseDatum, PostStateValidatorsRequest, @@ -56,12 +57,13 @@ impl ActiveValidators { /// A provider of cached validator information for the current epoch, /// including both active validators and complete validator data. -pub trait CachedValidatorsProvider { +#[async_trait] +pub trait CachedValidatorsProvider: Send + Sync { /// Get the cached active validators. - fn active_validators(&self) -> Result; + async fn active_validators(&self) -> Result; /// Get all the cached validators. - fn complete_validators(&self) -> Result; + async fn complete_validators(&self) -> Result; } /// A cache for active validators. diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 6cd84def..bfd1cf6a 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -36,6 +36,9 @@ pub mod validatorapi; /// SigAgg — threshold BLS signature aggregation. pub mod sigagg; +/// `eth2wrap` — wrappers around the upstream beacon-node API client. +pub mod eth2wrap; + /// Implementations of AggSigDB. pub mod aggsigdb; diff --git a/crates/core/src/validatorapi/component.rs b/crates/core/src/validatorapi/component.rs index 29aa3af7..ded04ca0 100644 --- a/crates/core/src/validatorapi/component.rs +++ b/crates/core/src/validatorapi/component.rs @@ -13,7 +13,7 @@ use pluto_eth2api::{ EthBeaconNodeApiClient, GetAttesterDutiesRequest, GetAttesterDutiesResponse, GetProposerDutiesRequest, GetProposerDutiesResponse, GetSyncCommitteeDutiesRequest, GetSyncCommitteeDutiesResponse, - spec::phase0::{BLSPubKey, Epoch, Root, ValidatorIndex}, + spec::phase0::{BLSPubKey, Epoch, Root}, }; use pluto_eth2util::signing::{self, DomainName, SigningError}; use tokio::time::error::Elapsed; @@ -32,10 +32,10 @@ use super::{ VersionedAttestation, VersionedProposal, VersionedSignedAggregateAndProof, VersionedSignedBlindedProposal, VersionedSignedProposal, }, - validator_cache::CachedValidatorsProvider, }; use crate::{ dutydb::{Error as DutyDbError, MemDB}, + eth2wrap::valcache::{ActiveValidators, CachedValidatorsProvider}, signeddata::{ SyncContribution, VersionedAggregatedAttestation, VersionedProposal as UnsignedVersionedProposal, @@ -180,8 +180,7 @@ pub struct Component { /// selections / voluntary-exit / sync-committee submit handlers to /// translate validator-client-supplied `validator_index` values into /// DV root public keys. Mirrors Go's `c.eth2Cl.ActiveValidators(ctx)`, - /// which is itself backed by `app/eth2wrap`'s per-epoch validator - /// cache. + /// which is itself backed by `eth2wrap`'s per-epoch validator cache. #[allow(dead_code, reason = "consumed by submit_* handlers in later PRs")] validator_cache: Arc, } @@ -371,11 +370,9 @@ impl Component { /// Translates cache failures into `ApiError`s without leaking the /// underlying error into the client-visible message. Mirrors Go's /// `c.eth2Cl.ActiveValidators(ctx)`, which is itself implemented via - /// `app/eth2wrap`'s validator cache. + /// `eth2wrap`'s validator cache. #[allow(dead_code, reason = "consumed by submit_* handlers in later PRs")] - async fn fetch_active_validators( - &self, - ) -> Result, ApiError> { + async fn fetch_active_validators(&self) -> Result { tokio::time::timeout( UPSTREAM_REQUEST_TIMEOUT, self.validator_cache.active_validators(), @@ -384,7 +381,7 @@ impl Component { .map_err(|_: Elapsed| upstream_timeout("active validators"))? .map_err(|err| { ApiError::new(StatusCode::BAD_GATEWAY, "active validators lookup failed") - .with_boxed_source(err) + .with_source(err) }) } } @@ -856,21 +853,16 @@ mod tests { validatorapi::types::AttestationDataOpts, }; - /// In-memory stand-in for the per-epoch validator cache. Tests supply - /// the `validator_index -> root pubkey` map up front instead of - /// running a real beacon-node mock through a `ValidatorCache`. + /// In-memory stand-in for the per-epoch validator cache. Returns an + /// empty [`ActiveValidators`]; tests that need populated data go + /// through the real [`crate::eth2wrap::valcache::ValidatorCache`] with + /// a beacon mock instead. #[derive(Default)] - pub(super) struct TestValidatorCache(HashMap); + pub(super) struct TestValidatorCache; impl TestValidatorCache { - pub(super) fn arc( - map: HashMap, - ) -> Arc { - Arc::new(Self(map)) - } - pub(super) fn empty() -> Arc { - Self::arc(HashMap::new()) + Arc::new(Self) } } @@ -878,11 +870,17 @@ mod tests { impl CachedValidatorsProvider for TestValidatorCache { async fn active_validators( &self, + ) -> Result { + Ok(ActiveValidators::default()) + } + + async fn complete_validators( + &self, ) -> Result< - HashMap, - super::super::validator_cache::CachedValidatorsError, + crate::eth2wrap::valcache::CompleteValidators, + crate::eth2wrap::valcache::ValidatorCacheError, > { - Ok(self.0.clone()) + Ok(crate::eth2wrap::valcache::CompleteValidators::default()) } } @@ -1672,7 +1670,7 @@ mod tests { // ==================================================================== /// `fetch_active_validators` returns whatever the registered - /// `CachedValidatorsProvider` yields, untouched. Mirrors Go's + /// [`CachedValidatorsProvider`] yields, untouched. Mirrors Go's /// `c.eth2Cl.ActiveValidators(ctx)` return shape. #[tokio::test] async fn fetch_active_validators_returns_cache_contents() { @@ -1687,19 +1685,13 @@ mod tests { let eth2_cl = Arc::new(EthBeaconNodeApiClient::with_base_url("http://127.0.0.1:0").unwrap()); - let expected = HashMap::from([(1u64, dv_pubkey(0xA1)), (7u64, dv_pubkey(0xA7))]); - let component = Component::new_insecure( - eth2_cl, - dutydb, - 1, - TestValidatorCache::arc(expected.clone()), - ); + let component = Component::new_insecure(eth2_cl, dutydb, 1, TestValidatorCache::empty()); let got = component .fetch_active_validators() .await .expect("test cache always succeeds"); - assert_eq!(got, expected); + assert!(got.is_empty()); } /// A provider that surfaces a transport-style error is mapped to a 502 @@ -1707,17 +1699,30 @@ mod tests { /// message. #[tokio::test] async fn fetch_active_validators_maps_provider_error_to_502() { + use pluto_eth2api::EthBeaconNodeApiClientError; + struct FailingCache; #[async_trait] impl CachedValidatorsProvider for FailingCache { async fn active_validators( &self, + ) -> Result + { + Err(crate::eth2wrap::valcache::ValidatorCacheError::from( + EthBeaconNodeApiClientError::UnexpectedResponse, + )) + } + + async fn complete_validators( + &self, ) -> Result< - HashMap, - super::super::validator_cache::CachedValidatorsError, + crate::eth2wrap::valcache::CompleteValidators, + crate::eth2wrap::valcache::ValidatorCacheError, > { - Err("upstream unavailable".into()) + Err(crate::eth2wrap::valcache::ValidatorCacheError::from( + EthBeaconNodeApiClientError::UnexpectedResponse, + )) } } diff --git a/crates/core/src/validatorapi/mod.rs b/crates/core/src/validatorapi/mod.rs index 28a1d680..8442859c 100644 --- a/crates/core/src/validatorapi/mod.rs +++ b/crates/core/src/validatorapi/mod.rs @@ -9,7 +9,6 @@ pub mod handler; pub mod metrics; pub mod router; pub mod types; -pub mod validator_cache; #[cfg(test)] pub mod testutils; @@ -18,4 +17,3 @@ pub use component::Component; pub use error::ApiError; pub use handler::Handler; pub use router::new_router; -pub use validator_cache::{CachedValidatorsError, CachedValidatorsProvider}; diff --git a/crates/core/src/validatorapi/validator_cache.rs b/crates/core/src/validatorapi/validator_cache.rs deleted file mode 100644 index 34f22b5f..00000000 --- a/crates/core/src/validatorapi/validator_cache.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Cluster-wide active-validators lookup consumed by submit handlers. -//! -//! Mirrors Charon's `app/eth2wrap.CachedValidatorsProvider` interface: -//! submit handlers that have to translate a validator-client-supplied -//! `validator_index` into the cluster's DV root public key consult this -//! trait. Defined here in `pluto-core` so the validator API does not need -//! to depend on the application crate that owns the concrete per-epoch -//! cache implementation. - -use std::collections::HashMap; - -use async_trait::async_trait; -use pluto_eth2api::spec::phase0::{BLSPubKey, ValidatorIndex}; - -/// Boxed error returned by [`CachedValidatorsProvider`] methods. Kept -/// opaque so the trait does not bind callers to any single backing -/// implementation's error type. -pub type CachedValidatorsError = Box; - -/// Provides the cluster's currently active validators, indexed by -/// validator index. Mirrors Go's `eth2Cl.ActiveValidators(ctx)`, which is -/// itself backed by `app/eth2wrap`'s per-epoch validator cache; the -/// validator-API [`Component`](super::Component) calls through this trait -/// so the cache is the single source of truth across duty handlers -/// without `pluto-core` depending on the cache crate. -/// -/// Implementations may populate the underlying cache on demand — callers -/// must not assume the call is non-blocking. -#[async_trait] -pub trait CachedValidatorsProvider: Send + Sync { - /// Returns the `validator_index -> DV root BLS public key` map. - async fn active_validators( - &self, - ) -> Result, CachedValidatorsError>; -} From aaca795e2a86559d6b0c0f7c481c70aa386c3d63 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:49:14 +0200 Subject: [PATCH 3/3] refactor: move eth2wrap to a new crate + make CachedValidatorsProvider async --- .gitignore | 1 + Cargo.lock | 36 +++++- Cargo.toml | 4 + crates/app/Cargo.toml | 2 - crates/app/src/eth2wrap/mod.rs | 2 - crates/app/src/lib.rs | 3 - crates/core-utils/Cargo.toml | 22 ++++ crates/core-utils/build.rs | 12 ++ crates/core-utils/src/lib.rs | 7 ++ crates/core-utils/src/pubkey.rs | 112 ++++++++++++++++++ crates/{core => core-utils}/src/version.rs | 0 crates/core/Cargo.toml | 3 +- crates/core/src/eth2wrap/mod.rs | 10 -- crates/core/src/lib.rs | 8 +- crates/core/src/types.rs | 109 +---------------- crates/core/src/validatorapi/component.rs | 27 ++--- crates/eth2wrap/Cargo.toml | 27 +++++ crates/eth2wrap/src/lib.rs | 7 ++ .../src/eth2wrap => eth2wrap/src}/valcache.rs | 2 +- .../src/eth2wrap => eth2wrap/src}/version.rs | 2 +- 20 files changed, 245 insertions(+), 151 deletions(-) delete mode 100644 crates/app/src/eth2wrap/mod.rs create mode 100644 crates/core-utils/Cargo.toml create mode 100644 crates/core-utils/build.rs create mode 100644 crates/core-utils/src/lib.rs create mode 100644 crates/core-utils/src/pubkey.rs rename crates/{core => core-utils}/src/version.rs (100%) delete mode 100644 crates/core/src/eth2wrap/mod.rs create mode 100644 crates/eth2wrap/Cargo.toml create mode 100644 crates/eth2wrap/src/lib.rs rename crates/{core/src/eth2wrap => eth2wrap/src}/valcache.rs (99%) rename crates/{app/src/eth2wrap => eth2wrap/src}/version.rs (99%) diff --git a/.gitignore b/.gitignore index 304cfad0..bcefc1af 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ test-infra/sszfixtures/sszfixtures .claude/worktrees/ .claude/scheduled_tasks.lock +.claude/notes/ test-cluster \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f31b1b44..cc85c72a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5476,7 +5476,6 @@ dependencies = [ "k256", "pluto-build-proto", "pluto-cluster", - "pluto-core", "pluto-crypto", "pluto-eth2api", "pluto-k1util", @@ -5484,7 +5483,6 @@ dependencies = [ "pluto-testutil", "prost 0.14.3", "prost-types 0.14.3", - "regex", "reqwest 0.13.3", "serde", "serde_json", @@ -5606,9 +5604,11 @@ dependencies = [ "libp2p", "pluto-build-proto", "pluto-cluster", + "pluto-core-utils", "pluto-crypto", "pluto-eth2api", "pluto-eth2util", + "pluto-eth2wrap", "pluto-featureset", "pluto-k1util", "pluto-p2p", @@ -5618,7 +5618,6 @@ dependencies = [ "prost 0.14.3", "prost-types 0.14.3", "rand 0.8.6", - "regex", "serde", "serde_json", "test-case", @@ -5632,6 +5631,20 @@ dependencies = [ "wiremock", ] +[[package]] +name = "pluto-core-utils" +version = "1.7.1" +dependencies = [ + "built", + "chrono", + "hex", + "pluto-crypto", + "regex", + "serde", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "pluto-crypto" version = "1.7.1" @@ -5771,6 +5784,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "pluto-eth2wrap" +version = "1.7.1" +dependencies = [ + "async-trait", + "pluto-build-proto", + "pluto-core-utils", + "pluto-eth2api", + "pluto-testutil", + "regex", + "test-case", + "thiserror 2.0.18", + "tokio", + "tracing", + "wiremock", +] + [[package]] name = "pluto-featureset" version = "1.7.1" diff --git a/Cargo.toml b/Cargo.toml index bd7beb79..e080510b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,13 @@ members = [ "crates/cli", "crates/cluster", "crates/core", + "crates/core-utils", "crates/crypto", "crates/dkg", "crates/eth2api", "crates/eth2util", "crates/eth1wrap", + "crates/eth2wrap", "crates/featureset", "crates/k1util", "crates/relay-server", @@ -131,6 +133,8 @@ pluto-tracing = { path = "crates/tracing" } pluto-p2p = { path = "crates/p2p" } pluto-peerinfo = { path = "crates/peerinfo" } pluto-frost = { path = "crates/frost" } +pluto-eth2wrap = { path = "crates/eth2wrap" } +pluto-core-utils = { path = "crates/core-utils"} [workspace.lints.rust] missing_docs = "deny" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 77f14edf..fe283bdb 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -9,13 +9,11 @@ publish.workspace = true [dependencies] backon.workspace = true chrono.workspace = true -pluto-core.workspace = true pluto-eth2api.workspace = true tokio.workspace = true tokio-util.workspace = true prost.workspace = true prost-types.workspace = true -regex.workspace = true thiserror.workspace = true tracing.workspace = true url.workspace = true diff --git a/crates/app/src/eth2wrap/mod.rs b/crates/app/src/eth2wrap/mod.rs deleted file mode 100644 index cd3ab232..00000000 --- a/crates/app/src/eth2wrap/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -/// Validate Beacon node versions -pub mod version; diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 87d7061e..3867a661 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -16,9 +16,6 @@ pub mod retry; /// Obol API client for interacting with the Obol network API. pub mod obolapi; -/// Ethereum CL RPC client management. -pub mod eth2wrap; - /// Private key locking service. pub mod privkeylock; diff --git a/crates/core-utils/Cargo.toml b/crates/core-utils/Cargo.toml new file mode 100644 index 00000000..9e7d6145 --- /dev/null +++ b/crates/core-utils/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "pluto-core-utils" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +chrono.workspace = true +regex.workspace = true +thiserror.workspace = true +tracing.workspace = true +pluto-crypto.workspace = true +serde.workspace = true +hex.workspace = true + +[build-dependencies] +built.workspace = true + +[lints] +workspace = true diff --git a/crates/core-utils/build.rs b/crates/core-utils/build.rs new file mode 100644 index 00000000..9f05e941 --- /dev/null +++ b/crates/core-utils/build.rs @@ -0,0 +1,12 @@ +//! # Charon Core Build Script +//! +//! This build script compiles the protobuf files. + +use std::io::Result; + +fn main() -> Result<()> { + built::write_built_file()?; + println!("cargo:rerun-if-changed=../../Cargo.lock"); + + Ok(()) +} diff --git a/crates/core-utils/src/lib.rs b/crates/core-utils/src/lib.rs new file mode 100644 index 00000000..287975a1 --- /dev/null +++ b/crates/core-utils/src/lib.rs @@ -0,0 +1,7 @@ +//! Dependency-free core utilies + +/// Semver version parsing utilities. +pub mod version; + +/// PubKey type +pub mod pubkey; diff --git a/crates/core-utils/src/pubkey.rs b/crates/core-utils/src/pubkey.rs new file mode 100644 index 00000000..ba0a6c2a --- /dev/null +++ b/crates/core-utils/src/pubkey.rs @@ -0,0 +1,112 @@ +// In golang implementation they use pk_len = 98, which is 0x + [48 bytes] +// We use pk_len = 48, which is [48 bytes], the main difference is that we store +// the pub key as [u8; 48] instead of string. +// [original implementation](https://github.com/ObolNetwork/charon/blob/b3008103c5429b031b63518195f4c49db4e9a68d/core/types.go#L264) +/// Public key length +pub const PK_LEN: usize = 48; + +use std::fmt::Display; + +pub use pluto_crypto::types::{SIGNATURE_LENGTH, Signature}; +use serde::{Deserialize, Serialize}; + +/// Public key struct +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PubKey(pub(crate) [u8; PK_LEN]); + +impl Serialize for PubKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl TryFrom<&str> for PubKey { + type Error = PubKeyError; + + fn try_from(value: &str) -> Result { + let value = value.strip_prefix("0x").unwrap_or(value); + let hex_value = hex::decode(value).map_err(|_| PubKeyError::InvalidString)?; + PubKey::try_from(hex_value.as_slice()) + } +} + +impl<'de> Deserialize<'de> for PubKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let hex_str = String::deserialize(deserializer)?; + let hex_str = hex_str.strip_prefix("0x").unwrap_or(&hex_str); + + let bytes = hex::decode(hex_str).map_err(serde::de::Error::custom)?; + + if bytes.len() != PK_LEN { + return Err(serde::de::Error::custom(format!( + "invalid public key length: got {}, want {}", + bytes.len(), + PK_LEN + ))); + } + + let mut pk = [0u8; PK_LEN]; + pk.copy_from_slice(&bytes); + Ok(PubKey(pk)) + } +} + +impl From<[u8; PK_LEN]> for PubKey { + fn from(pk: [u8; PK_LEN]) -> Self { + PubKey(pk) + } +} + +/// Public key error type +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PubKeyError { + /// Invalid public key length. + InvalidLength, + /// Invalid public key string. + InvalidString, +} + +impl PubKey { + /// Create a new public key. + pub fn new(pk: [u8; PK_LEN]) -> Self { + PubKey(pk) + } + + /// Returns logging-friendly abbreviated form: "b82_97f" + pub fn abbreviated(&self) -> String { + let hex = hex::encode(self.0); + format!("{}_{}", &hex[0..3], &hex[93..96]) + } +} + +impl TryFrom<&[u8]> for PubKey { + type Error = PubKeyError; + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != PK_LEN { + return Err(PubKeyError::InvalidLength); + } + let mut arr = [0u8; PK_LEN]; + arr.copy_from_slice(bytes); + Ok(PubKey(arr)) + } +} + +impl Display for PubKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "0x{}", hex::encode(self.0)) + } +} + +/// Implement AsRef<[u8]> for PubKey to allow for easy conversion to bytes. +impl AsRef<[u8]> for PubKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/crates/core/src/version.rs b/crates/core-utils/src/version.rs similarity index 100% rename from crates/core/src/version.rs rename to crates/core-utils/src/version.rs diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index fb816537..10cf41e6 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -20,11 +20,12 @@ k256.workspace = true libp2p.workspace = true vise.workspace = true pluto-crypto.workspace = true +pluto-core-utils.workspace = true pluto-eth2api.workspace = true +pluto-eth2wrap.workspace = true pluto-k1util.workspace = true prost.workspace = true prost-types.workspace = true -regex.workspace = true serde.workspace = true serde_json.workspace = true base64.workspace = true diff --git a/crates/core/src/eth2wrap/mod.rs b/crates/core/src/eth2wrap/mod.rs deleted file mode 100644 index 56e80b6b..00000000 --- a/crates/core/src/eth2wrap/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Wrappers around the upstream beacon-node client. -//! -//! Mirrors Charon's `app/eth2wrap` package: a layer that decorates the -//! raw beacon-node API with cluster-wide concerns (caching, error -//! mapping). Lives in `pluto-core` so downstream modules (e.g. the -//! validator API [`crate::validatorapi`]) can consume the wrappers -//! without depending on `pluto-app`. - -/// Cache of validators retrieved from the Beacon node. -pub mod valcache; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index bfd1cf6a..0189b627 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -17,9 +17,6 @@ pub mod consensus; /// Protobuf definitions. pub mod corepb; -/// Semver version parsing utilities. -pub mod version; - /// Duty deadline tracking and notification. pub mod deadline; @@ -36,9 +33,6 @@ pub mod validatorapi; /// SigAgg — threshold BLS signature aggregation. pub mod sigagg; -/// `eth2wrap` — wrappers around the upstream beacon-node API client. -pub mod eth2wrap; - /// Implementations of AggSigDB. pub mod aggsigdb; @@ -56,6 +50,8 @@ pub use parsigex_codec::ParSigExCodecError; /// participation. pub mod tracker; +pub use pluto_core_utils::version; + /// Test utilities. #[cfg(test)] pub mod testutils; diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 0d85d9c3..854d144a 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -314,114 +314,7 @@ pub enum ProposalType { Synthetic, } -// In golang implementation they use pk_len = 98, which is 0x + [48 bytes] -// We use pk_len = 48, which is [48 bytes], the main difference is that we store -// the pub key as [u8; 48] instead of string. -// [original implementation](https://github.com/ObolNetwork/charon/blob/b3008103c5429b031b63518195f4c49db4e9a68d/core/types.go#L264) -const PK_LEN: usize = 48; - -pub use pluto_crypto::types::{SIGNATURE_LENGTH, Signature}; - -/// Public key struct -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct PubKey(pub(crate) [u8; PK_LEN]); - -impl Serialize for PubKey { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl TryFrom<&str> for PubKey { - type Error = PubKeyError; - - fn try_from(value: &str) -> Result { - let value = value.strip_prefix("0x").unwrap_or(value); - let hex_value = hex::decode(value).map_err(|_| PubKeyError::InvalidString)?; - PubKey::try_from(hex_value.as_slice()) - } -} - -impl<'de> Deserialize<'de> for PubKey { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let hex_str = String::deserialize(deserializer)?; - let hex_str = hex_str.strip_prefix("0x").unwrap_or(&hex_str); - - let bytes = hex::decode(hex_str).map_err(serde::de::Error::custom)?; - - if bytes.len() != PK_LEN { - return Err(serde::de::Error::custom(format!( - "invalid public key length: got {}, want {}", - bytes.len(), - PK_LEN - ))); - } - - let mut pk = [0u8; PK_LEN]; - pk.copy_from_slice(&bytes); - Ok(PubKey(pk)) - } -} - -impl From<[u8; PK_LEN]> for PubKey { - fn from(pk: [u8; PK_LEN]) -> Self { - PubKey(pk) - } -} - -/// Public key error type -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PubKeyError { - /// Invalid public key length. - InvalidLength, - /// Invalid public key string. - InvalidString, -} - -impl PubKey { - /// Create a new public key. - pub fn new(pk: [u8; PK_LEN]) -> Self { - PubKey(pk) - } - - /// Returns logging-friendly abbreviated form: "b82_97f" - pub fn abbreviated(&self) -> String { - let hex = hex::encode(self.0); - format!("{}_{}", &hex[0..3], &hex[93..96]) - } -} - -impl TryFrom<&[u8]> for PubKey { - type Error = PubKeyError; - - fn try_from(bytes: &[u8]) -> Result { - if bytes.len() != PK_LEN { - return Err(PubKeyError::InvalidLength); - } - let mut arr = [0u8; PK_LEN]; - arr.copy_from_slice(bytes); - Ok(PubKey(arr)) - } -} - -impl Display for PubKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "0x{}", hex::encode(self.0)) - } -} - -/// Implement AsRef<[u8]> for PubKey to allow for easy conversion to bytes. -impl AsRef<[u8]> for PubKey { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} +pub use pluto_core_utils::pubkey::*; // todo: add toEth2Format for the pub key // https://github.com/ObolNetwork/charon/blob/b3008103c5429b031b63518195f4c49db4e9a68d/core/types.go#L311 diff --git a/crates/core/src/validatorapi/component.rs b/crates/core/src/validatorapi/component.rs index ded04ca0..2c89c938 100644 --- a/crates/core/src/validatorapi/component.rs +++ b/crates/core/src/validatorapi/component.rs @@ -35,7 +35,6 @@ use super::{ }; use crate::{ dutydb::{Error as DutyDbError, MemDB}, - eth2wrap::valcache::{ActiveValidators, CachedValidatorsProvider}, signeddata::{ SyncContribution, VersionedAggregatedAttestation, VersionedProposal as UnsignedVersionedProposal, @@ -43,6 +42,7 @@ use crate::{ types::{Duty, ParSignedDataSet, PubKey, Signature, SignedData}, version, }; +use pluto_eth2wrap::valcache::{ActiveValidators, CachedValidatorsProvider}; /// Boxed error returned by registered callbacks. pub type CallbackError = Box; @@ -853,9 +853,11 @@ mod tests { validatorapi::types::AttestationDataOpts, }; + use pluto_eth2wrap as eth2wrap; + /// In-memory stand-in for the per-epoch validator cache. Returns an /// empty [`ActiveValidators`]; tests that need populated data go - /// through the real [`crate::eth2wrap::valcache::ValidatorCache`] with + /// through the real [`eth2wrap::valcache::ValidatorCache`] with /// a beacon mock instead. #[derive(Default)] pub(super) struct TestValidatorCache; @@ -870,17 +872,15 @@ mod tests { impl CachedValidatorsProvider for TestValidatorCache { async fn active_validators( &self, - ) -> Result { + ) -> Result { Ok(ActiveValidators::default()) } async fn complete_validators( &self, - ) -> Result< - crate::eth2wrap::valcache::CompleteValidators, - crate::eth2wrap::valcache::ValidatorCacheError, - > { - Ok(crate::eth2wrap::valcache::CompleteValidators::default()) + ) -> Result + { + Ok(eth2wrap::valcache::CompleteValidators::default()) } } @@ -1707,9 +1707,8 @@ mod tests { impl CachedValidatorsProvider for FailingCache { async fn active_validators( &self, - ) -> Result - { - Err(crate::eth2wrap::valcache::ValidatorCacheError::from( + ) -> Result { + Err(eth2wrap::valcache::ValidatorCacheError::from( EthBeaconNodeApiClientError::UnexpectedResponse, )) } @@ -1717,10 +1716,10 @@ mod tests { async fn complete_validators( &self, ) -> Result< - crate::eth2wrap::valcache::CompleteValidators, - crate::eth2wrap::valcache::ValidatorCacheError, + eth2wrap::valcache::CompleteValidators, + eth2wrap::valcache::ValidatorCacheError, > { - Err(crate::eth2wrap::valcache::ValidatorCacheError::from( + Err(eth2wrap::valcache::ValidatorCacheError::from( EthBeaconNodeApiClientError::UnexpectedResponse, )) } diff --git a/crates/eth2wrap/Cargo.toml b/crates/eth2wrap/Cargo.toml new file mode 100644 index 00000000..345e326f --- /dev/null +++ b/crates/eth2wrap/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "pluto-eth2wrap" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +async-trait.workspace = true +pluto-eth2api.workspace = true +pluto-core-utils.workspace = true +tokio.workspace = true +regex.workspace = true +thiserror.workspace = true +tracing.workspace = true + +[build-dependencies] +pluto-build-proto.workspace = true + +[dev-dependencies] +pluto-testutil.workspace = true +wiremock.workspace = true +test-case.workspace = true + +[lints] +workspace = true diff --git a/crates/eth2wrap/src/lib.rs b/crates/eth2wrap/src/lib.rs new file mode 100644 index 00000000..2fd18586 --- /dev/null +++ b/crates/eth2wrap/src/lib.rs @@ -0,0 +1,7 @@ +//! Ethereum CL RPC client management. + +/// Validate Beacon node versions +pub mod version; + +/// Cache of validators retrieved from the Beacon node. +pub mod valcache; diff --git a/crates/core/src/eth2wrap/valcache.rs b/crates/eth2wrap/src/valcache.rs similarity index 99% rename from crates/core/src/eth2wrap/valcache.rs rename to crates/eth2wrap/src/valcache.rs index a40855c0..145efbb8 100644 --- a/crates/core/src/eth2wrap/valcache.rs +++ b/crates/eth2wrap/src/valcache.rs @@ -1,5 +1,5 @@ -use crate::types::PubKey; use async_trait::async_trait; +use pluto_core_utils::pubkey::PubKey; use pluto_eth2api::{ EthBeaconNodeApiClient, EthBeaconNodeApiClientError, GetStateValidatorsResponseResponse, GetStateValidatorsResponseResponseDatum, PostStateValidatorsRequest, diff --git a/crates/app/src/eth2wrap/version.rs b/crates/eth2wrap/src/version.rs similarity index 99% rename from crates/app/src/eth2wrap/version.rs rename to crates/eth2wrap/src/version.rs index fd55a821..a27863d8 100644 --- a/crates/app/src/eth2wrap/version.rs +++ b/crates/eth2wrap/src/version.rs @@ -1,4 +1,4 @@ -use pluto_core::version::{self}; +use pluto_core_utils::version::{self}; use std::sync::LazyLock; use tracing::warn;