Add eip_3076 crate (#8206)

#7894


  Moves the `Interchange` format from `slashing_protection` and thus removes the dependency on `slashing_protection` from `eth2` which can now just depend on the slimmer `eip_3076` crate.


Co-Authored-By: Mac L <mjladson@pm.me>
This commit is contained in:
Mac L
2025-10-16 20:10:42 +04:00
committed by GitHub
parent d1e06dc40d
commit f13d0615fd
12 changed files with 155 additions and 18 deletions

View File

@@ -6,11 +6,12 @@ edition = { workspace = true }
autotests = false
[features]
arbitrary-fuzz = ["types/arbitrary-fuzz"]
arbitrary-fuzz = ["types/arbitrary-fuzz", "eip_3076/arbitrary-fuzz"]
portable = ["types/portable"]
[dependencies]
arbitrary = { workspace = true, features = ["derive"] }
eip_3076 = { workspace = true, features = ["json"] }
ethereum_serde_utils = { workspace = true }
filesystem = { workspace = true }
r2d2 = { workspace = true }

View File

@@ -1,7 +1,5 @@
use eip_3076::{Interchange, InterchangeData, InterchangeMetadata, SignedAttestation, SignedBlock};
use slashing_protection::SUPPORTED_INTERCHANGE_FORMAT_VERSION;
use slashing_protection::interchange::{
Interchange, InterchangeData, InterchangeMetadata, SignedAttestation, SignedBlock,
};
use slashing_protection::interchange_test::{MultiTestCase, TestCase};
use slashing_protection::test_utils::{DEFAULT_GENESIS_VALIDATORS_ROOT, pubkey};
use std::fs::{self, File};

View File

@@ -1,159 +0,0 @@
use crate::InterchangeError;
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::{HashMap, HashSet};
use std::io;
use types::{Epoch, Hash256, PublicKeyBytes, Slot};
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct InterchangeMetadata {
#[serde(with = "serde_utils::quoted_u64::require_quotes")]
pub interchange_format_version: u64,
pub genesis_validators_root: Hash256,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct InterchangeData {
pub pubkey: PublicKeyBytes,
pub signed_blocks: Vec<SignedBlock>,
pub signed_attestations: Vec<SignedAttestation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct SignedBlock {
#[serde(with = "serde_utils::quoted_u64::require_quotes")]
pub slot: Slot,
#[serde(skip_serializing_if = "Option::is_none")]
pub signing_root: Option<Hash256>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct SignedAttestation {
#[serde(with = "serde_utils::quoted_u64::require_quotes")]
pub source_epoch: Epoch,
#[serde(with = "serde_utils::quoted_u64::require_quotes")]
pub target_epoch: Epoch,
#[serde(skip_serializing_if = "Option::is_none")]
pub signing_root: Option<Hash256>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
pub struct Interchange {
pub metadata: InterchangeMetadata,
pub data: Vec<InterchangeData>,
}
impl Interchange {
pub fn from_json_str(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
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> {
serde_json::to_writer(writer, self)
}
/// Do these two `Interchange`s contain the same data (ignoring ordering)?
pub fn equiv(&self, other: &Self) -> bool {
let self_set = self.data.iter().collect::<HashSet<_>>();
let other_set = other.data.iter().collect::<HashSet<_>>();
self.metadata == other.metadata && self_set == other_set
}
/// The number of entries in `data`.
pub fn len(&self) -> usize {
self.data.len()
}
/// Is the `data` part of the interchange completely empty?
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::MaxInconsistent),
};
// 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,
})
}
}

View File

@@ -1,8 +1,8 @@
use crate::{
SigningRoot, SlashingDatabase,
interchange::{Interchange, SignedAttestation, SignedBlock},
test_utils::{DEFAULT_GENESIS_VALIDATORS_ROOT, pubkey},
};
use eip_3076::{Interchange, SignedAttestation, SignedBlock};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tempfile::tempdir;

View File

@@ -1,7 +1,6 @@
mod attestation_tests;
mod block_tests;
mod extra_interchange_tests;
pub mod interchange;
pub mod interchange_test;
mod parallel_tests;
mod registration_tests;
@@ -10,6 +9,10 @@ mod signed_block;
mod slashing_database;
pub mod test_utils;
pub mod interchange {
pub use eip_3076::{Interchange, InterchangeMetadata};
}
pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation};
pub use crate::signed_block::{InvalidBlock, SignedBlock};
pub use crate::slashing_database::{

View File

@@ -1,10 +1,10 @@
use crate::interchange::{
Interchange, InterchangeData, InterchangeMetadata, SignedAttestation as InterchangeAttestation,
SignedBlock as InterchangeBlock,
};
use crate::signed_attestation::InvalidAttestation;
use crate::signed_block::InvalidBlock;
use crate::{NotSafe, Safe, SignedAttestation, SignedBlock, SigningRoot, signing_root_from_row};
use eip_3076::{
Interchange, InterchangeData, InterchangeMetadata, SignedAttestation as InterchangeAttestation,
SignedBlock as InterchangeBlock,
};
use filesystem::restrict_file_permissions;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params};
@@ -1219,7 +1219,7 @@ pub enum InterchangeError {
interchange_file: Hash256,
client: Hash256,
},
MaxInconsistent,
Eip3076(eip_3076::Error),
SummaryInconsistent,
SQLError(String),
SQLPoolError(r2d2::Error),