mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-15 02:42:38 +00:00
Minify slashing protection interchange data (#2380)
## Issue Addressed Closes #2354 ## Proposed Changes Add a `minify` method to `slashing_protection::Interchange` that keeps only the maximum-epoch attestation and maximum-slot block for each validator. Specifically, `minify` constructs "synthetic" attestations (with no `signing_root`) containing the maximum source epoch _and_ the maximum target epoch from the input. This is equivalent to the `minify_synth` algorithm that I've formally verified in this repository: https://github.com/michaelsproul/slashing-proofs ## Additional Info Includes the JSON loading optimisation from #2347
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
use crate::{
|
||||
interchange::Interchange,
|
||||
interchange::{Interchange, SignedAttestation, SignedBlock},
|
||||
test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT},
|
||||
SigningRoot, SlashingDatabase,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use tempfile::tempdir;
|
||||
use types::{Epoch, Hash256, PublicKeyBytes, Slot};
|
||||
|
||||
@@ -17,7 +18,7 @@ pub struct MultiTestCase {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TestCase {
|
||||
pub should_succeed: bool,
|
||||
pub allow_partial_import: bool,
|
||||
pub contains_slashable_data: bool,
|
||||
pub interchange: Interchange,
|
||||
pub blocks: Vec<TestBlock>,
|
||||
pub attestations: Vec<TestAttestation>,
|
||||
@@ -58,41 +59,53 @@ impl MultiTestCase {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn run(&self) {
|
||||
pub fn run(&self, minify: bool) {
|
||||
let dir = tempdir().unwrap();
|
||||
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
|
||||
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
|
||||
|
||||
// If minification is used, false positives are allowed, i.e. there may be some situations
|
||||
// in which signing is safe but the minified file prevents it.
|
||||
let allow_false_positives = minify;
|
||||
|
||||
for test_case in &self.steps {
|
||||
match slashing_db.import_interchange_info(
|
||||
test_case.interchange.clone(),
|
||||
self.genesis_validators_root,
|
||||
) {
|
||||
// If the test case is marked as containing slashable data, then it is permissible
|
||||
// that we fail to import the file, in which case execution of the whole test should
|
||||
// be aborted.
|
||||
let allow_import_failure = test_case.contains_slashable_data;
|
||||
|
||||
let interchange = if minify {
|
||||
let minified = test_case.interchange.minify().unwrap();
|
||||
check_minification_invariants(&test_case.interchange, &minified);
|
||||
minified
|
||||
} else {
|
||||
test_case.interchange.clone()
|
||||
};
|
||||
|
||||
match slashing_db.import_interchange_info(interchange, self.genesis_validators_root) {
|
||||
Ok(import_outcomes) => {
|
||||
let failed_records = import_outcomes
|
||||
.iter()
|
||||
.filter(|o| o.failed())
|
||||
.collect::<Vec<_>>();
|
||||
let none_failed = import_outcomes.iter().all(|o| !o.failed());
|
||||
assert!(
|
||||
none_failed,
|
||||
"test `{}` failed to import some records: {:#?}",
|
||||
self.name, import_outcomes
|
||||
);
|
||||
if !test_case.should_succeed {
|
||||
panic!(
|
||||
"test `{}` succeeded on import when it should have failed",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
if !failed_records.is_empty() && !test_case.allow_partial_import {
|
||||
}
|
||||
Err(e) => {
|
||||
if test_case.should_succeed && !allow_import_failure {
|
||||
panic!(
|
||||
"test `{}` failed to import some records but should have succeeded: {:#?}",
|
||||
self.name, failed_records,
|
||||
"test `{}` failed on import when it should have succeeded, error: {:?}",
|
||||
self.name, e
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) if test_case.should_succeed => {
|
||||
panic!(
|
||||
"test `{}` failed on import when it should have succeeded, error: {:?}",
|
||||
self.name, e
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
for (i, block) in test_case.blocks.iter().enumerate() {
|
||||
@@ -107,7 +120,7 @@ impl MultiTestCase {
|
||||
i, self.name, safe
|
||||
);
|
||||
}
|
||||
Err(e) if block.should_succeed => {
|
||||
Err(e) if block.should_succeed && !allow_false_positives => {
|
||||
panic!(
|
||||
"block {} from `{}` failed when it should have succeeded: {:?}",
|
||||
i, self.name, e
|
||||
@@ -130,7 +143,7 @@ impl MultiTestCase {
|
||||
i, self.name, safe
|
||||
);
|
||||
}
|
||||
Err(e) if att.should_succeed => {
|
||||
Err(e) if att.should_succeed && !allow_false_positives => {
|
||||
panic!(
|
||||
"attestation {} from `{}` failed when it should have succeeded: {:?}",
|
||||
i, self.name, e
|
||||
@@ -147,7 +160,7 @@ impl TestCase {
|
||||
pub fn new(interchange: Interchange) -> Self {
|
||||
TestCase {
|
||||
should_succeed: true,
|
||||
allow_partial_import: false,
|
||||
contains_slashable_data: false,
|
||||
interchange,
|
||||
blocks: vec![],
|
||||
attestations: vec![],
|
||||
@@ -159,8 +172,8 @@ impl TestCase {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn allow_partial_import(mut self) -> Self {
|
||||
self.allow_partial_import = true;
|
||||
pub fn contains_slashable_data(mut self) -> Self {
|
||||
self.contains_slashable_data = true;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -216,3 +229,81 @@ impl TestCase {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn check_minification_invariants(interchange: &Interchange, minified: &Interchange) {
|
||||
// Metadata should be unchanged.
|
||||
assert_eq!(interchange.metadata, minified.metadata);
|
||||
|
||||
// Minified data should contain one entry per *unique* public key.
|
||||
let uniq_pubkeys = get_uniq_pubkeys(interchange);
|
||||
assert_eq!(uniq_pubkeys, get_uniq_pubkeys(minified));
|
||||
assert_eq!(uniq_pubkeys.len(), minified.data.len());
|
||||
|
||||
for &pubkey in uniq_pubkeys.iter() {
|
||||
// Minified data should contain 1 block per validator, unless the validator never signed any
|
||||
// blocks. All of those blocks should have slots <= the slot of the minified block.
|
||||
let original_blocks = get_blocks_of_validator(interchange, pubkey);
|
||||
let minified_blocks = get_blocks_of_validator(minified, pubkey);
|
||||
|
||||
if original_blocks.is_empty() {
|
||||
assert!(minified_blocks.is_empty());
|
||||
} else {
|
||||
// Should have exactly 1 block.
|
||||
assert_eq!(minified_blocks.len(), 1);
|
||||
|
||||
// That block should have no signing root (it's synthetic).
|
||||
let mini_block = minified_blocks.first().unwrap();
|
||||
assert_eq!(mini_block.signing_root, None);
|
||||
|
||||
// All original blocks should have slots <= the mini block.
|
||||
assert!(original_blocks
|
||||
.iter()
|
||||
.all(|block| block.slot <= mini_block.slot));
|
||||
}
|
||||
|
||||
// Minified data should contain 1 attestation per validator, unless the validator never
|
||||
// signed any attestations. All attestations should have source and target <= the source
|
||||
// and target of the minified attestation.
|
||||
let original_attestations = get_attestations_of_validator(interchange, pubkey);
|
||||
let minified_attestations = get_attestations_of_validator(minified, pubkey);
|
||||
|
||||
if original_attestations.is_empty() {
|
||||
assert!(minified_attestations.is_empty());
|
||||
} else {
|
||||
assert_eq!(minified_attestations.len(), 1);
|
||||
|
||||
let mini_attestation = minified_attestations.first().unwrap();
|
||||
assert_eq!(mini_attestation.signing_root, None);
|
||||
|
||||
assert!(original_attestations
|
||||
.iter()
|
||||
.all(|att| att.source_epoch <= mini_attestation.source_epoch
|
||||
&& att.target_epoch <= mini_attestation.target_epoch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_uniq_pubkeys(interchange: &Interchange) -> HashSet<PublicKeyBytes> {
|
||||
interchange.data.iter().map(|data| data.pubkey).collect()
|
||||
}
|
||||
|
||||
fn get_blocks_of_validator(interchange: &Interchange, pubkey: PublicKeyBytes) -> Vec<&SignedBlock> {
|
||||
interchange
|
||||
.data
|
||||
.iter()
|
||||
.filter(|data| data.pubkey == pubkey)
|
||||
.flat_map(|data| data.signed_blocks.iter())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_attestations_of_validator(
|
||||
interchange: &Interchange,
|
||||
pubkey: PublicKeyBytes,
|
||||
) -> Vec<&SignedAttestation> {
|
||||
interchange
|
||||
.data
|
||||
.iter()
|
||||
.filter(|data| data.pubkey == pubkey)
|
||||
.flat_map(|data| data.signed_attestations.iter())
|
||||
.collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user