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:
Michael Sproul
2021-06-21 05:46:36 +00:00
parent b84ff9f793
commit 6583ce325b
11 changed files with 441 additions and 95 deletions

View File

@@ -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()
}