mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-21 22:04:44 +00:00
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:
@@ -135,12 +135,15 @@ impl MultiTestCase {
|
||||
}
|
||||
|
||||
for (i, att) in test_case.attestations.iter().enumerate() {
|
||||
match slashing_db.check_and_insert_attestation_signing_root(
|
||||
&att.pubkey,
|
||||
att.source_epoch,
|
||||
att.target_epoch,
|
||||
SigningRoot::from(att.signing_root),
|
||||
) {
|
||||
match slashing_db.with_transaction(|txn| {
|
||||
slashing_db.check_and_insert_attestation_signing_root(
|
||||
&att.pubkey,
|
||||
att.source_epoch,
|
||||
att.target_epoch,
|
||||
SigningRoot::from(att.signing_root),
|
||||
txn,
|
||||
)
|
||||
}) {
|
||||
Ok(safe) if !att.should_succeed => {
|
||||
panic!(
|
||||
"attestation {} from `{}` succeeded when it should have failed: {:?}",
|
||||
|
||||
@@ -16,8 +16,8 @@ pub mod interchange {
|
||||
pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation};
|
||||
pub use crate::signed_block::{InvalidBlock, SignedBlock};
|
||||
pub use crate::slashing_database::{
|
||||
InterchangeError, InterchangeImportOutcome, SUPPORTED_INTERCHANGE_FORMAT_VERSION,
|
||||
SlashingDatabase,
|
||||
CheckSlashability, InterchangeError, InterchangeImportOutcome,
|
||||
SUPPORTED_INTERCHANGE_FORMAT_VERSION, SlashingDatabase,
|
||||
};
|
||||
use bls::PublicKeyBytes;
|
||||
use rusqlite::Error as SQLError;
|
||||
|
||||
@@ -44,11 +44,14 @@ fn attestation_same_target() {
|
||||
let results = (0..num_attestations)
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
slashing_db.check_and_insert_attestation(
|
||||
&pk,
|
||||
&attestation_data_builder(i, num_attestations),
|
||||
DEFAULT_DOMAIN,
|
||||
)
|
||||
slashing_db.with_transaction(|txn| {
|
||||
slashing_db.check_and_insert_attestation(
|
||||
&pk,
|
||||
&attestation_data_builder(i, num_attestations),
|
||||
DEFAULT_DOMAIN,
|
||||
txn,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -73,7 +76,9 @@ fn attestation_surround_fest() {
|
||||
.into_par_iter()
|
||||
.map(|i| {
|
||||
let att = attestation_data_builder(i, 2 * num_attestations - i);
|
||||
slashing_db.check_and_insert_attestation(&pk, &att, DEFAULT_DOMAIN)
|
||||
slashing_db.with_transaction(|txn| {
|
||||
slashing_db.check_and_insert_attestation(&pk, &att, DEFAULT_DOMAIN, txn)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -38,6 +38,17 @@ pub struct SlashingDatabase {
|
||||
conn_pool: Pool,
|
||||
}
|
||||
|
||||
/// Whether to check slashability of a message.
|
||||
///
|
||||
/// The `No` variant MUST only be used if there is another source of slashing protection configured,
|
||||
/// e.g. web3signer's slashing protection.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum CheckSlashability {
|
||||
#[default]
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
impl SlashingDatabase {
|
||||
/// Open an existing database at the given `path`, or create one if none exists.
|
||||
pub fn open_or_create(path: &Path) -> Result<Self, NotSafe> {
|
||||
@@ -183,7 +194,9 @@ impl SlashingDatabase {
|
||||
U: From<NotSafe>,
|
||||
{
|
||||
let mut conn = self.conn_pool.get().map_err(NotSafe::from)?;
|
||||
let txn = conn.transaction().map_err(NotSafe::from)?;
|
||||
let txn = conn
|
||||
.transaction_with_behavior(TransactionBehavior::Exclusive)
|
||||
.map_err(NotSafe::from)?;
|
||||
let value = f(&txn)?;
|
||||
txn.commit().map_err(NotSafe::from)?;
|
||||
Ok(value)
|
||||
@@ -635,6 +648,43 @@ impl SlashingDatabase {
|
||||
self.check_block_proposal(&txn, validator_pubkey, slot, signing_root)
|
||||
}
|
||||
|
||||
#[instrument(name = "db_check_and_insert_attestations", level = "debug", skip_all)]
|
||||
pub fn check_and_insert_attestations<'a>(
|
||||
&self,
|
||||
attestations: &'a [(
|
||||
&'a AttestationData,
|
||||
&'a PublicKeyBytes,
|
||||
Hash256,
|
||||
CheckSlashability,
|
||||
)],
|
||||
) -> Result<Vec<Result<Safe, NotSafe>>, NotSafe> {
|
||||
let mut conn = self.conn_pool.get()?;
|
||||
let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
||||
|
||||
let mut results = Vec::with_capacity(attestations.len());
|
||||
for (attestation, validator_pubkey, domain, check_slashability) in attestations {
|
||||
match check_slashability {
|
||||
CheckSlashability::No => {
|
||||
results.push(Ok(Safe::Valid));
|
||||
}
|
||||
CheckSlashability::Yes => {
|
||||
let attestation_signing_root = attestation.signing_root(*domain).into();
|
||||
results.push(self.check_and_insert_attestation_signing_root(
|
||||
validator_pubkey,
|
||||
attestation.source.epoch,
|
||||
attestation.target.epoch,
|
||||
attestation_signing_root,
|
||||
&txn,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Check an attestation for slash safety, and if it is safe, record it in the database.
|
||||
///
|
||||
/// The checking and inserting happen atomically and exclusively. We enforce exclusivity
|
||||
@@ -647,6 +697,7 @@ impl SlashingDatabase {
|
||||
validator_pubkey: &PublicKeyBytes,
|
||||
attestation: &AttestationData,
|
||||
domain: Hash256,
|
||||
txn: &Transaction,
|
||||
) -> Result<Safe, NotSafe> {
|
||||
let attestation_signing_root = attestation.signing_root(domain).into();
|
||||
self.check_and_insert_attestation_signing_root(
|
||||
@@ -654,6 +705,7 @@ impl SlashingDatabase {
|
||||
attestation.source.epoch,
|
||||
attestation.target.epoch,
|
||||
attestation_signing_root,
|
||||
txn,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -664,17 +716,15 @@ impl SlashingDatabase {
|
||||
att_source_epoch: Epoch,
|
||||
att_target_epoch: Epoch,
|
||||
att_signing_root: SigningRoot,
|
||||
txn: &Transaction,
|
||||
) -> Result<Safe, NotSafe> {
|
||||
let mut conn = self.conn_pool.get()?;
|
||||
let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
||||
let safe = self.check_and_insert_attestation_signing_root_txn(
|
||||
validator_pubkey,
|
||||
att_source_epoch,
|
||||
att_target_epoch,
|
||||
att_signing_root,
|
||||
&txn,
|
||||
txn,
|
||||
)?;
|
||||
txn.commit()?;
|
||||
Ok(safe)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::slashing_database::CheckSlashability;
|
||||
use crate::*;
|
||||
use tempfile::{TempDir, tempdir};
|
||||
use types::{AttestationData, BeaconBlockHeader, test_utils::generate_deterministic_keypair};
|
||||
@@ -72,6 +73,12 @@ impl<T> Default for StreamTest<T> {
|
||||
|
||||
impl StreamTest<AttestationData> {
|
||||
pub fn run(&self) {
|
||||
self.run_solo();
|
||||
self.run_batched();
|
||||
}
|
||||
|
||||
// Run the test with every attestation processed individually.
|
||||
pub fn run_solo(&self) {
|
||||
let dir = tempdir().unwrap();
|
||||
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
|
||||
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
|
||||
@@ -84,7 +91,12 @@ impl StreamTest<AttestationData> {
|
||||
|
||||
for (i, test) in self.cases.iter().enumerate() {
|
||||
assert_eq!(
|
||||
slashing_db.check_and_insert_attestation(&test.pubkey, &test.data, test.domain),
|
||||
slashing_db.with_transaction(|txn| slashing_db.check_and_insert_attestation(
|
||||
&test.pubkey,
|
||||
&test.data,
|
||||
test.domain,
|
||||
txn
|
||||
)),
|
||||
test.expected,
|
||||
"attestation {} not processed as expected",
|
||||
i
|
||||
@@ -93,6 +105,48 @@ impl StreamTest<AttestationData> {
|
||||
|
||||
roundtrip_database(&dir, &slashing_db, self.registered_validators.is_empty());
|
||||
}
|
||||
|
||||
// Run the test with all attestations processed by the slashing DB as part of a batch.
|
||||
pub fn run_batched(&self) {
|
||||
let dir = tempdir().unwrap();
|
||||
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
|
||||
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
|
||||
|
||||
for pubkey in &self.registered_validators {
|
||||
slashing_db.register_validator(*pubkey).unwrap();
|
||||
}
|
||||
|
||||
check_registration_invariants(&slashing_db, &self.registered_validators);
|
||||
|
||||
let attestations_to_check = self
|
||||
.cases
|
||||
.iter()
|
||||
.map(|test| {
|
||||
(
|
||||
&test.data,
|
||||
&test.pubkey,
|
||||
test.domain,
|
||||
CheckSlashability::Yes,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let results = slashing_db
|
||||
.check_and_insert_attestations(&attestations_to_check)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(results.len(), self.cases.len());
|
||||
|
||||
for ((i, test), result) in self.cases.iter().enumerate().zip(results) {
|
||||
assert_eq!(
|
||||
result, test.expected,
|
||||
"attestation {} not processed as expected",
|
||||
i
|
||||
);
|
||||
}
|
||||
|
||||
roundtrip_database(&dir, &slashing_db, self.registered_validators.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTest<BeaconBlockHeader> {
|
||||
|
||||
Reference in New Issue
Block a user