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

15
Cargo.lock generated
View File

@@ -2581,6 +2581,18 @@ dependencies = [
"sha2 0.10.8",
]
[[package]]
name = "eip_3076"
version = "0.1.0"
dependencies = [
"arbitrary",
"ethereum_serde_utils",
"serde",
"serde_json",
"tempfile",
"types",
]
[[package]]
name = "either"
version = "1.15.0"
@@ -2848,6 +2860,7 @@ name = "eth2"
version = "0.1.0"
dependencies = [
"derivative",
"eip_3076",
"either",
"enr",
"eth2_keystore",
@@ -2867,7 +2880,6 @@ dependencies = [
"sensitive_url",
"serde",
"serde_json",
"slashing_protection",
"ssz_types",
"test_random_derive",
"tokio",
@@ -8832,6 +8844,7 @@ name = "slashing_protection"
version = "0.1.0"
dependencies = [
"arbitrary",
"eip_3076",
"ethereum_serde_utils",
"filesystem",
"r2d2",

View File

@@ -23,6 +23,7 @@ members = [
"common/compare_fields_derive",
"common/deposit_contract",
"common/directory",
"common/eip_3076",
"common/eth2",
"common/eth2_config",
"common/eth2_interop_keypairs",
@@ -135,6 +136,7 @@ directory = { path = "common/directory" }
dirs = "3"
discv5 = { version = "0.10", features = ["libp2p"] }
doppelganger_service = { path = "validator_client/doppelganger_service" }
eip_3076 = { path = "common/eip_3076" }
either = "1.9"
environment = { path = "lighthouse/environment" }
eth2 = { path = "common/eth2" }

View File

@@ -0,0 +1,20 @@
[package]
name = "eip_3076"
version = "0.1.0"
authors = ["Sigma Prime <contact@sigmaprime.io>"]
edition = { workspace = true }
[features]
default = []
arbitrary-fuzz = ["dep:arbitrary", "types/arbitrary"]
json = ["dep:serde_json"]
[dependencies]
arbitrary = { workspace = true, features = ["derive"], optional = true }
ethereum_serde_utils = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true, optional = true }
types = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@@ -1,10 +1,15 @@
use crate::InterchangeError;
use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::{HashMap, HashSet};
#[cfg(feature = "json")]
use std::io;
use types::{Epoch, Hash256, PublicKeyBytes, Slot};
#[derive(Debug)]
pub enum Error {
MaxInconsistent,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
@@ -53,10 +58,12 @@ pub struct Interchange {
}
impl Interchange {
#[cfg(feature = "json")]
pub fn from_json_str(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
#[cfg(feature = "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
@@ -65,6 +72,7 @@ impl Interchange {
Ok(Interchange::from_json_str(&json_str)?)
}
#[cfg(feature = "json")]
pub fn write_to(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> {
serde_json::to_writer(writer, self)
}
@@ -87,7 +95,7 @@ impl Interchange {
}
/// Minify an interchange by constructing a synthetic block & attestation for each validator.
pub fn minify(&self) -> Result<Self, InterchangeError> {
pub fn minify(&self) -> Result<Self, Error> {
// Map from pubkey to optional max block and max attestation.
let mut validator_data =
HashMap::<PublicKeyBytes, (Option<SignedBlock>, Option<SignedAttestation>)>::new();
@@ -124,7 +132,7 @@ impl Interchange {
}
}
(None, None) => {}
_ => return Err(InterchangeError::MaxInconsistent),
_ => return Err(Error::MaxInconsistent),
};
// Find maximum block slot.
@@ -157,3 +165,96 @@ impl Interchange {
})
}
}
#[cfg(feature = "json")]
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::tempdir;
use types::FixedBytesExtended;
fn get_interchange() -> Interchange {
Interchange {
metadata: InterchangeMetadata {
interchange_format_version: 5,
genesis_validators_root: Hash256::from_low_u64_be(555),
},
data: vec![
InterchangeData {
pubkey: PublicKeyBytes::deserialize(&[1u8; 48]).unwrap(),
signed_blocks: vec![SignedBlock {
slot: Slot::new(100),
signing_root: Some(Hash256::from_low_u64_be(1)),
}],
signed_attestations: vec![SignedAttestation {
source_epoch: Epoch::new(0),
target_epoch: Epoch::new(5),
signing_root: Some(Hash256::from_low_u64_be(2)),
}],
},
InterchangeData {
pubkey: PublicKeyBytes::deserialize(&[2u8; 48]).unwrap(),
signed_blocks: vec![],
signed_attestations: vec![],
},
],
}
}
#[test]
fn test_roundtrip() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("interchange.json");
let interchange = get_interchange();
let mut file = File::create(&file_path).unwrap();
interchange.write_to(&mut file).unwrap();
let file = File::open(&file_path).unwrap();
let from_file = Interchange::from_json_reader(file).unwrap();
assert_eq!(interchange, from_file);
}
#[test]
fn test_empty_roundtrip() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("empty.json");
let empty = Interchange {
metadata: InterchangeMetadata {
interchange_format_version: 5,
genesis_validators_root: Hash256::zero(),
},
data: vec![],
};
let mut file = File::create(&file_path).unwrap();
empty.write_to(&mut file).unwrap();
let file = File::open(&file_path).unwrap();
let from_file = Interchange::from_json_reader(file).unwrap();
assert_eq!(empty, from_file);
}
#[test]
fn test_minify_roundtrip() {
let interchange = get_interchange();
let minified = interchange.minify().unwrap();
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("minified.json");
let mut file = File::create(&file_path).unwrap();
minified.write_to(&mut file).unwrap();
let file = File::open(&file_path).unwrap();
let from_file = Interchange::from_json_reader(file).unwrap();
assert_eq!(minified, from_file);
}
}

View File

@@ -10,6 +10,7 @@ lighthouse = []
[dependencies]
derivative = { workspace = true }
eip_3076 = { workspace = true }
either = { workspace = true }
enr = { version = "0.13.0", features = ["ed25519"] }
eth2_keystore = { workspace = true }
@@ -29,7 +30,6 @@ reqwest-eventsource = "0.5.0"
sensitive_url = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
slashing_protection = { workspace = true }
ssz_types = { workspace = true }
test_random_derive = { path = "../../common/test_random_derive" }
types = { workspace = true }

View File

@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use types::{Address, Graffiti, PublicKeyBytes};
use zeroize::Zeroizing;
pub use slashing_protection::interchange::Interchange;
pub use eip_3076::Interchange;
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct GetFeeRecipientResponse {

View File

@@ -10,7 +10,6 @@ use test_random_derive::TestRandom;
use tree_hash_derive::TreeHash;
/// A Validators aggregate sync committee contribution and selection proof.
#[cfg_attr(
feature = "arbitrary",
derive(arbitrary::Arbitrary),

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,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),