Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cache for selection proof signatures #3223

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion validator_client/src/http_api/keystores.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Implementation of the standard keystore management API.
use crate::{
initialized_validators::Error, signing_method::SigningMethod, InitializedValidators,
initialized_validators::Error, signing_handler::SigningMethod, InitializedValidators,
ValidatorStore,
};
use account_utils::ZeroizeString;
Expand Down
5 changes: 5 additions & 0 deletions validator_client/src/http_metrics/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ lazy_static::lazy_static! {
"Duration to obtain a signature",
&["type"]
);
pub static ref SIGNATURE_CACHE_HIT: Result<IntCounterVec> = try_create_int_counter_vec(
"vc_signature_cache_hits",
"Number of signature cache hits",
&["type"]
);
}

pub fn gather_prometheus_metrics<T: EthSpec>(
Expand Down
29 changes: 21 additions & 8 deletions validator_client/src/initialized_validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! The `InitializedValidators` struct in this file serves as the source-of-truth of which
//! validators are managed by this validator client.

use crate::signing_method::SigningMethod;
use crate::signing_handler::{SigningHandler, SigningMethod};
use account_utils::{
read_password, read_password_from_user,
validator_definitions::{
Expand Down Expand Up @@ -106,7 +106,7 @@ impl From<LockfileError> for Error {

/// A validator that is ready to sign messages.
pub struct InitializedValidator {
signing_method: Arc<SigningMethod>,
signing_handler: Arc<SigningHandler>,
graffiti: Option<Graffiti>,
suggested_fee_recipient: Option<Address>,
/// The validators index in `state.validators`, to be updated by an external service.
Expand All @@ -116,7 +116,7 @@ pub struct InitializedValidator {
impl InitializedValidator {
/// Return a reference to this validator's lockfile if it has one.
pub fn keystore_lockfile(&self) -> Option<MappedMutexGuard<Lockfile>> {
match self.signing_method.as_ref() {
match self.signing_handler.as_ref().method {
SigningMethod::LocalKeystore {
ref voting_keystore_lockfile,
..
Expand Down Expand Up @@ -288,7 +288,7 @@ impl InitializedValidator {
};

Ok(Self {
signing_method: Arc::new(signing_method),
signing_handler: Arc::new(SigningHandler::new(signing_method)),
graffiti: def.graffiti.map(Into::into),
suggested_fee_recipient: def.suggested_fee_recipient,
index: None,
Expand All @@ -297,7 +297,7 @@ impl InitializedValidator {

/// Returns the voting public key for this validator.
pub fn voting_public_key(&self) -> &PublicKey {
match self.signing_method.as_ref() {
match &self.signing_handler.as_ref().method {
SigningMethod::LocalKeystore { voting_keypair, .. } => &voting_keypair.pk,
SigningMethod::Web3Signer {
voting_public_key, ..
Expand Down Expand Up @@ -422,10 +422,23 @@ impl InitializedValidators {
///
/// - The validator is known to `self`.
/// - The validator is enabled.
pub fn signing_method(&self, voting_public_key: &PublicKeyBytes) -> Option<Arc<SigningMethod>> {
pub fn signing_method(&self, voting_public_key: &PublicKeyBytes) -> Option<&SigningMethod> {
self.validators
.get(voting_public_key)
.map(|v| v.signing_method.clone())
.map(|v| &v.signing_handler.method)
}

/// Returns the `SigningHandler` for a given voting `PublicKey`, if all are true:
///
/// - The validator is known to `self`.
/// - The validator is enabled.
pub fn signing_handler(
&self,
voting_public_key: &PublicKeyBytes,
) -> Option<Arc<SigningHandler>> {
self.validators
.get(voting_public_key)
.map(|v| v.signing_handler.clone())
}

/// Add a validator definition to `self`, replacing any disabled definition with the same
Expand Down Expand Up @@ -511,7 +524,7 @@ impl InitializedValidators {
ref voting_keystore_lockfile,
ref voting_keystore,
..
} = *initialized_validator.signing_method
} = initialized_validator.signing_handler.as_ref().method
{
// Drop the lock file so that it may be deleted. This is particularly important on
// Windows where the lockfile will fail to be deleted if it is still open.
Expand Down
2 changes: 1 addition & 1 deletion validator_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mod http_metrics;
mod key_cache;
mod notifier;
mod preparation_service;
mod signing_method;
mod signing_handler;
mod sync_committee_service;

mod doppelganger_service;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
use crate::http_metrics::metrics;
use eth2_keystore::Keystore;
use lockfile::Lockfile;
use parking_lot::Mutex;
use parking_lot::{Mutex, RwLock};
use reqwest::Client;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use task_executor::TaskExecutor;
Expand All @@ -19,6 +20,8 @@ pub use web3signer::Web3SignerObject;

mod web3signer;

const MAX_SIGNATURE_CACHE_SIZE: usize = 64;

#[derive(Debug, PartialEq)]
pub enum Error {
InconsistentDomains {
Expand All @@ -33,6 +36,7 @@ pub enum Error {
}

/// Enumerates all messages that can be signed by a validator.
#[derive(Debug)]
pub enum SignableMessage<'a, T: EthSpec, Payload: ExecPayload<T> = FullPayload<T>> {
RandaoReveal(Epoch),
BeaconBlock(&'a BeaconBlock<T, Payload>),
Expand Down Expand Up @@ -90,6 +94,31 @@ pub enum SigningMethod {
},
}

/// A cache for signatures previously used for different verifications and proofs.
/// Currently, caching is only used for Selection Proof message variants.
pub type SignatureCache = HashMap<Hash256, (Signature, Slot)>;

/// Handler type to manage signing methods and caches on a per-validator basis.
pub struct SigningHandler {
pub method: SigningMethod,
pub selection_proof_signature_cache: RwLock<SignatureCache>,
pub sync_selection_proof_signature_cache: RwLock<SignatureCache>,
}

impl SigningHandler {
pub fn new(method: SigningMethod) -> Self {
Self {
method,
selection_proof_signature_cache: RwLock::new(HashMap::with_capacity(
MAX_SIGNATURE_CACHE_SIZE,
)),
sync_selection_proof_signature_cache: RwLock::new(HashMap::with_capacity(
MAX_SIGNATURE_CACHE_SIZE,
)),
}
}
}

/// The additional information used to construct a signature. Mostly used for protection from replay
/// attacks.
pub struct SigningContext {
Expand All @@ -111,7 +140,62 @@ impl SigningContext {
}
}

impl SigningMethod {
impl SigningHandler {
fn get_from_cache<T: EthSpec, Payload: ExecPayload<T>>(
&self,
signing_root: &Hash256,
message_type: &SignableMessage<'_, T, Payload>,
) -> Option<Signature> {
match message_type {
SignableMessage::SelectionProof(_) => self
.selection_proof_signature_cache
.read()
.get(signing_root)
.map(|val| val.0.clone()),
SignableMessage::SyncSelectionProof(_) => self
.sync_selection_proof_signature_cache
.read()
.get(signing_root)
.map(|val| val.0.clone()),
_ => None,
}
}

fn store_in_cache<T: EthSpec, Payload: ExecPayload<T>>(
&self,
signing_root: Hash256,
signature: Signature,
message: &SignableMessage<'_, T, Payload>,
) {
match message {
SignableMessage::SelectionProof(slot) => {
let mut cache = self.selection_proof_signature_cache.write();
if cache.len() >= MAX_SIGNATURE_CACHE_SIZE {
// Find the entry with the oldest slot and prune it.
let min_slot = cache.iter().min_by_key(|c| c.1 .1);
macladson marked this conversation as resolved.
Show resolved Hide resolved
if let Some(item) = min_slot {
let min_key = *item.0;
cache.remove(&min_key);
}
}
cache.insert(signing_root, (signature, *slot));
}
SignableMessage::SyncSelectionProof(data) => {
let mut cache = self.sync_selection_proof_signature_cache.write();
if cache.len() >= MAX_SIGNATURE_CACHE_SIZE {
// Find the entry with the oldest slot and prune it.
let min_slot = cache.iter().min_by_key(|c| c.1 .1);
if let Some(item) = min_slot {
let min_key = *item.0;
cache.remove(&min_key);
}
}
cache.insert(signing_root, (signature, data.slot));
}
_ => (),
}
}

/// Return the signature of `signable_message`, with respect to the `signing_context`.
pub async fn get_signature<T: EthSpec, Payload: ExecPayload<T>>(
&self,
Expand All @@ -129,9 +213,31 @@ impl SigningMethod {

let signing_root = signable_message.signing_root(domain_hash);

match self {
// Use cached signature if it exists.
let cached = match signable_message {
SignableMessage::SelectionProof(_) => {
self.get_from_cache(&signing_root, &signable_message)
}
SignableMessage::SyncSelectionProof(_) => {
self.get_from_cache(&signing_root, &signable_message)
}
_ => None,
};

match &self.method {
SigningMethod::LocalKeystore { voting_keypair, .. } => {
let _timer =
match cached {
Some(signature) => {
metrics::inc_counter_vec(
&metrics::SIGNATURE_CACHE_HIT,
&[metrics::LOCAL_KEYSTORE],
);
return Ok(signature);
}
None => (),
};

let timer =
metrics::start_timer_vec(&metrics::SIGNING_TIMES, &[metrics::LOCAL_KEYSTORE]);

let voting_keypair = voting_keypair.clone();
Expand All @@ -145,14 +251,38 @@ impl SigningMethod {
.ok_or(Error::ShuttingDown)?
.await
.map_err(|e| Error::TokioJoin(e.to_string()))?;
drop(timer);

match signable_message {
// Store signatures for selection proof messages in the signature cache.
SignableMessage::SelectionProof(_) => {
self.store_in_cache(signing_root, signature.clone(), &signable_message)
}
SignableMessage::SyncSelectionProof(_) => {
self.store_in_cache(signing_root, signature.clone(), &signable_message)
}
_ => (),
};

Ok(signature)
}
SigningMethod::Web3Signer {
signing_url,
http_client,
..
} => {
let _timer =
match cached {
Some(signature) => {
metrics::inc_counter_vec(
&metrics::SIGNATURE_CACHE_HIT,
&[metrics::WEB3SIGNER],
);
return Ok(signature);
}
None => (),
};

let timer =
metrics::start_timer_vec(&metrics::SIGNING_TIMES, &[metrics::WEB3SIGNER]);

// Map the message into a Web3Signer type.
Expand Down Expand Up @@ -216,6 +346,22 @@ impl SigningMethod {
.json()
.await
.map_err(|e| Error::Web3SignerJsonParsingFailed(e.to_string()))?;
drop(timer);

match signable_message {
// Store signature in the signature cache.
SignableMessage::SelectionProof(_) => self.store_in_cache(
signing_root,
response.signature.clone(),
&signable_message,
),
SignableMessage::SyncSelectionProof(_) => self.store_in_cache(
signing_root,
response.signature.clone(),
&signable_message,
),
_ => (),
};

Ok(response.signature)
}
Expand Down
Loading