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 51970966..00000000 --- a/crates/app/src/eth2wrap/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// Validate Beacon node versions -pub mod version; - -/// Cache of Validators retrieved from the Beacon node -pub mod valcache; 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/lib.rs b/crates/core/src/lib.rs index 6cd84def..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; @@ -53,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 969f92e7..2c89c938 100644 --- a/crates/core/src/validatorapi/component.rs +++ b/crates/core/src/validatorapi/component.rs @@ -42,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; @@ -175,6 +176,13 @@ 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 `eth2wrap`'s per-epoch validator cache. + #[allow(dead_code, reason = "consumed by submit_* handlers in later PRs")] + validator_cache: Arc, } impl Component { @@ -185,6 +193,7 @@ impl Component { share_idx: u64, pub_share_by_pubkey: HashMap, builder_enabled: bool, + validator_cache: Arc, ) -> Self { Self { eth2_cl, @@ -200,6 +209,7 @@ impl Component { await_agg_sig_db_fn: None, duty_def_fn: None, pub_key_by_att_fn: None, + validator_cache, } } @@ -212,6 +222,7 @@ impl Component { eth2_cl: Arc, dutydb: Arc, share_idx: u64, + validator_cache: Arc, ) -> Self { Self { eth2_cl, @@ -227,6 +238,7 @@ impl Component { await_agg_sig_db_fn: None, duty_def_fn: None, pub_key_by_att_fn: None, + validator_cache, } } @@ -352,6 +364,26 @@ 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 + /// `eth2wrap`'s validator cache. + #[allow(dead_code, reason = "consumed by submit_* handlers in later PRs")] + async fn fetch_active_validators(&self) -> Result { + 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_source(err) + }) + } } /// Errors returned by [`Component::verify_partial_sig`]. @@ -821,6 +853,37 @@ 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 [`eth2wrap::valcache::ValidatorCache`] with + /// a beacon mock instead. + #[derive(Default)] + pub(super) struct TestValidatorCache; + + impl TestValidatorCache { + pub(super) fn empty() -> Arc { + Arc::new(Self) + } + } + + #[async_trait] + impl CachedValidatorsProvider for TestValidatorCache { + async fn active_validators( + &self, + ) -> Result { + Ok(ActiveValidators::default()) + } + + async fn complete_validators( + &self, + ) -> Result + { + Ok(eth2wrap::valcache::CompleteValidators::default()) + } + } + /// Schedules every duty with a deadline at `MAX_UTC`, so duties are /// `Scheduled` but never naturally expire. struct FarFutureCalculator; @@ -844,7 +907,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 +1150,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 +1337,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 +1568,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 +1651,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 +1664,81 @@ 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 component = Component::new_insecure(eth2_cl, dutydb, 1, TestValidatorCache::empty()); + + let got = component + .fetch_active_validators() + .await + .expect("test cache always succeeds"); + assert!(got.is_empty()); + } + + /// 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() { + use pluto_eth2api::EthBeaconNodeApiClientError; + + struct FailingCache; + + #[async_trait] + impl CachedValidatorsProvider for FailingCache { + async fn active_validators( + &self, + ) -> Result { + Err(eth2wrap::valcache::ValidatorCacheError::from( + EthBeaconNodeApiClientError::UnexpectedResponse, + )) + } + + async fn complete_validators( + &self, + ) -> Result< + eth2wrap::valcache::CompleteValidators, + eth2wrap::valcache::ValidatorCacheError, + > { + Err(eth2wrap::valcache::ValidatorCacheError::from( + EthBeaconNodeApiClientError::UnexpectedResponse, + )) + } + } + + 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/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/app/src/eth2wrap/valcache.rs b/crates/eth2wrap/src/valcache.rs similarity index 98% rename from crates/app/src/eth2wrap/valcache.rs rename to crates/eth2wrap/src/valcache.rs index 3ec4d6b8..145efbb8 100644 --- a/crates/app/src/eth2wrap/valcache.rs +++ b/crates/eth2wrap/src/valcache.rs @@ -1,4 +1,5 @@ -use pluto_core::types::PubKey; +use async_trait::async_trait; +use pluto_core_utils::pubkey::PubKey; 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/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;