Skip to content

Commit 5a17143

Browse files
committed
Reload validator public keys from Web3Signer instances on each epoch if previously failed to load or CLI option is set
1 parent 85bdb1b commit 5a17143

File tree

6 files changed

+112
-7
lines changed

6 files changed

+112
-7
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

grandine/src/grandine_args.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,10 @@ struct ValidatorOptions {
685685
#[clap(long, num_args = 1.., value_delimiter = ',')]
686686
web3signer_public_keys: Vec<PublicKeyBytes>,
687687

688+
/// Refetches keys from Web3Signer once every epoch. This overwrites changes done via Keymanager API
689+
#[clap(long)]
690+
web3signer_refresh_keys_every_epoch: bool,
691+
688692
/// [DEPRECATED] List of Web3Signer API URLs
689693
#[clap(long, num_args = 1..)]
690694
web3signer_api_urls: Vec<Url>,
@@ -873,6 +877,7 @@ impl GrandineArgs {
873877
builder_max_skipped_slots_per_epoch,
874878
use_validator_key_cache,
875879
web3signer_public_keys,
880+
web3signer_refresh_keys_every_epoch,
876881
web3signer_api_urls,
877882
web3signer_urls,
878883
slashing_protection_history_limit,
@@ -1152,6 +1157,7 @@ impl GrandineArgs {
11521157

11531158
let web3signer_config = Web3SignerConfig {
11541159
public_keys: web3signer_public_keys.into_iter().collect(),
1160+
allow_to_reload_keys: web3signer_refresh_keys_every_epoch,
11551161
urls: web3signer_urls,
11561162
};
11571163

signer/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ workspace = true
1010
anyhow = { workspace = true }
1111
bls = { workspace = true }
1212
builder_api = { workspace = true }
13-
derive_more = { workspace = true }
1413
futures = { workspace = true }
1514
itertools = { workspace = true }
1615
log = { workspace = true }

signer/src/signer.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use futures::{
1010
try_join,
1111
};
1212
use itertools::Itertools as _;
13+
use log::info;
1314
use prometheus_metrics::Metrics;
1415
use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _};
1516
use reqwest::{Client, Url};
@@ -151,6 +152,38 @@ impl Signer {
151152
self.sign_methods.is_empty()
152153
}
153154

155+
pub async fn refresh_keys_from_web3signer(&mut self) {
156+
for (url, remote_keys) in self.web3signer.load_public_keys().await {
157+
self.sign_methods
158+
.retain(|public_key, sign_method| match sign_method {
159+
SignMethod::SecretKey(_, _) => true,
160+
SignMethod::Web3Signer(api_url) => {
161+
let retain = url != api_url || remote_keys.contains(public_key);
162+
163+
if !retain {
164+
info!(
165+
"Validator credentials with public key {:?} were removed from Web3Signer at {}",
166+
public_key, url,
167+
);
168+
}
169+
170+
retain
171+
}
172+
});
173+
174+
for public_key in remote_keys {
175+
self.sign_methods.entry(public_key).or_insert_with(|| {
176+
info!(
177+
"Validator credentials with public key {:?} were added to Web3Signer at {}",
178+
public_key, url,
179+
);
180+
181+
SignMethod::Web3Signer(url.clone())
182+
});
183+
}
184+
}
185+
}
186+
154187
pub async fn sign<'block, P: Preset>(
155188
&self,
156189
message: SigningMessage<'block, P>,

signer/src/web3signer/api.rs

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use std::{
55

66
use anyhow::Result;
77
use bls::{PublicKeyBytes, SignatureBytes};
8-
use derive_more::Constructor;
98
use log::warn;
109
use prometheus_metrics::Metrics;
1110
use reqwest::{Client, Url};
@@ -17,24 +16,36 @@ use super::types::{SigningRequest, SigningResponse};
1716

1817
#[derive(Clone, Default, Debug)]
1918
pub struct Config {
19+
pub allow_to_reload_keys: bool,
2020
pub public_keys: HashSet<PublicKeyBytes>,
2121
pub urls: Vec<Url>,
2222
}
2323

24-
#[derive(Clone, Constructor)]
24+
#[derive(Clone)]
2525
pub struct Web3Signer {
2626
client: Client,
2727
config: Config,
2828
metrics: Option<Arc<Metrics>>,
29+
keys_loaded: HashSet<Url>,
2930
}
3031

3132
impl Web3Signer {
33+
#[must_use]
34+
pub fn new(client: Client, config: Config, metrics: Option<Arc<Metrics>>) -> Self {
35+
Self {
36+
client,
37+
config,
38+
metrics,
39+
keys_loaded: HashSet::new(),
40+
}
41+
}
42+
3243
#[must_use]
3344
pub const fn client(&self) -> &Client {
3445
&self.client
3546
}
3647

37-
pub async fn load_public_keys(&self) -> HashMap<&Url, HashSet<PublicKeyBytes>> {
48+
pub async fn load_public_keys(&mut self) -> HashMap<&Url, HashSet<PublicKeyBytes>> {
3849
let _timer = self
3950
.metrics
4051
.as_ref()
@@ -43,13 +54,18 @@ impl Web3Signer {
4354
let mut keys = HashMap::new();
4455

4556
for url in &self.config.urls {
57+
if !self.config.allow_to_reload_keys && self.keys_loaded.contains(url) {
58+
continue;
59+
}
60+
4661
match self.load_public_keys_from_url(url).await {
4762
Ok(mut remote_keys) => {
4863
if !self.config.public_keys.is_empty() {
4964
remote_keys.retain(|pubkey| self.config.public_keys.contains(pubkey));
5065
}
5166

5267
keys.insert(url, remote_keys);
68+
self.keys_loaded.insert(url.clone());
5369
}
5470
Err(error) => warn!("failed to load Web3Signer keys from {url}: {error:?}"),
5571
}
@@ -135,16 +151,53 @@ mod tests {
135151

136152
let url = Url::parse(&server.url("/"))?;
137153
let config = super::Config {
154+
allow_to_reload_keys: false,
138155
public_keys: HashSet::new(),
139156
urls: vec![url.clone()],
140157
};
141-
let web3signer = Web3Signer::new(Client::new(), config, None);
158+
let mut web3signer = Web3Signer::new(Client::new(), config, None);
142159

143160
let response = web3signer.load_public_keys().await;
144161
let expected = HashMap::from([(&url, HashSet::from([SAMPLE_PUBKEY, SAMPLE_PUBKEY_2]))]);
145162

146163
assert_eq!(response, expected);
147164

165+
let response = web3signer.load_public_keys().await;
166+
// By default, do not load pubkeys from Web3Signer again if keys were loaded
167+
let expected = HashMap::new();
168+
169+
assert_eq!(response, expected);
170+
171+
Ok(())
172+
}
173+
174+
#[tokio::test]
175+
async fn test_load_public_keys_if_reload_is_allowed() -> Result<()> {
176+
let server = MockServer::start();
177+
178+
server.mock(|when, then| {
179+
when.method(Method::GET).path("/api/v1/eth2/publicKeys");
180+
then.status(200)
181+
.body(json!([SAMPLE_PUBKEY, SAMPLE_PUBKEY_2]).to_string());
182+
});
183+
184+
let url = Url::parse(&server.url("/"))?;
185+
let config = super::Config {
186+
allow_to_reload_keys: true,
187+
public_keys: HashSet::new(),
188+
urls: vec![url.clone()],
189+
};
190+
let mut web3signer = Web3Signer::new(Client::new(), config, None);
191+
192+
let response = web3signer.load_public_keys().await;
193+
let expected = HashMap::from([(&url, HashSet::from([SAMPLE_PUBKEY, SAMPLE_PUBKEY_2]))]);
194+
195+
assert_eq!(response, expected);
196+
197+
let response = web3signer.load_public_keys().await;
198+
199+
assert_eq!(response, expected);
200+
148201
Ok(())
149202
}
150203

@@ -160,10 +213,11 @@ mod tests {
160213

161214
let url = Url::parse(&server.url("/"))?;
162215
let config = super::Config {
216+
allow_to_reload_keys: false,
163217
public_keys: vec![SAMPLE_PUBKEY_2].into_iter().collect(),
164218
urls: vec![url.clone()],
165219
};
166-
let web3signer = Web3Signer::new(Client::new(), config, None);
220+
let mut web3signer = Web3Signer::new(Client::new(), config, None);
167221

168222
let response = web3signer.load_public_keys().await;
169223
let expected = HashMap::from([(&url, HashSet::from([SAMPLE_PUBKEY_2]))]);
@@ -186,6 +240,7 @@ mod tests {
186240

187241
let url = Url::parse(&server.url("/"))?;
188242
let config = super::Config {
243+
allow_to_reload_keys: false,
189244
public_keys: HashSet::new(),
190245
urls: vec![url.clone()],
191246
};

validator/src/validator.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,7 @@ impl<P: Preset, W: Wait + Sync> Validator<P, W> {
770770
if misc::is_epoch_start::<P>(slot) {
771771
let current_epoch = misc::compute_epoch_at_slot::<P>(slot);
772772
self.spawn_slashing_protection_pruning(current_epoch);
773+
self.refresh_signer_keys();
773774
}
774775
}
775776
_ => {}
@@ -2947,6 +2948,18 @@ impl<P: Preset, W: Wait + Sync> Validator<P, W> {
29472948
misc::compute_start_slot_at_epoch::<P>(epoch)
29482949
}
29492950

2951+
fn refresh_signer_keys(&self) {
2952+
let signer_arc = self.signer.clone_arc();
2953+
2954+
tokio::spawn(async move {
2955+
signer_arc
2956+
.write()
2957+
.await
2958+
.refresh_keys_from_web3signer()
2959+
.await
2960+
});
2961+
}
2962+
29502963
fn get_execution_payload_header(
29512964
&self,
29522965
slot_head: &SlotHead<P>,

0 commit comments

Comments
 (0)