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,5 +1,8 @@
use crate::InterchangeError;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashSet;
use std::cmp::max;
use std::collections::{HashMap, HashSet};
use std::io;
use types::{Epoch, Hash256, PublicKeyBytes, Slot};
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
@@ -49,8 +52,12 @@ impl Interchange {
serde_json::from_str(json)
}
pub fn from_json_reader(reader: impl std::io::Read) -> Result<Self, serde_json::Error> {
serde_json::from_reader(reader)
pub fn from_json_reader(mut reader: impl std::io::Read) -> Result<Self, io::Error> {
// We read the entire file into memory first, as this is *a lot* faster than using
// `serde_json::from_reader`. See https://github.com/serde-rs/json/issues/160
let mut json_str = String::new();
reader.read_to_string(&mut json_str)?;
Ok(Interchange::from_json_str(&json_str)?)
}
pub fn write_to(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> {
@@ -73,4 +80,75 @@ impl Interchange {
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Minify an interchange by constructing a synthetic block & attestation for each validator.
pub fn minify(&self) -> Result<Self, InterchangeError> {
// Map from pubkey to optional max block and max attestation.
let mut validator_data =
HashMap::<PublicKeyBytes, (Option<SignedBlock>, Option<SignedAttestation>)>::new();
for data in self.data.iter() {
// Existing maximum attestation and maximum block.
let (max_block, max_attestation) = validator_data
.entry(data.pubkey)
.or_insert_with(|| (None, None));
// Find maximum source and target epochs.
let max_source_epoch = data
.signed_attestations
.iter()
.map(|attestation| attestation.source_epoch)
.max();
let max_target_epoch = data
.signed_attestations
.iter()
.map(|attestation| attestation.target_epoch)
.max();
match (max_source_epoch, max_target_epoch) {
(Some(source_epoch), Some(target_epoch)) => {
if let Some(prev_max) = max_attestation {
prev_max.source_epoch = max(prev_max.source_epoch, source_epoch);
prev_max.target_epoch = max(prev_max.target_epoch, target_epoch);
} else {
*max_attestation = Some(SignedAttestation {
source_epoch,
target_epoch,
signing_root: None,
});
}
}
(None, None) => {}
_ => return Err(InterchangeError::MinAndMaxInconsistent),
};
// Find maximum block slot.
let max_block_slot = data.signed_blocks.iter().map(|block| block.slot).max();
if let Some(max_slot) = max_block_slot {
if let Some(prev_max) = max_block {
prev_max.slot = max(prev_max.slot, max_slot);
} else {
*max_block = Some(SignedBlock {
slot: max_slot,
signing_root: None,
});
}
}
}
let data = validator_data
.into_iter()
.map(|(pubkey, (maybe_block, maybe_att))| InterchangeData {
pubkey,
signed_blocks: maybe_block.into_iter().collect(),
signed_attestations: maybe_att.into_iter().collect(),
})
.collect();
Ok(Self {
metadata: self.metadata.clone(),
data,
})
}
}