Check slashability of attestations in batches to avoid sequential bottleneck (#8516)

Closes:

- https://github.com/sigp/lighthouse/issues/1914


  Sign attestations prior to checking them against the slashing protection DB. This allows us to avoid the sequential DB checks which are observed in traces here:

- https://github.com/sigp/lighthouse/pull/8508#discussion_r2576686107


Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>
This commit is contained in:
Michael Sproul
2026-01-27 18:56:09 +11:00
committed by GitHub
parent 1476c20cfc
commit 0f57fc9d8e
13 changed files with 563 additions and 267 deletions

View File

@@ -539,6 +539,58 @@ mod tests {
}
self
}
/// Assert that a slashable attestation fails to be signed locally (empty result) and is
/// either signed or not by the web3signer rig depending on the value of
/// `web3signer_should_sign`.
///
/// The batch attestation signing API returns an empty result instead of an error for
/// slashable attestations.
pub async fn assert_slashable_attestation_should_sign<F, R>(
self,
case_name: &str,
generate_sig: F,
web3signer_should_sign: bool,
) -> Self
where
F: Fn(PublicKeyBytes, Arc<LighthouseValidatorStore<TestingSlotClock, E>>) -> R,
R: Future<
Output = Result<Vec<(u64, Attestation<E>)>, lighthouse_validator_store::Error>,
>,
{
for validator_rig in &self.validator_rigs {
let result =
generate_sig(self.validator_pubkey, validator_rig.validator_store.clone())
.await;
if !validator_rig.using_web3signer || !web3signer_should_sign {
// For local validators, slashable attestations should return an empty result
// or an error.
match result {
Ok(attestations) => {
assert!(
attestations.is_empty(),
"should not sign slashable {case_name}: expected empty result"
);
}
Err(ValidatorStoreError::Slashable(_)) => {
// Also acceptable - error indicates slashable
}
Err(e) => {
panic!("unexpected error for slashable {case_name}: {e:?}");
}
}
} else {
// Web3signer should sign (has its own slashing protection)
let attestations = result.expect("should sign slashable {case_name}");
assert!(
!attestations.is_empty(),
"web3signer should sign slashable {case_name}"
);
}
}
self
}
}
/// Get a generic, arbitrary attestation for signing.
@@ -605,12 +657,14 @@ mod tests {
})
.await
.assert_signatures_match("attestation", |pubkey, validator_store| async move {
let mut attestation = get_attestation();
let attestation = get_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, Epoch::new(0))
.sign_attestations(vec![(0, pubkey, 0, attestation)])
.await
.unwrap();
attestation
.unwrap()
.pop()
.unwrap()
.1
})
.await
.assert_signatures_match("signed_aggregate", |pubkey, validator_store| async move {
@@ -820,8 +874,6 @@ mod tests {
block
};
let current_epoch = Epoch::new(5);
TestingRig::new(
network,
slashing_protection_config,
@@ -830,42 +882,44 @@ mod tests {
)
.await
.assert_signatures_match("first_attestation", |pubkey, validator_store| async move {
let mut attestation = first_attestation();
let attestation = first_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, current_epoch)
.sign_attestations(vec![(0, pubkey, 0, attestation)])
.await
.unwrap();
attestation
.unwrap()
.pop()
.unwrap()
.1
})
.await
.assert_slashable_message_should_sign(
.assert_slashable_attestation_should_sign(
"double_vote_attestation",
move |pubkey, validator_store| async move {
let mut attestation = double_vote_attestation();
let attestation = double_vote_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, current_epoch)
.sign_attestations(vec![(0, pubkey, 0, attestation)])
.await
},
slashable_message_should_sign,
)
.await
.assert_slashable_message_should_sign(
.assert_slashable_attestation_should_sign(
"surrounding_attestation",
move |pubkey, validator_store| async move {
let mut attestation = surrounding_attestation();
let attestation = surrounding_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, current_epoch)
.sign_attestations(vec![(0, pubkey, 0, attestation)])
.await
},
slashable_message_should_sign,
)
.await
.assert_slashable_message_should_sign(
.assert_slashable_attestation_should_sign(
"surrounded_attestation",
move |pubkey, validator_store| async move {
let mut attestation = surrounded_attestation();
let attestation = surrounded_attestation();
validator_store
.sign_attestation(pubkey, 0, &mut attestation, current_epoch)
.sign_attestations(vec![(0, pubkey, 0, attestation)])
.await
},
slashable_message_should_sign,