diff --git a/Cargo.lock b/Cargo.lock index 352ff779752..09448ce222c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "clap", "clap_utils", "directory", + "eip_3076", "environment", "eth2", "eth2_keystore", @@ -2582,6 +2583,21 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "eip_3076" +version = "0.1.0" +dependencies = [ + "arbitrary", + "ethereum_serde_utils", + "r2d2", + "rayon", + "rusqlite", + "serde", + "serde_json", + "tracing", + "types", +] + [[package]] name = "either" version = "1.15.0" @@ -2849,6 +2865,7 @@ name = "eth2" version = "0.1.0" dependencies = [ "derivative", + "eip_3076", "either", "enr", "eth2_keystore", @@ -2868,7 +2885,6 @@ dependencies = [ "sensitive_url", "serde", "serde_json", - "slashing_protection", "ssz_types", "test_random_derive", "tokio", @@ -5575,6 +5591,7 @@ dependencies = [ "console-subscriber", "database_manager", "directory", + "eip_3076", "environment", "eth2", "eth2_network_config", @@ -5674,6 +5691,7 @@ dependencies = [ "account_utils", "beacon_node_fallback", "doppelganger_service", + "eip_3076", "either", "environment", "eth2", @@ -8831,6 +8849,7 @@ name = "slashing_protection" version = "0.1.0" dependencies = [ "arbitrary", + "eip_3076", "ethereum_serde_utils", "filesystem", "r2d2", @@ -9002,7 +9021,6 @@ dependencies = [ "ethereum_ssz_derive", "itertools 0.10.5", "leveldb", - "logging", "lru", "metrics", "parking_lot 0.12.3", @@ -9017,7 +9035,6 @@ dependencies = [ "superstruct", "tempfile", "tracing", - "tracing-subscriber", "types", "xdelta3", "zstd 0.13.3", @@ -10103,6 +10120,7 @@ dependencies = [ "directory", "dirs", "doppelganger_service", + "eip_3076", "environment", "eth2", "fdlimit", @@ -10156,6 +10174,7 @@ dependencies = [ "directory", "dirs", "doppelganger_service", + "eip_3076", "eth2", "eth2_keystore", "ethereum_serde_utils", @@ -10276,8 +10295,8 @@ dependencies = [ name = "validator_store" version = "0.1.0" dependencies = [ + "eip_3076", "eth2", - "slashing_protection", "types", ] @@ -10531,6 +10550,7 @@ version = "0.1.0" dependencies = [ "account_utils", "async-channel 1.9.0", + "eip_3076", "environment", "eth2", "eth2_keystore", diff --git a/Cargo.toml b/Cargo.toml index 66378a16c46..21ff4868ede 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", @@ -79,6 +80,7 @@ members = [ "testing/validator_test_rig", "testing/web3signer_tests", "validator_client", + "validator_client/slashing_protection", "validator_client/beacon_node_fallback", "validator_client/doppelganger_service", "validator_client/graffiti_file", @@ -87,7 +89,6 @@ members = [ "validator_client/initialized_validators", "validator_client/lighthouse_validator_store", "validator_client/signing_method", - "validator_client/slashing_protection", "validator_client/validator_metrics", "validator_client/validator_services", "validator_manager", @@ -144,6 +145,7 @@ eth2_key_derivation = { path = "crypto/eth2_key_derivation" } eth2_keystore = { path = "crypto/eth2_keystore" } eth2_network_config = { path = "common/eth2_network_config" } eth2_wallet = { path = "crypto/eth2_wallet" } +eip_3076 = { path = "common/eip_3076" } ethereum_hashing = "0.7.0" ethereum_serde_utils = "0.8.0" ethereum_ssz = "0.9.0" @@ -234,7 +236,6 @@ serde_yaml = "0.9" sha2 = "0.9" signing_method = { path = "validator_client/signing_method" } slasher = { path = "slasher", default-features = false } -slashing_protection = { path = "validator_client/slashing_protection" } slot_clock = { path = "common/slot_clock" } smallvec = { version = "1.11.2", features = ["arbitrary"] } snap = "1" @@ -275,6 +276,7 @@ validator_http_metrics = { path = "validator_client/http_metrics" } validator_metrics = { path = "validator_client/validator_metrics" } validator_services = { path = "validator_client/validator_services" } validator_store = { path = "validator_client/validator_store" } +slashing_protection = { path = "validator_client/slashing_protection" } validator_test_rig = { path = "testing/validator_test_rig" } warp = { version = "0.3.7", default-features = false, features = ["tls"] } warp_utils = { path = "common/warp_utils" } diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 071e2681dd1..2c7d4854e03 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -19,12 +19,13 @@ eth2_keystore = { workspace = true } eth2_network_config = { workspace = true } eth2_wallet = { workspace = true } eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } +eip_3076 = { workspace = true } filesystem = { workspace = true } safe_arith = { workspace = true } sensitive_url = { workspace = true } serde_json = { workspace = true } -slashing_protection = { workspace = true } slot_clock = { workspace = true } +slashing_protection = { workspace = true } tokio = { workspace = true } types = { workspace = true } validator_dir = { workspace = true } diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index 18064b990f3..c53dfd307b1 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -1,9 +1,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; +use eip_3076::interchange::{Interchange, InterchangeError, InterchangeImportOutcome}; use environment::Environment; -use slashing_protection::{ - InterchangeError, InterchangeImportOutcome, SLASHING_PROTECTION_FILENAME, SlashingDatabase, - interchange::Interchange, -}; +use slashing_protection::{SLASHING_PROTECTION_FILENAME, SlashingDatabase}; use std::fs::File; use std::path::PathBuf; use std::str::FromStr; diff --git a/beacon_node/store/Cargo.toml b/beacon_node/store/Cargo.toml index 61a8474a731..089cc7f92cf 100644 --- a/beacon_node/store/Cargo.toml +++ b/beacon_node/store/Cargo.toml @@ -17,7 +17,6 @@ ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } itertools = { workspace = true } leveldb = { version = "0.8.6", optional = true, default-features = false } -logging = { workspace = true } lru = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } @@ -30,7 +29,6 @@ state_processing = { workspace = true } strum = { workspace = true } superstruct = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } types = { workspace = true } xdelta3 = { workspace = true } zstd = { workspace = true } diff --git a/common/eip_3076/Cargo.toml b/common/eip_3076/Cargo.toml new file mode 100644 index 00000000000..4c4a1148385 --- /dev/null +++ b/common/eip_3076/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "eip_3076" +version = "0.1.0" +authors = ["Michael Sproul ", "pscott "] +edition.workspace = true +autotests = false + +[features] +arbitrary-fuzz = ["types/arbitrary-fuzz"] +portable = ["types/portable"] + +[dependencies] +arbitrary = { workspace = true, features = ["derive"] } +ethereum_serde_utils = { workspace = true } +r2d2 = { workspace = true } +rusqlite = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } +types = { workspace = true } + +[dev-dependencies] +rayon = { workspace = true } diff --git a/common/eip_3076/Makefile b/common/eip_3076/Makefile new file mode 100644 index 00000000000..2d641c9e8dd --- /dev/null +++ b/common/eip_3076/Makefile @@ -0,0 +1,43 @@ +TESTS_TAG := v5.3.0 +GENERATE_DIR := generated-tests +OUTPUT_DIR := interchange-tests +TARBALL := $(OUTPUT_DIR)-$(TESTS_TAG).tar.gz +ARCHIVE_URL := https://github.com/eth-clients/slashing-protection-interchange-tests/tarball/$(TESTS_TAG) + +ifeq ($(OS),Windows_NT) +ifeq (, $(shell where rm)) + rmfile = if exist $(1) (del /F /Q $(1)) + rmdir = if exist $(1) (rmdir /Q /S $(1)) + makedir = if not exist $(1) (mkdir $(1)) +else + rmfile = rm -f $(1) + rmdir = rm -rf $(1) + makedir = mkdir -p $(1) +endif +else + rmfile = rm -f $(1) + rmdir = rm -rf $(1) + makedir = mkdir -p $(1) +endif + +$(OUTPUT_DIR): $(TARBALL) + $(call rmdir,$@) + $(call makedir,$@) + tar --strip-components=1 -xzf $^ -C $@ + +$(TARBALL): + curl --fail -L -o $@ $(ARCHIVE_URL) + +clean-test-files: + $(call rmdir,$(OUTPUT_DIR)) + +clean-archives: + $(call rmfile,$(TARBALL)) + +generate: + $(call rmdir,$(GENERATE_DIR)) + cargo run --release --bin test_generator -- $(GENERATE_DIR) + +clean: clean-test-files clean-archives + +.PHONY: clean clean-archives clean-test-files generate diff --git a/validator_client/slashing_protection/src/interchange.rs b/common/eip_3076/src/interchange.rs similarity index 61% rename from validator_client/slashing_protection/src/interchange.rs rename to common/eip_3076/src/interchange.rs index 95a39c50e48..70a52d34417 100644 --- a/validator_client/slashing_protection/src/interchange.rs +++ b/common/eip_3076/src/interchange.rs @@ -1,10 +1,11 @@ -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}; +use crate::NotSafe; + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] #[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] @@ -157,3 +158,112 @@ impl Interchange { }) } } + +#[derive(Debug)] +pub enum InterchangeError { + UnsupportedVersion(u64), + GenesisValidatorsMismatch { + interchange_file: Hash256, + client: Hash256, + }, + MaxInconsistent, + SummaryInconsistent, + SQLError(String), + SQLPoolError(r2d2::Error), + SerdeJsonError(serde_json::Error), + InvalidPubkey(String), + NotSafe(NotSafe), + AtomicBatchAborted(Vec), +} + +impl From for InterchangeError { + fn from(error: NotSafe) -> Self { + InterchangeError::NotSafe(error) + } +} + +impl From for InterchangeError { + fn from(error: rusqlite::Error) -> Self { + Self::SQLError(error.to_string()) + } +} + +impl From for InterchangeError { + fn from(error: r2d2::Error) -> Self { + InterchangeError::SQLPoolError(error) + } +} + +impl From for InterchangeError { + fn from(error: serde_json::Error) -> Self { + InterchangeError::SerdeJsonError(error) + } +} + +/// Check that `new` is `Some` and greater than or equal to prev. +/// +/// If prev is `None` and `new` is `Some` then `true` is returned. +fn monotonic(new: Option, prev: Option) -> bool { + new.is_some_and(|new_val| prev.is_none_or(|prev_val| new_val >= prev_val)) +} + +/// The result of importing a single entry from an interchange file. +#[derive(Debug)] +pub enum InterchangeImportOutcome { + Success { + pubkey: PublicKeyBytes, + summary: ValidatorSummary, + }, + Failure { + pubkey: PublicKeyBytes, + error: NotSafe, + }, +} + +impl InterchangeImportOutcome { + pub fn failed(&self) -> bool { + matches!(self, InterchangeImportOutcome::Failure { .. }) + } +} + +/// Minimum and maximum slots and epochs signed by a validator. +#[derive(Debug)] +pub struct ValidatorSummary { + pub min_block_slot: Option, + pub max_block_slot: Option, + pub min_attestation_source: Option, + pub min_attestation_target: Option, + pub max_attestation_source: Option, + pub max_attestation_target: Option, +} + +impl ValidatorSummary { + pub fn check_block_consistency(&self, prev: &Self, imported_blocks: bool) -> bool { + if imported_blocks { + // Max block slot should be monotonically increasing and non-null. + // Minimum should match maximum due to pruning. + monotonic(self.max_block_slot, prev.max_block_slot) + && self.min_block_slot == self.max_block_slot + } else { + // Block slots should be unchanged. + prev.min_block_slot == self.min_block_slot && prev.max_block_slot == self.max_block_slot + } + } + + pub fn check_attestation_consistency(&self, prev: &Self, imported_attestations: bool) -> bool { + if imported_attestations { + // Max source and target epochs should be monotically increasing and non-null. + // Minimums should match maximums due to pruning. + monotonic(self.max_attestation_source, prev.max_attestation_source) + && monotonic(self.max_attestation_target, prev.max_attestation_target) + && self.min_attestation_source == self.max_attestation_source + && self.min_attestation_target == self.max_attestation_target + } else { + // Attestation epochs should be unchanged. + self.min_attestation_source == prev.min_attestation_source + && self.max_attestation_source == prev.max_attestation_source + && self.min_attestation_target == prev.min_attestation_target + && self.max_attestation_target == prev.max_attestation_target + } + } +} diff --git a/common/eip_3076/src/lib.rs b/common/eip_3076/src/lib.rs new file mode 100644 index 00000000000..397265edfd1 --- /dev/null +++ b/common/eip_3076/src/lib.rs @@ -0,0 +1,133 @@ +pub mod interchange; +pub mod signed_attestation; +pub mod signed_block; +pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation}; +pub use crate::signed_block::{InvalidBlock, SignedBlock}; +use rusqlite::Error as SQLError; +use std::fmt::Display; +use std::io::{Error as IOError, ErrorKind}; +use types::{Hash256, PublicKeyBytes}; + +/// The attestation or block is not safe to sign. +/// +/// This could be because it's slashable, or because an error occurred. +#[derive(PartialEq, Debug, Clone)] +pub enum NotSafe { + UnregisteredValidator(PublicKeyBytes), + DisabledValidator(PublicKeyBytes), + InvalidBlock(InvalidBlock), + InvalidAttestation(InvalidAttestation), + PermissionsError, + IOError(ErrorKind), + SQLError(String), + SQLPoolError(String), + ConsistencyError, +} + +/// The attestation or block is safe to sign, and will not cause the signer to be slashed. +#[derive(PartialEq, Debug)] +pub enum Safe { + /// Casting the exact same data (block or attestation) twice is never slashable. + SameData, + /// Incoming data is safe from slashing, and is not a duplicate. + Valid, +} + +/// A wrapper for `Hash256` that treats `0x0` as a special null value. +/// +/// Notably `SigningRoot(0x0) != SigningRoot(0x0)`. It is `PartialEq` but not `Eq`! +#[derive(Debug, Clone, Copy, Default)] +pub struct SigningRoot(pub Hash256); + +impl PartialEq for SigningRoot { + fn eq(&self, other: &Self) -> bool { + !self.is_null() && self.0 == other.0 + } +} + +impl From for SigningRoot { + fn from(hash: Hash256) -> Self { + SigningRoot(hash) + } +} + +impl From for Hash256 { + fn from(from: SigningRoot) -> Hash256 { + from.0 + } +} + +impl SigningRoot { + pub fn is_null(&self) -> bool { + self.0.is_zero() + } + + pub fn to_hash256_raw(self) -> Hash256 { + self.into() + } + + pub fn to_hash256(self) -> Option { + Some(self.0).filter(|_| !self.is_null()) + } +} + +/// Safely parse a `SigningRoot` from the given `column` of an SQLite `row`. +pub fn signing_root_from_row(column: usize, row: &rusqlite::Row) -> rusqlite::Result { + use rusqlite::{Error, types::Type}; + + let bytes: Vec = row.get(column)?; + if bytes.len() == 32 { + Ok(SigningRoot::from(Hash256::from_slice(&bytes))) + } else { + Err(Error::FromSqlConversionFailure( + column, + Type::Blob, + Box::from(format!("Invalid length for Hash256: {}", bytes.len())), + )) + } +} + +impl From for NotSafe { + fn from(error: IOError) -> NotSafe { + NotSafe::IOError(error.kind()) + } +} + +impl From for NotSafe { + fn from(error: SQLError) -> NotSafe { + NotSafe::SQLError(error.to_string()) + } +} + +impl From for NotSafe { + fn from(error: r2d2::Error) -> Self { + // Use `Display` impl to print "timed out waiting for connection" + NotSafe::SQLPoolError(format!("{}", error)) + } +} + +impl Display for NotSafe { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[cfg(test)] +mod test { + use types::{FixedBytesExtended, Hash256}; + + use super::*; + + #[test] + #[allow(clippy::eq_op)] + fn signing_root_partial_eq() { + let h0 = SigningRoot(Hash256::zero()); + let h1 = SigningRoot(Hash256::repeat_byte(1)); + let h2 = SigningRoot(Hash256::repeat_byte(2)); + assert_ne!(h0, h0); + assert_ne!(h0, h1); + assert_ne!(h1, h0); + assert_eq!(h1, h1); + assert_ne!(h1, h2); + } +} diff --git a/validator_client/slashing_protection/src/signed_attestation.rs b/common/eip_3076/src/signed_attestation.rs similarity index 100% rename from validator_client/slashing_protection/src/signed_attestation.rs rename to common/eip_3076/src/signed_attestation.rs diff --git a/validator_client/slashing_protection/src/signed_block.rs b/common/eip_3076/src/signed_block.rs similarity index 100% rename from validator_client/slashing_protection/src/signed_block.rs rename to common/eip_3076/src/signed_block.rs diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 81666a64216..d61f4c55424 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -16,6 +16,7 @@ eth2_keystore = { workspace = true } ethereum_serde_utils = { workspace = true } ethereum_ssz = { workspace = true } ethereum_ssz_derive = { workspace = true } +eip_3076 = { workspace = true } futures = { workspace = true } futures-util = "0.3.8" libp2p-identity = { version = "0.2", features = ["peerid"] } @@ -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..38ac1a1b2b2 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::Interchange; #[derive(Debug, Deserialize, Serialize, PartialEq)] pub struct GetFeeRecipientResponse { diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 4139286b532..43d2142f6cb 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -87,6 +87,7 @@ eth2 = { workspace = true } initialized_validators = { workspace = true } lighthouse_network = { workspace = true } sensitive_url = { workspace = true } +eip_3076 = { workspace = true } slashing_protection = { workspace = true } tempfile = { workspace = true } validator_dir = { workspace = true } diff --git a/testing/web3signer_tests/Cargo.toml b/testing/web3signer_tests/Cargo.toml index b4637b4030f..d138149e622 100644 --- a/testing/web3signer_tests/Cargo.toml +++ b/testing/web3signer_tests/Cargo.toml @@ -22,6 +22,7 @@ reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } +eip_3076 = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } task_executor = { workspace = true } diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index a8c8fd59f13..3d1cba76d25 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -29,6 +29,7 @@ parking_lot = { workspace = true } reqwest = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } +eip_3076 = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } tokio = { workspace = true } diff --git a/validator_client/http_api/Cargo.toml b/validator_client/http_api/Cargo.toml index 588aa2ca931..cafe8655585 100644 --- a/validator_client/http_api/Cargo.toml +++ b/validator_client/http_api/Cargo.toml @@ -32,6 +32,7 @@ sensitive_url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } signing_method = { workspace = true } +eip_3076 = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } sysinfo = { workspace = true } diff --git a/validator_client/http_api/src/tests/keystores.rs b/validator_client/http_api/src/tests/keystores.rs index a3c6cb4be32..efa70a57afb 100644 --- a/validator_client/http_api/src/tests/keystores.rs +++ b/validator_client/http_api/src/tests/keystores.rs @@ -1,6 +1,7 @@ use super::*; use account_utils::random_password_string; use bls::PublicKeyBytes; +use eip_3076::interchange::{Interchange, InterchangeMetadata}; use eth2::lighthouse_vc::types::UpdateFeeRecipientRequest; use eth2::lighthouse_vc::{ http_client::ValidatorClientHttpClient as HttpClient, @@ -9,9 +10,7 @@ use eth2::lighthouse_vc::{ }; use itertools::Itertools; use lighthouse_validator_store::DEFAULT_GAS_LIMIT; -use rand::rngs::StdRng; -use rand::{Rng, SeedableRng}; -use slashing_protection::interchange::{Interchange, InterchangeMetadata}; +use rand::{Rng, SeedableRng, rngs::StdRng}; use std::{collections::HashMap, path::Path}; use tokio::runtime::Handle; use types::{Address, attestation::AttestationBase}; diff --git a/validator_client/lighthouse_validator_store/Cargo.toml b/validator_client/lighthouse_validator_store/Cargo.toml index 0f8220bdc9f..0017e7b465a 100644 --- a/validator_client/lighthouse_validator_store/Cargo.toml +++ b/validator_client/lighthouse_validator_store/Cargo.toml @@ -11,6 +11,7 @@ doppelganger_service = { workspace = true } either = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } +eip_3076 = { workspace = true } initialized_validators = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index ed1ffa6bf6f..f2cc861fe6d 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -1,5 +1,9 @@ use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use doppelganger_service::DoppelgangerService; +use eip_3076::{ + NotSafe, Safe, + interchange::{Interchange, InterchangeError}, +}; use eth2::types::PublishBlockRequest; use initialized_validators::InitializedValidators; use logging::crit; @@ -7,9 +11,7 @@ use parking_lot::{Mutex, RwLock}; use serde::{Deserialize, Serialize}; use signing_method::Error as SigningError; use signing_method::{SignableMessage, SigningContext, SigningMethod}; -use slashing_protection::{ - InterchangeError, NotSafe, Safe, SlashingDatabase, interchange::Interchange, -}; +use slashing_protection::SlashingDatabase; use slot_clock::SlotClock; use std::marker::PhantomData; use std::path::Path; diff --git a/validator_client/slashing_protection/Cargo.toml b/validator_client/slashing_protection/Cargo.toml index 3860af514db..62755597ca1 100644 --- a/validator_client/slashing_protection/Cargo.toml +++ b/validator_client/slashing_protection/Cargo.toml @@ -16,6 +16,7 @@ filesystem = { workspace = true } r2d2 = { workspace = true } r2d2_sqlite = "0.21.0" rusqlite = { workspace = true } +eip_3076 = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } diff --git a/validator_client/slashing_protection/src/attestation_tests.rs b/validator_client/slashing_protection/src/attestation_tests.rs index 37766f271bb..595813b0386 100644 --- a/validator_client/slashing_protection/src/attestation_tests.rs +++ b/validator_client/slashing_protection/src/attestation_tests.rs @@ -2,6 +2,7 @@ use crate::test_utils::*; use crate::*; +use eip_3076::{InvalidAttestation, NotSafe, SignedAttestation}; use types::{AttestationData, Checkpoint, Epoch, FixedBytesExtended, Slot}; pub fn build_checkpoint(epoch_num: u64) -> Checkpoint { diff --git a/validator_client/slashing_protection/src/bin/test_generator.rs b/validator_client/slashing_protection/src/bin/test_generator.rs index 4576231b7bd..3a168bc06b7 100644 --- a/validator_client/slashing_protection/src/bin/test_generator.rs +++ b/validator_client/slashing_protection/src/bin/test_generator.rs @@ -1,7 +1,7 @@ -use slashing_protection::SUPPORTED_INTERCHANGE_FORMAT_VERSION; -use slashing_protection::interchange::{ +use eip_3076::interchange::{ Interchange, InterchangeData, InterchangeMetadata, SignedAttestation, SignedBlock, }; +use slashing_protection::SUPPORTED_INTERCHANGE_FORMAT_VERSION; 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/block_tests.rs b/validator_client/slashing_protection/src/block_tests.rs index b3273015f42..3360e47cbad 100644 --- a/validator_client/slashing_protection/src/block_tests.rs +++ b/validator_client/slashing_protection/src/block_tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::test_utils::*; +use eip_3076::{InvalidBlock, NotSafe, SignedBlock}; use types::{BeaconBlockHeader, FixedBytesExtended, Slot}; pub fn block(slot: u64) -> BeaconBlockHeader { diff --git a/validator_client/slashing_protection/src/extra_interchange_tests.rs b/validator_client/slashing_protection/src/extra_interchange_tests.rs index 0f88ec8b1dc..a6b4cc534fb 100644 --- a/validator_client/slashing_protection/src/extra_interchange_tests.rs +++ b/validator_client/slashing_protection/src/extra_interchange_tests.rs @@ -2,6 +2,7 @@ use crate::test_utils::pubkey; use crate::*; +use eip_3076::{NotSafe, interchange::InterchangeError}; use tempfile::tempdir; use types::FixedBytesExtended; diff --git a/validator_client/slashing_protection/src/interchange_test.rs b/validator_client/slashing_protection/src/interchange_test.rs index 1bc4326b4f6..74bd8dfc81b 100644 --- a/validator_client/slashing_protection/src/interchange_test.rs +++ b/validator_client/slashing_protection/src/interchange_test.rs @@ -1,8 +1,11 @@ use crate::{ - SigningRoot, SlashingDatabase, - interchange::{Interchange, SignedAttestation, SignedBlock}, + SlashingDatabase, test_utils::{DEFAULT_GENESIS_VALIDATORS_ROOT, pubkey}, }; +use eip_3076::{ + SigningRoot, + interchange::{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..bdd6f63cbfe 100644 --- a/validator_client/slashing_protection/src/lib.rs +++ b/validator_client/slashing_protection/src/lib.rs @@ -1,135 +1,21 @@ mod attestation_tests; mod block_tests; mod extra_interchange_tests; -pub mod interchange; pub mod interchange_test; mod parallel_tests; mod registration_tests; -mod signed_attestation; -mod signed_block; mod slashing_database; pub mod test_utils; -pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation}; -pub use crate::signed_block::{InvalidBlock, SignedBlock}; -pub use crate::slashing_database::{ - InterchangeError, InterchangeImportOutcome, SUPPORTED_INTERCHANGE_FORMAT_VERSION, - SlashingDatabase, -}; -use rusqlite::Error as SQLError; -use std::fmt::Display; -use std::io::{Error as IOError, ErrorKind}; +pub use crate::slashing_database::{SUPPORTED_INTERCHANGE_FORMAT_VERSION, SlashingDatabase}; use types::{Hash256, PublicKeyBytes}; /// The filename within the `validators` directory that contains the slashing protection DB. pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite"; -/// The attestation or block is not safe to sign. -/// -/// This could be because it's slashable, or because an error occurred. -#[derive(PartialEq, Debug, Clone)] -pub enum NotSafe { - UnregisteredValidator(PublicKeyBytes), - DisabledValidator(PublicKeyBytes), - InvalidBlock(InvalidBlock), - InvalidAttestation(InvalidAttestation), - PermissionsError, - IOError(ErrorKind), - SQLError(String), - SQLPoolError(String), - ConsistencyError, -} - -/// The attestation or block is safe to sign, and will not cause the signer to be slashed. -#[derive(PartialEq, Debug)] -pub enum Safe { - /// Casting the exact same data (block or attestation) twice is never slashable. - SameData, - /// Incoming data is safe from slashing, and is not a duplicate. - Valid, -} - -/// A wrapper for `Hash256` that treats `0x0` as a special null value. -/// -/// Notably `SigningRoot(0x0) != SigningRoot(0x0)`. It is `PartialEq` but not `Eq`! -#[derive(Debug, Clone, Copy, Default)] -pub struct SigningRoot(Hash256); - -impl PartialEq for SigningRoot { - fn eq(&self, other: &Self) -> bool { - !self.is_null() && self.0 == other.0 - } -} - -impl From for SigningRoot { - fn from(hash: Hash256) -> Self { - SigningRoot(hash) - } -} - -impl From for Hash256 { - fn from(from: SigningRoot) -> Hash256 { - from.0 - } -} - -impl SigningRoot { - fn is_null(&self) -> bool { - self.0.is_zero() - } - - fn to_hash256_raw(self) -> Hash256 { - self.into() - } - - fn to_hash256(self) -> Option { - Some(self.0).filter(|_| !self.is_null()) - } -} - -/// Safely parse a `SigningRoot` from the given `column` of an SQLite `row`. -fn signing_root_from_row(column: usize, row: &rusqlite::Row) -> rusqlite::Result { - use rusqlite::{Error, types::Type}; - - let bytes: Vec = row.get(column)?; - if bytes.len() == 32 { - Ok(SigningRoot::from(Hash256::from_slice(&bytes))) - } else { - Err(Error::FromSqlConversionFailure( - column, - Type::Blob, - Box::from(format!("Invalid length for Hash256: {}", bytes.len())), - )) - } -} - -impl From for NotSafe { - fn from(error: IOError) -> NotSafe { - NotSafe::IOError(error.kind()) - } -} - -impl From for NotSafe { - fn from(error: SQLError) -> NotSafe { - NotSafe::SQLError(error.to_string()) - } -} - -impl From for NotSafe { - fn from(error: r2d2::Error) -> Self { - // Use `Display` impl to print "timed out waiting for connection" - NotSafe::SQLPoolError(format!("{}", error)) - } -} - -impl Display for NotSafe { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - #[cfg(test)] mod test { + use eip_3076::SigningRoot; use types::FixedBytesExtended; use super::*; diff --git a/validator_client/slashing_protection/src/registration_tests.rs b/validator_client/slashing_protection/src/registration_tests.rs index 472f41577d5..723286acdc9 100644 --- a/validator_client/slashing_protection/src/registration_tests.rs +++ b/validator_client/slashing_protection/src/registration_tests.rs @@ -2,6 +2,7 @@ use crate::test_utils::*; use crate::*; +use eip_3076::NotSafe; use std::iter; use tempfile::tempdir; diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index 7d8947a5847..77c83fc97b1 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -1,10 +1,12 @@ -use crate::interchange::{ - Interchange, InterchangeData, InterchangeMetadata, SignedAttestation as InterchangeAttestation, - SignedBlock as InterchangeBlock, +use eip_3076::{ + InvalidAttestation, InvalidBlock, NotSafe, Safe, SignedAttestation, SignedBlock, SigningRoot, + interchange::{ + Interchange, InterchangeData, InterchangeError, InterchangeImportOutcome, + InterchangeMetadata, SignedAttestation as InterchangeAttestation, + SignedBlock as InterchangeBlock, ValidatorSummary, + }, + signing_root_from_row, }; -use crate::signed_attestation::InvalidAttestation; -use crate::signed_block::InvalidBlock; -use crate::{NotSafe, Safe, SignedAttestation, SignedBlock, SigningRoot, signing_root_from_row}; use filesystem::restrict_file_permissions; use r2d2_sqlite::SqliteConnectionManager; use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params}; @@ -1139,48 +1141,6 @@ impl SlashingDatabase { } } -/// Minimum and maximum slots and epochs signed by a validator. -#[derive(Debug)] -pub struct ValidatorSummary { - pub min_block_slot: Option, - pub max_block_slot: Option, - pub min_attestation_source: Option, - pub min_attestation_target: Option, - pub max_attestation_source: Option, - pub max_attestation_target: Option, -} - -impl ValidatorSummary { - fn check_block_consistency(&self, prev: &Self, imported_blocks: bool) -> bool { - if imported_blocks { - // Max block slot should be monotonically increasing and non-null. - // Minimum should match maximum due to pruning. - monotonic(self.max_block_slot, prev.max_block_slot) - && self.min_block_slot == self.max_block_slot - } else { - // Block slots should be unchanged. - prev.min_block_slot == self.min_block_slot && prev.max_block_slot == self.max_block_slot - } - } - - fn check_attestation_consistency(&self, prev: &Self, imported_attestations: bool) -> bool { - if imported_attestations { - // Max source and target epochs should be monotically increasing and non-null. - // Minimums should match maximums due to pruning. - monotonic(self.max_attestation_source, prev.max_attestation_source) - && monotonic(self.max_attestation_target, prev.max_attestation_target) - && self.min_attestation_source == self.max_attestation_source - && self.min_attestation_target == self.max_attestation_target - } else { - // Attestation epochs should be unchanged. - self.min_attestation_source == prev.min_attestation_source - && self.max_attestation_source == prev.max_attestation_source - && self.min_attestation_target == prev.min_attestation_target - && self.max_attestation_target == prev.max_attestation_target - } - } -} - /// Take the maximum of `opt_x` and `y`, returning `y` if `opt_x` is `None`. fn max_or(opt_x: Option, y: T) -> T { opt_x.map_or(y, |x| std::cmp::max(x, y)) @@ -1193,66 +1153,6 @@ fn monotonic(new: Option, prev: Option) -> bool { new.is_some_and(|new_val| prev.is_none_or(|prev_val| new_val >= prev_val)) } -/// The result of importing a single entry from an interchange file. -#[derive(Debug)] -pub enum InterchangeImportOutcome { - Success { - pubkey: PublicKeyBytes, - summary: ValidatorSummary, - }, - Failure { - pubkey: PublicKeyBytes, - error: NotSafe, - }, -} - -impl InterchangeImportOutcome { - pub fn failed(&self) -> bool { - matches!(self, InterchangeImportOutcome::Failure { .. }) - } -} - -#[derive(Debug)] -pub enum InterchangeError { - UnsupportedVersion(u64), - GenesisValidatorsMismatch { - interchange_file: Hash256, - client: Hash256, - }, - MaxInconsistent, - SummaryInconsistent, - SQLError(String), - SQLPoolError(r2d2::Error), - SerdeJsonError(serde_json::Error), - InvalidPubkey(String), - NotSafe(NotSafe), - AtomicBatchAborted(Vec), -} - -impl From for InterchangeError { - fn from(error: NotSafe) -> Self { - InterchangeError::NotSafe(error) - } -} - -impl From for InterchangeError { - fn from(error: rusqlite::Error) -> Self { - Self::SQLError(error.to_string()) - } -} - -impl From for InterchangeError { - fn from(error: r2d2::Error) -> Self { - InterchangeError::SQLPoolError(error) - } -} - -impl From for InterchangeError { - fn from(error: serde_json::Error) -> Self { - InterchangeError::SerdeJsonError(error) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/validator_client/slashing_protection/src/test_utils.rs b/validator_client/slashing_protection/src/test_utils.rs index 39ede58bb27..9a7e4fb5214 100644 --- a/validator_client/slashing_protection/src/test_utils.rs +++ b/validator_client/slashing_protection/src/test_utils.rs @@ -1,4 +1,5 @@ use crate::*; +use eip_3076::{InvalidAttestation, InvalidBlock, NotSafe, Safe}; use tempfile::{TempDir, tempdir}; use types::{AttestationData, BeaconBlockHeader, test_utils::generate_deterministic_keypair}; diff --git a/validator_client/slashing_protection/tests/migration.rs b/validator_client/slashing_protection/tests/migration.rs index 3d4ec7ea9a8..696e20b596b 100644 --- a/validator_client/slashing_protection/tests/migration.rs +++ b/validator_client/slashing_protection/tests/migration.rs @@ -1,5 +1,6 @@ //! Tests for upgrading a previous version of the database to the latest schema. -use slashing_protection::{NotSafe, SlashingDatabase}; +use eip_3076::NotSafe; +use slashing_protection::SlashingDatabase; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; diff --git a/validator_client/validator_store/Cargo.toml b/validator_client/validator_store/Cargo.toml index 8c5451b2d00..91913c87baf 100644 --- a/validator_client/validator_store/Cargo.toml +++ b/validator_client/validator_store/Cargo.toml @@ -6,5 +6,5 @@ authors = ["Sigma Prime "] [dependencies] eth2 = { workspace = true } -slashing_protection = { workspace = true } +eip_3076 = { workspace = true } types = { workspace = true } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 6fd2e270649..dc5cd88eeeb 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -1,5 +1,5 @@ +use eip_3076::NotSafe; use eth2::types::{FullBlockContents, PublishBlockRequest}; -use slashing_protection::NotSafe; use std::fmt::Debug; use std::future::Future; use std::sync::Arc;