Skip to content

Commit 4b6aa1f

Browse files
authored
feat: add eth2wrap/version (#121)
* Initial `eth2wrap` module * Add initial `version` * Add tests * Merge remote-tracking branch 'origin/main' into emlautarom1/eth2wrap * Use `SemVer::parse` instead of `SemVer::try_from` for version parsing * Replace function with static HashMap - Avoids parsing on each request * Fix clippy lint errors * Add `RUSTC_BOOTSTRAP` - Prevents VSCode from rebuilding everything during tests * Merge remote-tracking branch 'origin/main' into emlautarom1/eth2wrap/version
1 parent 9ad5a5a commit 4b6aa1f

7 files changed

Lines changed: 169 additions & 6 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ prost-build = "0.14"
4545
prost-types = "0.14"
4646
rand = { version = "0.8", features = ["std_rng"] }
4747
rand_core = "0.6"
48+
regex = "1.12"
4849
serde = { version = "1.0", features = ["derive"] }
4950
serde_json = { version = "1.0" }
5051
thiserror = "2.0"
@@ -59,7 +60,6 @@ base64 = "0.22"
5960
sha3 = "0.10"
6061
k256 = { version = "0.13.4", features = ["ecdsa", "sha256"] }
6162
criterion = "0.8.0"
62-
regex = "1.12"
6363
reqwest = "0.13"
6464
http = "1.4"
6565
tempfile = "3.24"

crates/charon/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ tokio.workspace = true
1515
tokio-util.workspace = true
1616
prost.workspace = true
1717
prost-types.workspace = true
18+
regex.workspace = true
1819
thiserror.workspace = true
1920
tracing.workspace = true
2021
tracing-subscriber.workspace = true

crates/charon/src/eth2wrap/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/// Validate Beacon Node versions
2+
pub mod version;
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
use charon_core::version::{self};
2+
use std::sync::LazyLock;
3+
use tracing::warn;
4+
5+
type Result<T> = std::result::Result<T, BeaconNodeVersionError>;
6+
7+
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
8+
enum BeaconNodeVersionError {
9+
#[error("Version string has an unexpected format")]
10+
InvalidFormat,
11+
12+
#[error("Unknown beacon node client")]
13+
UnknownClient,
14+
15+
#[error("Beacon node client version is too old")]
16+
TooOld {
17+
client: version::SemVer,
18+
minimum: version::SemVer,
19+
},
20+
}
21+
22+
static MINIMUM_BEACON_NODE_VERSIONS: LazyLock<std::collections::HashMap<&str, version::SemVer>> =
23+
LazyLock::new(|| {
24+
#[allow(clippy::unwrap_used, reason = "literals should be valid semver")]
25+
std::collections::HashMap::from([
26+
("lighthouse", version::SemVer::parse("v8.0.0-rc.0").unwrap()),
27+
("teku", version::SemVer::parse("v25.9.3").unwrap()),
28+
("lodestar", version::SemVer::parse("v1.35.0-rc.1").unwrap()),
29+
("nimbus", version::SemVer::parse("v25.9.2").unwrap()),
30+
("prysm", version::SemVer::parse("v6.1.0").unwrap()),
31+
("grandine", version::SemVer::parse("v2.0.0-rc0").unwrap()),
32+
])
33+
});
34+
35+
static VERSION_EXTRACT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
36+
regex::Regex::new(r"^([^/]+)/v?([0-9]+\.[0-9]+\.[0-9]+)").expect("invalid regex")
37+
});
38+
39+
fn check_beacon_node_version_status(bn_version: &str) -> Result<()> {
40+
let matches = VERSION_EXTRACT_REGEX
41+
.captures(bn_version)
42+
.ok_or(BeaconNodeVersionError::InvalidFormat)?;
43+
44+
if matches.len() != 3 {
45+
return Err(BeaconNodeVersionError::InvalidFormat);
46+
}
47+
48+
let client = version::SemVer::parse(format!("v{}", &matches[2]))
49+
.map_err(|_| BeaconNodeVersionError::InvalidFormat)?;
50+
51+
let name = &matches[1];
52+
let minimum = MINIMUM_BEACON_NODE_VERSIONS
53+
.get(&name.to_lowercase().as_str())
54+
.ok_or(BeaconNodeVersionError::UnknownClient)?
55+
.clone();
56+
57+
if client < minimum {
58+
return Err(BeaconNodeVersionError::TooOld { client, minimum });
59+
}
60+
61+
Ok(())
62+
}
63+
64+
/// Checks the version of the beacon node client and logs a warning if the
65+
/// version is below the minimum or if the client is not recognized.
66+
pub fn check_beacon_node_version(bn_version: &str) {
67+
match check_beacon_node_version_status(bn_version) {
68+
Err(BeaconNodeVersionError::InvalidFormat) => {
69+
warn!(
70+
input = bn_version,
71+
"Failed to parse beacon node version string due to unexpected format"
72+
);
73+
}
74+
Err(BeaconNodeVersionError::UnknownClient) => {
75+
warn!(
76+
client = bn_version,
77+
"Unknown beacon node client not in supported client list"
78+
);
79+
}
80+
Err(BeaconNodeVersionError::TooOld { client, minimum }) => {
81+
warn!(
82+
client_version = %client,
83+
minimum_required = %minimum,
84+
"Beacon node client version is below the minimum supported version. Please upgrade your beacon node."
85+
);
86+
}
87+
Ok(()) => { /* Do nothing */ }
88+
}
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use super::*;
94+
95+
#[test]
96+
fn check_beacon_node_version_status() {
97+
let tc = vec![
98+
// Teku
99+
(
100+
"teku/v25.9.3/linux-x86_64/-eclipseadoptium-openjdk64bitservervm-java-21",
101+
Ok(()),
102+
),
103+
(
104+
"teku/vUNKNOWN+g40561a9/linux-x86_64/-eclipseadoptium-openjdk64bitservervm-java-21",
105+
Err(BeaconNodeVersionError::InvalidFormat),
106+
),
107+
// Lighthouse
108+
("Lighthouse/v8.0.1-e42406d/x86_64-linux", Ok(())),
109+
("Lighthouse/v8.0.0-54f7bc5/aarch64-linux", Ok(())),
110+
// Lodestar
111+
("Lodestar/v1.35.0/8335180", Ok(())),
112+
("Lodestar/v1.36.0/1a34f98", Ok(())),
113+
// Nimbus
114+
("Nimbus/v26.4.1-77cfa7-stateofus", Ok(())),
115+
("Nimbus/v26.5.0-d2f233-stateofus", Ok(())),
116+
(
117+
"Nimbus/v25.9.0-c7e5ca-stateofus",
118+
Err(BeaconNodeVersionError::TooOld {
119+
client: version::SemVer::parse("v25.9.0").unwrap(),
120+
minimum: version::SemVer::parse("v25.9.2").unwrap(),
121+
}),
122+
),
123+
// Prysm
124+
(
125+
"Prysm/v5.3.2 (linux amd64)",
126+
Err(BeaconNodeVersionError::TooOld {
127+
client: version::SemVer::parse("v5.3.2").unwrap(),
128+
minimum: version::SemVer::parse("v6.1.0").unwrap(),
129+
}),
130+
),
131+
("Prysm/v6.1.2 (linux amd64)", Ok(())),
132+
("Prysm/v6.2.0 (linux amd64)", Ok(())),
133+
// Grandine
134+
("Grandine/2.1.0-29cb5c1/x86_64-linux2025-05-19", Ok(())),
135+
// Additional error cases
136+
("", Err(BeaconNodeVersionError::InvalidFormat)),
137+
("justastring", Err(BeaconNodeVersionError::InvalidFormat)),
138+
("/v7.0.0", Err(BeaconNodeVersionError::InvalidFormat)),
139+
(
140+
"UnknownClient/v7.0.0",
141+
Err(BeaconNodeVersionError::UnknownClient),
142+
),
143+
("Lighthouse/", Err(BeaconNodeVersionError::InvalidFormat)),
144+
(
145+
"Lighthouse/vBAD",
146+
Err(BeaconNodeVersionError::InvalidFormat),
147+
),
148+
];
149+
150+
for (input, expected) in tc {
151+
let result = super::check_beacon_node_version_status(input);
152+
assert_eq!(result, expected, "input = {input}");
153+
}
154+
}
155+
}

crates/charon/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ pub mod retry;
1616
/// Deadline
1717
pub mod deadline;
1818

19-
/// Ethereum EL RPC client management
19+
/// Ethereum EL RPC client management.
2020
pub mod eth1wrap;
2121

2222
/// Featureset defines a set of global features and their rollout status.
2323
pub mod featureset;
2424

2525
/// Obol API client for interacting with the Obol network API.
2626
pub mod obolapi;
27+
28+
/// Ethereum CL RPC client management.
29+
pub mod eth2wrap;

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
chmod +x .githooks/* && git config --local core.hooksPath .githooks/
3636
'';
3737

38+
RUSTC_BOOTSTRAP = "1";
3839
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.openssl ];
3940
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
4041
};

0 commit comments

Comments
 (0)