diff --git a/Cargo.lock b/Cargo.lock index 31cccc6a986..59e7bda170a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index a46dc355e72..ae84d645bb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/common/eip_3076/Cargo.toml b/common/eip_3076/Cargo.toml new file mode 100644 index 00000000000..851ef26238a --- /dev/null +++ b/common/eip_3076/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "eip_3076" +version = "0.1.0" +authors = ["Sigma Prime "] +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 } diff --git a/validator_client/slashing_protection/src/interchange.rs b/common/eip_3076/src/lib.rs similarity index 64% rename from validator_client/slashing_protection/src/interchange.rs rename to common/eip_3076/src/lib.rs index 95a39c50e48..2d47a77de40 100644 --- a/validator_client/slashing_protection/src/interchange.rs +++ b/common/eip_3076/src/lib.rs @@ -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 { serde_json::from_str(json) } + #[cfg(feature = "json")] pub fn from_json_reader(mut reader: impl std::io::Read) -> Result { // 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 { + pub fn minify(&self) -> Result { // Map from pubkey to optional max block and max attestation. let mut validator_data = HashMap::, Option)>::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); + } +} diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 81666a64216..46066a559f8 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -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 } diff --git a/common/eth2/src/lighthouse_vc/std_types.rs b/common/eth2/src/lighthouse_vc/std_types.rs index ae192312bdb..0290bdd0b79 100644 --- a/common/eth2/src/lighthouse_vc/std_types.rs +++ b/common/eth2/src/lighthouse_vc/std_types.rs @@ -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 { diff --git a/consensus/types/src/contribution_and_proof.rs b/consensus/types/src/contribution_and_proof.rs index 85c9ac15fb8..4d70cd1f8a0 100644 --- a/consensus/types/src/contribution_and_proof.rs +++ b/consensus/types/src/contribution_and_proof.rs @@ -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), diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 3860af514db..6a778c5de31 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -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 } diff --git a/validator_client/slashing_protection/src/bin/test_generator.rs b/validator_client/slashing_protection/src/bin/test_generator.rs index 4576231b7bd..dfda7983f73 100644 --- a/validator_client/slashing_protection/src/bin/test_generator.rs +++ b/validator_client/slashing_protection/src/bin/test_generator.rs @@ -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}; diff --git a/validator_client/slashing_protection/src/interchange_test.rs b/validator_client/slashing_protection/src/interchange_test.rs index 1bc4326b4f6..ebe0105f24d 100644 --- a/validator_client/slashing_protection/src/interchange_test.rs +++ b/validator_client/slashing_protection/src/interchange_test.rs @@ -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; diff --git a/validator_client/slashing_protection/src/lib.rs b/validator_client/slashing_protection/src/lib.rs index ded64adb492..917d51d38b7 100644 --- a/validator_client/slashing_protection/src/lib.rs +++ b/validator_client/slashing_protection/src/lib.rs @@ -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::{ diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index 7d8947a5847..ce32299a511 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -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),