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

View File

@@ -23,6 +23,7 @@ members = [
"common/compare_fields_derive", "common/compare_fields_derive",
"common/deposit_contract", "common/deposit_contract",
"common/directory", "common/directory",
"common/eip_3076",
"common/eth2", "common/eth2",
"common/eth2_config", "common/eth2_config",
"common/eth2_interop_keypairs", "common/eth2_interop_keypairs",
@@ -135,6 +136,7 @@ directory = { path = "common/directory" }
dirs = "3" dirs = "3"
discv5 = { version = "0.10", features = ["libp2p"] } discv5 = { version = "0.10", features = ["libp2p"] }
doppelganger_service = { path = "validator_client/doppelganger_service" } doppelganger_service = { path = "validator_client/doppelganger_service" }
eip_3076 = { path = "common/eip_3076" }
either = "1.9" either = "1.9"
environment = { path = "lighthouse/environment" } environment = { path = "lighthouse/environment" }
eth2 = { path = "common/eth2" } 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 serde::{Deserialize, Serialize};
use std::cmp::max; use std::cmp::max;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
#[cfg(feature = "json")]
use std::io; use std::io;
use types::{Epoch, Hash256, PublicKeyBytes, Slot}; use types::{Epoch, Hash256, PublicKeyBytes, Slot};
#[derive(Debug)]
pub enum Error {
MaxInconsistent,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))]
@@ -53,10 +58,12 @@ pub struct Interchange {
} }
impl Interchange { impl Interchange {
#[cfg(feature = "json")]
pub fn from_json_str(json: &str) -> Result<Self, serde_json::Error> { pub fn from_json_str(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json) serde_json::from_str(json)
} }
#[cfg(feature = "json")]
pub fn from_json_reader(mut reader: impl std::io::Read) -> Result<Self, io::Error> { 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 // 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 // `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)?) Ok(Interchange::from_json_str(&json_str)?)
} }
#[cfg(feature = "json")]
pub fn write_to(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> { pub fn write_to(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> {
serde_json::to_writer(writer, self) serde_json::to_writer(writer, self)
} }
@@ -87,7 +95,7 @@ impl Interchange {
} }
/// Minify an interchange by constructing a synthetic block & attestation for each validator. /// 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. // Map from pubkey to optional max block and max attestation.
let mut validator_data = let mut validator_data =
HashMap::<PublicKeyBytes, (Option<SignedBlock>, Option<SignedAttestation>)>::new(); HashMap::<PublicKeyBytes, (Option<SignedBlock>, Option<SignedAttestation>)>::new();
@@ -124,7 +132,7 @@ impl Interchange {
} }
} }
(None, None) => {} (None, None) => {}
_ => return Err(InterchangeError::MaxInconsistent), _ => return Err(Error::MaxInconsistent),
}; };
// Find maximum block slot. // 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] [dependencies]
derivative = { workspace = true } derivative = { workspace = true }
eip_3076 = { workspace = true }
either = { workspace = true } either = { workspace = true }
enr = { version = "0.13.0", features = ["ed25519"] } enr = { version = "0.13.0", features = ["ed25519"] }
eth2_keystore = { workspace = true } eth2_keystore = { workspace = true }
@@ -29,7 +30,6 @@ reqwest-eventsource = "0.5.0"
sensitive_url = { workspace = true } sensitive_url = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
slashing_protection = { workspace = true }
ssz_types = { workspace = true } ssz_types = { workspace = true }
test_random_derive = { path = "../../common/test_random_derive" } test_random_derive = { path = "../../common/test_random_derive" }
types = { workspace = true } types = { workspace = true }

View File

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

View File

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

View File

@@ -6,11 +6,12 @@ edition = { workspace = true }
autotests = false autotests = false
[features] [features]
arbitrary-fuzz = ["types/arbitrary-fuzz"] arbitrary-fuzz = ["types/arbitrary-fuzz", "eip_3076/arbitrary-fuzz"]
portable = ["types/portable"] portable = ["types/portable"]
[dependencies] [dependencies]
arbitrary = { workspace = true, features = ["derive"] } arbitrary = { workspace = true, features = ["derive"] }
eip_3076 = { workspace = true, features = ["json"] }
ethereum_serde_utils = { workspace = true } ethereum_serde_utils = { workspace = true }
filesystem = { workspace = true } filesystem = { workspace = true }
r2d2 = { 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::SUPPORTED_INTERCHANGE_FORMAT_VERSION;
use slashing_protection::interchange::{
Interchange, InterchangeData, InterchangeMetadata, SignedAttestation, SignedBlock,
};
use slashing_protection::interchange_test::{MultiTestCase, TestCase}; use slashing_protection::interchange_test::{MultiTestCase, TestCase};
use slashing_protection::test_utils::{DEFAULT_GENESIS_VALIDATORS_ROOT, pubkey}; use slashing_protection::test_utils::{DEFAULT_GENESIS_VALIDATORS_ROOT, pubkey};
use std::fs::{self, File}; use std::fs::{self, File};

View File

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

View File

@@ -1,7 +1,6 @@
mod attestation_tests; mod attestation_tests;
mod block_tests; mod block_tests;
mod extra_interchange_tests; mod extra_interchange_tests;
pub mod interchange;
pub mod interchange_test; pub mod interchange_test;
mod parallel_tests; mod parallel_tests;
mod registration_tests; mod registration_tests;
@@ -10,6 +9,10 @@ mod signed_block;
mod slashing_database; mod slashing_database;
pub mod test_utils; pub mod test_utils;
pub mod interchange {
pub use eip_3076::{Interchange, InterchangeMetadata};
}
pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation}; pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation};
pub use crate::signed_block::{InvalidBlock, SignedBlock}; pub use crate::signed_block::{InvalidBlock, SignedBlock};
pub use crate::slashing_database::{ 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_attestation::InvalidAttestation;
use crate::signed_block::InvalidBlock; use crate::signed_block::InvalidBlock;
use crate::{NotSafe, Safe, SignedAttestation, SignedBlock, SigningRoot, signing_root_from_row}; 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 filesystem::restrict_file_permissions;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params}; use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params};
@@ -1219,7 +1219,7 @@ pub enum InterchangeError {
interchange_file: Hash256, interchange_file: Hash256,
client: Hash256, client: Hash256,
}, },
MaxInconsistent, Eip3076(eip_3076::Error),
SummaryInconsistent, SummaryInconsistent,
SQLError(String), SQLError(String),
SQLPoolError(r2d2::Error), SQLPoolError(r2d2::Error),