From 20813ac0bcf0af767ccb4c97184b6c44927f0599 Mon Sep 17 00:00:00 2001 From: Aliaksei Misiukevich Date: Sun, 7 Sep 2025 19:23:55 +0200 Subject: [PATCH 1/2] making eip-3076 Signed-off-by: Aliaksei Misiukevich --- Cargo.lock | 52 +- Cargo.toml | 4 +- account_manager/Cargo.toml | 2 +- account_manager/src/validator/create.rs | 5 +- account_manager/src/validator/import.rs | 2 +- .../src/validator/slashing_protection.rs | 4 +- common/eth2/Cargo.toml | 2 +- common/eth2/src/lighthouse_vc/std_types.rs | 2 +- lighthouse/Cargo.toml | 2 +- lighthouse/tests/account_manager.rs | 2 +- testing/web3signer_tests/Cargo.toml | 2 +- testing/web3signer_tests/src/lib.rs | 2 +- validator_client/Cargo.toml | 2 +- validator_client/http_api/Cargo.toml | 2 +- validator_client/http_api/src/test_utils.rs | 2 +- validator_client/http_api/src/tests.rs | 2 +- .../http_api/src/tests/keystores.rs | 2 +- .../lighthouse_validator_store/Cargo.toml | 2 +- .../lighthouse_validator_store/src/lib.rs | 4 +- .../slashing_protection/.gitignore | 2 - .../slashing_protection/Cargo.toml | 30 - validator_client/slashing_protection/Makefile | 44 - .../v0_no_enabled_column.sqlite | Bin 28672 -> 0 bytes .../src/attestation_tests.rs | 389 ----- .../src/bin/test_generator.rs | 554 -------- .../slashing_protection/src/block_tests.rs | 124 -- .../src/extra_interchange_tests.rs | 76 - .../slashing_protection/src/interchange.rs | 159 --- .../src/interchange_test.rs | 323 ----- .../slashing_protection/src/lib.rs | 149 -- .../slashing_protection/src/parallel_tests.rs | 82 -- .../src/registration_tests.rs | 74 - .../src/signed_attestation.rs | 62 - .../slashing_protection/src/signed_block.rs | 36 - .../src/slashing_database.rs | 1251 ----------------- .../slashing_protection/src/test_utils.rs | 159 --- .../slashing_protection/tests/interop.rs | 51 - .../slashing_protection/tests/main.rs | 2 - .../slashing_protection/tests/migration.rs | 68 - validator_client/src/lib.rs | 3 +- validator_client/validator_store/Cargo.toml | 2 +- validator_client/validator_store/src/lib.rs | 2 +- 42 files changed, 51 insertions(+), 3688 deletions(-) delete mode 100644 validator_client/slashing_protection/.gitignore delete mode 100644 validator_client/slashing_protection/Cargo.toml delete mode 100644 validator_client/slashing_protection/Makefile delete mode 100644 validator_client/slashing_protection/migration-tests/v0_no_enabled_column.sqlite delete mode 100644 validator_client/slashing_protection/src/attestation_tests.rs delete mode 100644 validator_client/slashing_protection/src/bin/test_generator.rs delete mode 100644 validator_client/slashing_protection/src/block_tests.rs delete mode 100644 validator_client/slashing_protection/src/extra_interchange_tests.rs delete mode 100644 validator_client/slashing_protection/src/interchange.rs delete mode 100644 validator_client/slashing_protection/src/interchange_test.rs delete mode 100644 validator_client/slashing_protection/src/lib.rs delete mode 100644 validator_client/slashing_protection/src/parallel_tests.rs delete mode 100644 validator_client/slashing_protection/src/registration_tests.rs delete mode 100644 validator_client/slashing_protection/src/signed_attestation.rs delete mode 100644 validator_client/slashing_protection/src/signed_block.rs delete mode 100644 validator_client/slashing_protection/src/slashing_database.rs delete mode 100644 validator_client/slashing_protection/src/test_utils.rs delete mode 100644 validator_client/slashing_protection/tests/interop.rs delete mode 100644 validator_client/slashing_protection/tests/main.rs delete mode 100644 validator_client/slashing_protection/tests/migration.rs diff --git a/Cargo.lock b/Cargo.lock index 8f4dec0da26..95e9ccde720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,7 @@ dependencies = [ "clap", "clap_utils", "directory", + "eip_3076", "environment", "eth2", "eth2_keystore", @@ -21,7 +22,6 @@ dependencies = [ "safe_arith", "sensitive_url", "serde_json", - "slashing_protection", "slot_clock", "tempfile", "tokio", @@ -2570,6 +2570,24 @@ dependencies = [ "types", ] +[[package]] +name = "eip_3076" +version = "0.1.0" +dependencies = [ + "arbitrary", + "ethereum_serde_utils", + "filesystem", + "r2d2", + "r2d2_sqlite", + "rayon", + "rusqlite", + "serde", + "serde_json", + "tempfile", + "tracing", + "types", +] + [[package]] name = "either" version = "1.15.0" @@ -2764,6 +2782,7 @@ name = "eth2" version = "0.1.0" dependencies = [ "derivative", + "eip_3076", "either", "enr", "eth2_keystore", @@ -2783,7 +2802,6 @@ dependencies = [ "sensitive_url", "serde", "serde_json", - "slashing_protection", "ssz_types", "test_random_derive", "tokio", @@ -5494,6 +5512,7 @@ dependencies = [ "console-subscriber", "database_manager", "directory", + "eip_3076", "environment", "eth2", "eth2_network_config", @@ -5510,7 +5529,6 @@ dependencies = [ "serde_json", "serde_yaml", "slasher", - "slashing_protection", "store", "task_executor", "tempfile", @@ -5586,6 +5604,7 @@ dependencies = [ "account_utils", "beacon_node_fallback", "doppelganger_service", + "eip_3076", "either", "environment", "eth2", @@ -5595,7 +5614,6 @@ dependencies = [ "parking_lot 0.12.3", "serde", "signing_method", - "slashing_protection", "slot_clock", "task_executor", "tokio", @@ -8576,24 +8594,6 @@ dependencies = [ "types", ] -[[package]] -name = "slashing_protection" -version = "0.1.0" -dependencies = [ - "arbitrary", - "ethereum_serde_utils", - "filesystem", - "r2d2", - "r2d2_sqlite", - "rayon", - "rusqlite", - "serde", - "serde_json", - "tempfile", - "tracing", - "types", -] - [[package]] name = "slot_clock" version = "0.2.0" @@ -9817,6 +9817,7 @@ dependencies = [ "directory", "dirs", "doppelganger_service", + "eip_3076", "environment", "eth2", "fdlimit", @@ -9830,7 +9831,6 @@ dependencies = [ "reqwest", "sensitive_url", "serde", - "slashing_protection", "slot_clock", "tokio", "tracing", @@ -9870,6 +9870,7 @@ dependencies = [ "directory", "dirs", "doppelganger_service", + "eip_3076", "eth2", "eth2_keystore", "ethereum_serde_utils", @@ -9888,7 +9889,6 @@ dependencies = [ "serde", "serde_json", "signing_method", - "slashing_protection", "slot_clock", "sysinfo", "system_health", @@ -9990,8 +9990,8 @@ dependencies = [ name = "validator_store" version = "0.1.0" dependencies = [ + "eip_3076", "eth2", - "slashing_protection", "types", ] @@ -10245,6 +10245,7 @@ version = "0.1.0" dependencies = [ "account_utils", "async-channel 1.9.0", + "eip_3076", "environment", "eth2", "eth2_keystore", @@ -10258,7 +10259,6 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "slashing_protection", "slot_clock", "task_executor", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 6a7b2f610e6..6b381f6ecb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "common/compare_fields_derive", "common/deposit_contract", "common/directory", + "common/eip_3076", "common/eth2", "common/eth2_config", "common/eth2_interop_keypairs", @@ -86,7 +87,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", @@ -142,6 +142,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.7" ethereum_ssz = "0.8.2" @@ -226,7 +227,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" diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 071e2681dd1..54aba6fb6ec 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -19,11 +19,11 @@ 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 } tokio = { workspace = true } types = { workspace = true } diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 3db8c3f152d..988121bb7bf 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -7,9 +7,9 @@ use account_utils::{ use clap::{Arg, ArgAction, ArgMatches, Command}; use clap_utils::FLAG_HEADER; use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR}; +use eip_3076::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use environment::Environment; use eth2_wallet_manager::WalletManager; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::ffi::OsStr; use std::fs; use std::fs::create_dir_all; @@ -272,8 +272,7 @@ fn existing_validator_count>(validator_dir: P) -> Result", "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 } -filesystem = { workspace = true } -r2d2 = { workspace = true } -r2d2_sqlite = "0.21.0" -rusqlite = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -tempfile = { workspace = true } -tracing = { workspace = true } -types = { workspace = true } - -[dev-dependencies] -rayon = { workspace = true } - -[[test]] -name = "slashing_protection_tests" -path = "tests/main.rs" diff --git a/validator_client/slashing_protection/Makefile b/validator_client/slashing_protection/Makefile deleted file mode 100644 index 1b9729634e5..00000000000 --- a/validator_client/slashing_protection/Makefile +++ /dev/null @@ -1,44 +0,0 @@ -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/migration-tests/v0_no_enabled_column.sqlite b/validator_client/slashing_protection/migration-tests/v0_no_enabled_column.sqlite deleted file mode 100644 index 5a95fe36e6e4e3058d08851e7148e195984e25c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28672 zcmeI)&u$w<7y$77>%@(m;g;)00ymK=G_y0iJA;Ho8f~knZbIBtq9-%6GZUs#g3vVG}P zRKIt3uQDBTr&oP&7?rGVY$fYATdmXK1G%?t2G1Qn>}hv*yR+Bp4U)^Pjmu};`pQPT zT)VoSTq|!a9q;>c(k`!*?Q(s!ypcRP(O>H766zim$ll$ zE$VySU2X35KiYANtd6`p=#BO}6QDa}!oivQtKrPT#f$!D~+Z9X|Uxs&lcJTX0&X;53Zy7;Wo z9rdR}j_Sj;{Os^krQu$2xyZ2N*2oSlp4+}UMOB@!82uO#e_A2%8F{DD{FDC ztt=?bX`U&~WQy}#VIw&a+8E;QK_#=Mph6Q>5gQ(iM^!XWF>%+)l+j!e!_rFY)VfZ) zzl;}z6{%3la>Iq-lxLPGBHb9dDX_$bx$ZHM7g|}G8zu9i5|wg2!#FQ&aL|sCx#9xL zf?HPMtjH^_mFqn(3fBRGQ;}(sD_TijSnDL6jV8uj#-*?}!;}!rDq9F=pE+`q=BW{G zEW9GrSpT3MOe^L3&uo=Z;;J&I{M223&UyVmdj5~z^3X5n zd-Nqzt_lSNKmY_l00ck)1V8`;KmY_l00cnbi9j=se7_aPD^p4{8pcJ@l(3a%IE)L! zsd2$DE(oT@z3xZ#dTUzL==neT)kFWGztNxQH&=xM0w4eaAOHd&00JNY0w4eaAOHd& z@bU=ExbF%4IF3i#D2hhgFbqfAAP6G&{Xl*E{9kzJXY?KV0!buZo&iEnK>!3m00ck) z1V8`;KmY_l00cnb+y$C(?9a}&j{hM3#5SAJC~o+JduNtrIEo9-400#m2;#bHcUoA} zAKfmV7B_zW|IkDKpuf-`=pp)d+7)mf1V8`;KmY_l00ck)1V8`;KmY_l-~|(?MgFWG X#iLCajW$6TNA+1haR0tATaW(-ZhfP; diff --git a/validator_client/slashing_protection/src/attestation_tests.rs b/validator_client/slashing_protection/src/attestation_tests.rs deleted file mode 100644 index b577ccd9d85..00000000000 --- a/validator_client/slashing_protection/src/attestation_tests.rs +++ /dev/null @@ -1,389 +0,0 @@ -#![cfg(test)] - -use crate::test_utils::*; -use crate::*; -use types::{AttestationData, Checkpoint, Epoch, FixedBytesExtended, Slot}; - -pub fn build_checkpoint(epoch_num: u64) -> Checkpoint { - Checkpoint { - epoch: Epoch::from(epoch_num), - root: Hash256::zero(), - } -} - -pub fn attestation_data_builder(source: u64, target: u64) -> AttestationData { - let source = build_checkpoint(source); - let target = build_checkpoint(target); - let index = 0u64; - let slot = Slot::from(0u64); - - AttestationData { - slot, - index, - beacon_block_root: Hash256::zero(), - source, - target, - } -} - -/// Create a signed attestation from `attestation`, assuming the default domain. -fn signed_att(attestation: &AttestationData) -> SignedAttestation { - SignedAttestation::from_attestation(attestation, DEFAULT_DOMAIN) -} - -#[test] -fn valid_empty_history() { - StreamTest { - cases: vec![Test::single(attestation_data_builder(2, 3))], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_genesis() { - StreamTest { - cases: vec![Test::single(attestation_data_builder(0, 0))], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_out_of_order_attestation() { - StreamTest { - cases: vec![ - Test::single(attestation_data_builder(0, 3)), - Test::single(attestation_data_builder(2, 5)), - Test::single(attestation_data_builder(1, 4)), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_repeat_attestation() { - StreamTest { - cases: vec![ - Test::single(attestation_data_builder(0, 1)), - Test::single(attestation_data_builder(0, 1)).expect_same_data(), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_source_from_first_entry() { - StreamTest { - cases: vec![ - Test::single(attestation_data_builder(6, 7)), - Test::single(attestation_data_builder(6, 8)), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_multiple_validators_double_vote() { - StreamTest { - registered_validators: vec![pubkey(0), pubkey(1)], - cases: vec![ - Test::with_pubkey(pubkey(0), attestation_data_builder(0, 1)), - Test::with_pubkey(pubkey(1), attestation_data_builder(0, 1)), - ], - } - .run() -} - -#[test] -fn valid_vote_chain_repeat_first() { - StreamTest { - cases: vec![ - Test::single(attestation_data_builder(0, 1)), - Test::single(attestation_data_builder(1, 2)), - Test::single(attestation_data_builder(2, 3)), - Test::single(attestation_data_builder(0, 1)).expect_same_data(), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_vote_chain_repeat_middle() { - StreamTest { - cases: vec![ - Test::single(attestation_data_builder(0, 1)), - Test::single(attestation_data_builder(1, 2)), - Test::single(attestation_data_builder(2, 3)), - Test::single(attestation_data_builder(1, 2)).expect_same_data(), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_vote_chain_repeat_last() { - StreamTest { - cases: vec![ - Test::single(attestation_data_builder(0, 1)), - Test::single(attestation_data_builder(1, 2)), - Test::single(attestation_data_builder(2, 3)), - Test::single(attestation_data_builder(2, 3)).expect_same_data(), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_multiple_validators_not_surrounding() { - // Attestations that would be problematic if they came from the same validator, but are OK - // coming from different validators. - StreamTest { - registered_validators: vec![pubkey(0), pubkey(1)], - cases: vec![ - Test::with_pubkey(pubkey(0), attestation_data_builder(0, 10)), - Test::with_pubkey(pubkey(0), attestation_data_builder(10, 20)), - Test::with_pubkey(pubkey(1), attestation_data_builder(1, 9)), - Test::with_pubkey(pubkey(1), attestation_data_builder(9, 21)), - ], - } - .run() -} - -#[test] -fn invalid_source_exceeds_target() { - StreamTest { - cases: vec![Test::single(attestation_data_builder(1, 0)) - .expect_invalid_att(InvalidAttestation::SourceExceedsTarget)], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_unregistered_validator() { - StreamTest { - registered_validators: vec![], - cases: vec![ - Test::single(attestation_data_builder(2, 3)).expect_result(Err( - NotSafe::UnregisteredValidator(pubkey(DEFAULT_VALIDATOR_INDEX)), - )), - ], - } - .run() -} - -#[test] -fn invalid_double_vote_diff_source() { - let first = attestation_data_builder(0, 2); - StreamTest { - cases: vec![ - Test::single(first.clone()), - Test::single(attestation_data_builder(1, 2)) - .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_double_vote_diff_target() { - let first = attestation_data_builder(0, 2); - let mut second = attestation_data_builder(0, 2); - second.target.root = Hash256::random(); - assert_ne!(first, second); - StreamTest { - cases: vec![ - Test::single(first.clone()), - Test::single(second) - .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_double_vote_diff_data() { - let first = attestation_data_builder(0, 2); - let mut second = attestation_data_builder(0, 2); - second.beacon_block_root = Hash256::random(); - assert_ne!(first, second); - StreamTest { - cases: vec![ - Test::single(first.clone()), - Test::single(second) - .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_double_vote_diff_domain() { - let first = attestation_data_builder(0, 2); - let domain1 = Hash256::from_low_u64_le(1); - let domain2 = Hash256::from_low_u64_le(2); - - StreamTest { - cases: vec![ - Test::single(first.clone()).with_domain(domain1), - Test::single(first.clone()) - .with_domain(domain2) - .expect_invalid_att(InvalidAttestation::DoubleVote( - SignedAttestation::from_attestation(&first, domain1), - )), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_double_vote_diff_source_multi() { - let first = attestation_data_builder(0, 2); - let second = attestation_data_builder(1, 3); - let third = attestation_data_builder(2, 4); - StreamTest { - cases: vec![ - Test::single(first.clone()), - Test::single(second.clone()), - Test::single(third.clone()), - Test::single(attestation_data_builder(1, 2)) - .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))), - Test::single(attestation_data_builder(2, 3)) - .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&second))), - Test::single(attestation_data_builder(3, 4)) - .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&third))), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_surrounding_single() { - let first = attestation_data_builder(2, 3); - let second = attestation_data_builder(4, 5); - let third = attestation_data_builder(6, 7); - StreamTest { - cases: vec![ - Test::single(first.clone()), - Test::single(second.clone()), - Test::single(third.clone()), - Test::single(attestation_data_builder(1, 4)).expect_invalid_att( - InvalidAttestation::NewSurroundsPrev { - prev: signed_att(&first), - }, - ), - Test::single(attestation_data_builder(3, 6)).expect_invalid_att( - InvalidAttestation::NewSurroundsPrev { - prev: signed_att(&second), - }, - ), - Test::single(attestation_data_builder(5, 8)).expect_invalid_att( - InvalidAttestation::NewSurroundsPrev { - prev: signed_att(&third), - }, - ), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_surrounding_from_first_source() { - let first = attestation_data_builder(2, 3); - let second = attestation_data_builder(3, 4); - StreamTest { - cases: vec![ - Test::single(first), - Test::single(second.clone()), - Test::single(attestation_data_builder(2, 5)).expect_invalid_att( - InvalidAttestation::NewSurroundsPrev { - prev: signed_att(&second), - }, - ), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_surrounding_multiple_votes() { - let first = attestation_data_builder(0, 1); - let second = attestation_data_builder(1, 2); - let third = attestation_data_builder(2, 3); - StreamTest { - cases: vec![ - Test::single(first), - Test::single(second), - Test::single(third.clone()), - Test::single(attestation_data_builder(0, 4)).expect_invalid_att( - InvalidAttestation::NewSurroundsPrev { - prev: signed_att(&third), - }, - ), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_prev_surrounds_new() { - let first = attestation_data_builder(0, 7); - StreamTest { - cases: vec![ - Test::single(first.clone()), - Test::single(attestation_data_builder(1, 6)).expect_invalid_att( - InvalidAttestation::PrevSurroundsNew { - prev: signed_att(&first), - }, - ), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_prev_surrounds_new_multiple() { - let first = attestation_data_builder(0, 4); - let second = attestation_data_builder(1, 7); - let third = attestation_data_builder(8, 10); - StreamTest { - cases: vec![ - Test::single(first.clone()), - Test::single(second.clone()), - Test::single(third.clone()), - Test::single(attestation_data_builder(9, 9)).expect_invalid_att( - InvalidAttestation::PrevSurroundsNew { - prev: signed_att(&third), - }, - ), - Test::single(attestation_data_builder(2, 6)).expect_invalid_att( - InvalidAttestation::PrevSurroundsNew { - prev: signed_att(&second), - }, - ), - Test::single(attestation_data_builder(1, 2)).expect_invalid_att( - InvalidAttestation::PrevSurroundsNew { - prev: signed_att(&first), - }, - ), - ], - ..StreamTest::default() - } - .run() -} diff --git a/validator_client/slashing_protection/src/bin/test_generator.rs b/validator_client/slashing_protection/src/bin/test_generator.rs deleted file mode 100644 index ff5866f9866..00000000000 --- a/validator_client/slashing_protection/src/bin/test_generator.rs +++ /dev/null @@ -1,554 +0,0 @@ -use slashing_protection::interchange::{ - Interchange, InterchangeData, InterchangeMetadata, SignedAttestation, SignedBlock, -}; -use slashing_protection::interchange_test::{MultiTestCase, TestCase}; -use slashing_protection::test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT}; -use slashing_protection::SUPPORTED_INTERCHANGE_FORMAT_VERSION; -use std::fs::{self, File}; -use std::io::Write; -use std::path::Path; -use types::{Epoch, FixedBytesExtended, Hash256, Slot}; - -fn metadata(genesis_validators_root: Hash256) -> InterchangeMetadata { - InterchangeMetadata { - interchange_format_version: SUPPORTED_INTERCHANGE_FORMAT_VERSION, - genesis_validators_root, - } -} - -type TestPubkey = usize; -type TestBlocks = Vec; -type TestBlocksWithRoots = Vec<(u64, Option)>; -type TestAttestations = Vec<(u64, u64)>; -type TestAttestationsWithRoots = Vec<(u64, u64, Option)>; - -fn interchange(data: Vec<(TestPubkey, TestBlocks, TestAttestations)>) -> Interchange { - let data = data - .into_iter() - .map(|(pk, blocks, attestations)| { - ( - pk, - blocks.into_iter().map(|slot| (slot, None)).collect(), - attestations - .into_iter() - .map(|(source, target)| (source, target, None)) - .collect(), - ) - }) - .collect(); - interchange_with_signing_roots(data) -} - -fn interchange_with_signing_roots( - data: Vec<(TestPubkey, TestBlocksWithRoots, TestAttestationsWithRoots)>, -) -> Interchange { - let data = data - .into_iter() - .map(|(pk, blocks, attestations)| InterchangeData { - pubkey: pubkey(pk), - signed_blocks: blocks - .into_iter() - .map(|(slot, signing_root)| SignedBlock { - slot: Slot::new(slot), - signing_root: signing_root.map(Hash256::from_low_u64_be), - }) - .collect(), - signed_attestations: attestations - .into_iter() - .map(|(source, target, signing_root)| SignedAttestation { - source_epoch: Epoch::new(source), - target_epoch: Epoch::new(target), - signing_root: signing_root.map(Hash256::from_low_u64_be), - }) - .collect(), - }) - .collect(); - Interchange { - metadata: metadata(DEFAULT_GENESIS_VALIDATORS_ROOT), - data, - } -} - -fn main() { - let single_validator_blocks = vec![ - (0, 32, false, false), - (0, 33, true, true), - (0, 31, false, false), - (0, 1, false, false), - ]; - let single_validator_attestations = vec![ - (0, 3, 4, false, false), - (0, 14, 19, false, false), - (0, 15, 20, false, false), - (0, 16, 20, false, false), - (0, 15, 21, true, true), - ]; - - let tests = vec![ - MultiTestCase::single( - "single_validator_import_only", - TestCase::new(interchange(vec![(0, vec![22], vec![(0, 2)])])), - ), - MultiTestCase::single( - "single_validator_single_block", - TestCase::new(interchange(vec![(0, vec![32], vec![])])) - .with_blocks(single_validator_blocks.clone()), - ), - MultiTestCase::single( - "single_validator_single_attestation", - TestCase::new(interchange(vec![(0, vec![], vec![(15, 20)])])) - .with_attestations(single_validator_attestations.clone()), - ), - MultiTestCase::single( - "single_validator_single_block_and_attestation", - TestCase::new(interchange(vec![(0, vec![32], vec![(15, 20)])])) - .with_blocks(single_validator_blocks) - .with_attestations(single_validator_attestations), - ), - MultiTestCase::single( - "single_validator_genesis_attestation", - TestCase::new(interchange(vec![(0, vec![], vec![(0, 0)])])) - .with_attestations(vec![(0, 0, 0, false, false)]), - ), - MultiTestCase::single( - "single_validator_multiple_blocks_and_attestations", - TestCase::new(interchange(vec![( - 0, - vec![2, 3, 10, 1200], - vec![(10, 11), (12, 13), (20, 24)], - )])) - .with_blocks(vec![ - (0, 1, false, false), - (0, 2, false, false), - (0, 3, false, false), - (0, 10, false, false), - (0, 1200, false, false), - (0, 4, false, true), - (0, 256, false, true), - (0, 1201, true, true), - ]) - .with_attestations(vec![ - (0, 9, 10, false, false), - (0, 12, 13, false, false), - (0, 11, 14, false, false), - (0, 21, 22, false, false), - (0, 10, 24, false, false), - (0, 11, 12, false, true), - (0, 20, 25, true, true), - ]), - ), - MultiTestCase::single( - "single_validator_single_block_and_attestation_signing_root", - TestCase::new(interchange_with_signing_roots(vec![( - 0, - vec![(19, Some(1))], - vec![(0, 1, Some(2))], - )])), - ), - MultiTestCase::single( - "multiple_validators_multiple_blocks_and_attestations", - TestCase::new(interchange(vec![ - ( - 0, - vec![10, 15, 20], - vec![(0, 1), (0, 2), (1, 3), (2, 4), (4, 5)], - ), - ( - 1, - vec![3, 4, 100], - vec![(0, 0), (0, 1), (1, 2), (2, 5), (5, 6)], - ), - (2, vec![10, 15, 20], vec![(1, 2), (1, 3), (2, 4)]), - ])) - .with_blocks(vec![ - (0, 9, false, false), - (0, 10, false, false), - (0, 21, true, true), - (0, 11, false, true), - (1, 2, false, false), - (1, 3, false, false), - (1, 0, false, false), - (1, 101, true, true), - (2, 9, false, false), - (2, 10, false, false), - (2, 22, true, true), - ]) - .with_attestations(vec![ - (0, 0, 5, false, false), - (0, 3, 6, false, false), - (0, 4, 6, true, true), - (0, 5, 7, true, true), - (0, 6, 8, true, true), - (1, 1, 7, false, false), - (1, 1, 4, false, true), - (1, 5, 7, true, true), - (2, 0, 0, false, false), - (2, 0, 1, false, false), - (2, 2, 5, true, true), - ]), - ), - MultiTestCase::single( - "multiple_validators_same_slot_blocks", - TestCase::new(interchange_with_signing_roots(vec![ - (0, vec![(1, Some(0)), (2, Some(0)), (3, Some(0))], vec![]), - (1, vec![(1, Some(1)), (3, Some(1))], vec![]), - (2, vec![(1, Some(2)), (2, Some(2))], vec![]), - ])), - ), - MultiTestCase::single( - "wrong_genesis_validators_root", - TestCase::new(interchange(vec![])).should_fail(), - ) - .gvr(Hash256::from_low_u64_be(1)), - MultiTestCase::new( - "multiple_interchanges_single_validator_single_message_gap", - vec![ - TestCase::new(interchange(vec![(0, vec![40], vec![(2, 30)])])), - TestCase::new(interchange(vec![(0, vec![50], vec![(10, 50)])])) - .with_blocks(vec![ - (0, 41, false, true), - (0, 45, false, true), - (0, 49, false, true), - (0, 50, false, false), - (0, 51, true, true), - ]) - .with_attestations(vec![ - (0, 3, 31, false, true), - (0, 9, 49, false, true), - (0, 10, 51, true, true), - ]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_single_validator_single_block_out_of_order", - vec![ - TestCase::new(interchange(vec![(0, vec![40], vec![])])), - TestCase::new(interchange(vec![(0, vec![20], vec![])])) - .contains_slashable_data() - .with_blocks(vec![(0, 20, false, false)]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_single_validator_multiple_blocks_out_of_order", - vec![ - TestCase::new(interchange(vec![(0, vec![0], vec![])])).with_blocks(vec![ - (0, 10, true, true), - (0, 20, true, true), - (0, 30, true, true), - ]), - TestCase::new(interchange(vec![(0, vec![20], vec![])])) - .contains_slashable_data() - .with_blocks(vec![(0, 29, false, true)]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_single_validator_fail_iff_imported", - vec![ - TestCase::new(interchange(vec![(0, vec![40], vec![])])), - TestCase::new(interchange(vec![(0, vec![20, 50], vec![])])) - .contains_slashable_data() - .with_blocks(vec![(0, 20, false, false), (0, 50, false, false)]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_single_validator_single_att_out_of_order", - vec![ - TestCase::new(interchange(vec![(0, vec![], vec![(12, 13)])])), - TestCase::new(interchange(vec![(0, vec![], vec![(10, 11)])])) - .contains_slashable_data() - .with_attestations(vec![ - (0, 10, 14, false, false), - (0, 12, 13, false, false), - (0, 12, 14, true, true), - (0, 13, 15, true, true), - ]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_single_validator_second_surrounds_first", - vec![ - TestCase::new(interchange(vec![(0, vec![], vec![(10, 20)])])), - TestCase::new(interchange(vec![(0, vec![], vec![(9, 21)])])) - .contains_slashable_data() - .with_attestations(vec![ - (0, 10, 20, false, false), - (0, 10, 21, false, false), - (0, 9, 21, false, false), - (0, 9, 22, false, false), - (0, 10, 22, true, true), - ]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_single_validator_first_surrounds_second", - vec![ - TestCase::new(interchange(vec![(0, vec![], vec![(9, 21)])])), - TestCase::new(interchange(vec![(0, vec![], vec![(10, 20)])])) - .contains_slashable_data() - .with_attestations(vec![ - (0, 10, 20, false, false), - (0, 10, 21, false, false), - (0, 9, 21, false, false), - (0, 9, 22, false, false), - (0, 10, 22, true, true), - ]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_multiple_validators_repeat_idem", - vec![ - TestCase::new(interchange(vec![ - (0, vec![2, 4, 6], vec![(0, 1), (1, 2)]), - (1, vec![8, 10, 12], vec![(0, 1), (0, 3)]), - ])), - TestCase::new(interchange(vec![ - (0, vec![2, 4, 6], vec![(0, 1), (1, 2)]), - (1, vec![8, 10, 12], vec![(0, 1), (0, 3)]), - ])) - .contains_slashable_data() - .with_blocks(vec![ - (0, 0, false, false), - (0, 3, false, true), - (0, 7, true, true), - (0, 3, false, true), - (1, 0, false, false), - ]) - .with_attestations(vec![(0, 0, 4, false, false), (1, 0, 4, true, true)]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_overlapping_validators_repeat_idem", - vec![ - TestCase::new(interchange(vec![ - (0, vec![2, 4, 6], vec![(0, 1), (1, 2)]), - (1, vec![8, 10, 12], vec![(0, 1), (0, 3)]), - ])), - TestCase::new(interchange(vec![ - (0, vec![2, 4, 6], vec![(0, 1), (1, 2)]), - (2, vec![8, 10, 12], vec![(0, 1), (0, 3)]), - ])) - .contains_slashable_data(), - TestCase::new(interchange(vec![ - (1, vec![8, 10, 12], vec![(0, 1), (0, 3)]), - (2, vec![8, 10, 12], vec![(0, 1), (0, 3)]), - ])) - .contains_slashable_data() - .with_attestations(vec![ - (0, 0, 4, false, false), - (1, 1, 2, false, false), - (2, 1, 2, false, false), - ]), - ], - ), - MultiTestCase::new( - "multiple_interchanges_overlapping_validators_merge_stale", - vec![ - TestCase::new(interchange(vec![ - (0, vec![100], vec![(12, 13)]), - (1, vec![101], vec![(12, 13)]), - (2, vec![4], vec![(4, 5)]), - ])), - TestCase::new(interchange(vec![ - (0, vec![2], vec![(4, 5)]), - (1, vec![3], vec![(3, 4)]), - (2, vec![102], vec![(12, 13)]), - ])) - .contains_slashable_data() - .with_blocks(vec![ - (0, 100, false, false), - (1, 101, false, false), - (2, 102, false, false), - (0, 103, true, true), - (1, 104, true, true), - (2, 105, true, true), - ]) - .with_attestations(vec![ - (0, 12, 13, false, false), - (0, 11, 14, false, false), - (1, 12, 13, false, false), - (1, 11, 14, false, false), - (2, 12, 13, false, false), - (2, 11, 14, false, false), - (0, 12, 14, true, true), - (1, 13, 14, true, true), - (2, 13, 14, true, true), - ]), - ], - ), - MultiTestCase::single( - "single_validator_source_greater_than_target", - TestCase::new(interchange(vec![(0, vec![], vec![(8, 7)])])).contains_slashable_data(), - ), - MultiTestCase::single( - "single_validator_source_greater_than_target_surrounding", - TestCase::new(interchange(vec![(0, vec![], vec![(5, 2)])])) - .contains_slashable_data() - .with_attestations(vec![(0, 3, 4, false, false)]), - ), - MultiTestCase::single( - "single_validator_source_greater_than_target_surrounded", - TestCase::new(interchange(vec![(0, vec![], vec![(5, 2)])])) - .contains_slashable_data() - .with_attestations(vec![(0, 6, 1, false, false)]), - ), - MultiTestCase::single( - "single_validator_source_greater_than_target_sensible_iff_minified", - TestCase::new(interchange(vec![(0, vec![], vec![(5, 2), (6, 7)])])) - .contains_slashable_data() - .with_attestations(vec![(0, 5, 8, false, false), (0, 6, 8, true, true)]), - ), - MultiTestCase::single( - "single_validator_out_of_order_blocks", - TestCase::new(interchange(vec![(0, vec![6, 5], vec![])])).with_blocks(vec![ - (0, 5, false, false), - (0, 6, false, false), - (0, 7, true, true), - ]), - ), - MultiTestCase::single( - "single_validator_out_of_order_attestations", - TestCase::new(interchange(vec![(0, vec![], vec![(4, 5), (3, 4)])])).with_attestations( - vec![ - (0, 3, 4, false, false), - (0, 4, 5, false, false), - (0, 1, 10, false, false), - (0, 3, 3, false, false), - ], - ), - ), - // Ensure that it's not just the minimum bound check preventing blocks at the same slot - // from being signed. - MultiTestCase::single( - "single_validator_two_blocks_no_signing_root", - TestCase::new(interchange(vec![(0, vec![10, 20], vec![])])) - .with_blocks(vec![(0, 20, false, false)]), - ), - MultiTestCase::single( - "single_validator_multiple_block_attempts", - TestCase::new(interchange(vec![(0, vec![15, 16, 17], vec![])])) - .with_signing_root_blocks(vec![ - (0, 16, 0, false, false), - (0, 16, 1, false, false), - (0, 16, u64::MAX, false, false), - ]), - ), - MultiTestCase::single( - "single_validator_resign_block", - TestCase::new(interchange_with_signing_roots(vec![( - 0, - vec![(15, Some(151)), (16, Some(161)), (17, Some(171))], - vec![], - )])) - .with_signing_root_blocks(vec![ - (0, 15, 151, false, true), - (0, 16, 161, false, true), - (0, 17, 171, false, true), - (0, 15, 152, false, false), - (0, 15, 0, false, false), - (0, 16, 151, false, false), - (0, 17, 151, false, false), - (0, 18, 151, true, true), - (0, 14, 171, false, false), - ]), - ), - MultiTestCase::single( - "single_validator_resign_attestation", - TestCase::new(interchange_with_signing_roots(vec![( - 0, - vec![], - vec![(5, 15, Some(515))], - )])) - .with_signing_root_attestations(vec![ - (0, 5, 15, 0, false, false), - (0, 5, 15, 1, false, false), - (0, 5, 15, 515, false, true), - (0, 6, 15, 615, false, false), - (0, 5, 14, 515, false, false), - ]), - ), - MultiTestCase::single( - "single_validator_slashable_blocks", - TestCase::new(interchange_with_signing_roots(vec![( - 0, - vec![(10, Some(0)), (10, Some(11))], - vec![], - )])) - .contains_slashable_data(), - ), - MultiTestCase::single( - "single_validator_slashable_blocks_no_root", - TestCase::new(interchange(vec![(0, vec![10, 10], vec![])])).contains_slashable_data(), - ), - MultiTestCase::single( - "single_validator_slashable_attestations_double_vote", - TestCase::new(interchange_with_signing_roots(vec![( - 0, - vec![], - vec![(2, 3, Some(0)), (2, 3, Some(1))], - )])) - .contains_slashable_data(), - ), - MultiTestCase::single( - "single_validator_slashable_attestations_surrounds_existing", - TestCase::new(interchange(vec![(0, vec![], vec![(2, 3), (0, 4)])])) - .contains_slashable_data(), - ), - MultiTestCase::single( - "single_validator_slashable_attestations_surrounded_by_existing", - TestCase::new(interchange(vec![(0, vec![], vec![(0, 4), (2, 3)])])) - .contains_slashable_data(), - ), - MultiTestCase::single( - "duplicate_pubkey_not_slashable", - TestCase::new(interchange(vec![ - (0, vec![10, 11], vec![(0, 2)]), - (0, vec![12, 13], vec![(1, 3)]), - ])) - .with_blocks(vec![ - (0, 10, false, false), - (0, 13, false, false), - (0, 14, true, true), - ]) - .with_attestations(vec![(0, 0, 2, false, false), (0, 1, 3, false, false)]), - ), - MultiTestCase::single( - "duplicate_pubkey_slashable_block", - TestCase::new(interchange(vec![ - (0, vec![10], vec![(0, 2)]), - (0, vec![10], vec![(1, 3)]), - ])) - .contains_slashable_data() - .with_blocks(vec![(0, 10, false, false), (0, 11, true, true)]), - ), - MultiTestCase::single( - "duplicate_pubkey_slashable_attestation", - TestCase::new(interchange_with_signing_roots(vec![ - (0, vec![], vec![(0, 3, Some(3))]), - (0, vec![], vec![(1, 2, None)]), - ])) - .contains_slashable_data() - .with_attestations(vec![ - (0, 0, 1, false, false), - (0, 0, 2, false, false), - (0, 0, 4, false, false), - (0, 1, 4, true, true), - ]), - ), - ]; - - let args = std::env::args().collect::>(); - let output_dir = Path::new(&args[1]); - fs::create_dir_all(output_dir).unwrap(); - - for test in tests { - // Check that test case passes without minification - test.run(false); - - // Check that test case passes with minification - test.run(true); - - let f = File::create(output_dir.join(format!("{}.json", test.name))).unwrap(); - serde_json::to_writer_pretty(&f, &test).unwrap(); - writeln!(&f).unwrap(); - } -} diff --git a/validator_client/slashing_protection/src/block_tests.rs b/validator_client/slashing_protection/src/block_tests.rs deleted file mode 100644 index b3273015f42..00000000000 --- a/validator_client/slashing_protection/src/block_tests.rs +++ /dev/null @@ -1,124 +0,0 @@ -#![cfg(test)] - -use super::*; -use crate::test_utils::*; -use types::{BeaconBlockHeader, FixedBytesExtended, Slot}; - -pub fn block(slot: u64) -> BeaconBlockHeader { - BeaconBlockHeader { - slot: Slot::new(slot), - proposer_index: 0, - parent_root: Hash256::random(), - state_root: Hash256::random(), - body_root: Hash256::random(), - } -} - -#[test] -fn valid_empty_history() { - StreamTest { - cases: vec![Test::single(block(1))], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_blocks() { - StreamTest { - cases: vec![ - Test::single(block(1)), - Test::single(block(2)), - Test::single(block(3)), - Test::single(block(4)), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_same_block() { - let block = block(100); - StreamTest { - cases: vec![ - Test::single(block.clone()), - Test::single(block).expect_same_data(), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn valid_same_slot_different_validator() { - StreamTest { - registered_validators: vec![pubkey(0), pubkey(1)], - cases: vec![ - Test::with_pubkey(pubkey(0), block(100)), - Test::with_pubkey(pubkey(1), block(100)), - ], - } - .run() -} - -#[test] -fn valid_same_block_different_validator() { - let block = block(100); - StreamTest { - registered_validators: vec![pubkey(0), pubkey(1)], - cases: vec![ - Test::with_pubkey(pubkey(0), block.clone()), - Test::with_pubkey(pubkey(1), block), - ], - } - .run() -} - -#[test] -fn invalid_double_block_proposal() { - let first_block = block(1); - StreamTest { - cases: vec![ - Test::single(first_block.clone()), - Test::single(block(1)).expect_invalid_block(InvalidBlock::DoubleBlockProposal( - SignedBlock::from_header(&first_block, DEFAULT_DOMAIN), - )), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_double_block_proposal_diff_domain() { - let first_block = block(1); - let domain1 = Hash256::from_low_u64_be(1); - let domain2 = Hash256::from_low_u64_be(2); - StreamTest { - cases: vec![ - Test::single(first_block.clone()).with_domain(domain1), - Test::single(first_block.clone()) - .with_domain(domain2) - .expect_invalid_block(InvalidBlock::DoubleBlockProposal(SignedBlock::from_header( - &first_block, - domain1, - ))), - ], - ..StreamTest::default() - } - .run() -} - -#[test] -fn invalid_unregistered_validator() { - StreamTest { - registered_validators: vec![], - cases: vec![ - Test::single(block(0)).expect_result(Err(NotSafe::UnregisteredValidator(pubkey( - DEFAULT_VALIDATOR_INDEX, - )))), - ], - } - .run() -} diff --git a/validator_client/slashing_protection/src/extra_interchange_tests.rs b/validator_client/slashing_protection/src/extra_interchange_tests.rs deleted file mode 100644 index 0f88ec8b1dc..00000000000 --- a/validator_client/slashing_protection/src/extra_interchange_tests.rs +++ /dev/null @@ -1,76 +0,0 @@ -#![cfg(test)] - -use crate::test_utils::pubkey; -use crate::*; -use tempfile::tempdir; -use types::FixedBytesExtended; - -#[test] -fn export_non_existent_key() { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - let key1 = pubkey(1); - let key2 = pubkey(2); - - // Exporting two non-existent keys should fail on the first one. - let err = slashing_db - .export_interchange_info(Hash256::zero(), Some(&[key1, key2])) - .unwrap_err(); - assert!(matches!( - err, - InterchangeError::NotSafe(NotSafe::UnregisteredValidator(k)) if k == key1 - )); - - slashing_db.register_validator(key1).unwrap(); - - // Exporting one key that exists and one that doesn't should fail on the one that doesn't. - let err = slashing_db - .export_interchange_info(Hash256::zero(), Some(&[key1, key2])) - .unwrap_err(); - assert!(matches!( - err, - InterchangeError::NotSafe(NotSafe::UnregisteredValidator(k)) if k == key2 - )); - - // Exporting only keys that exist should work. - let interchange = slashing_db - .export_interchange_info(Hash256::zero(), Some(&[key1])) - .unwrap(); - assert_eq!(interchange.data.len(), 1); - assert_eq!(interchange.data[0].pubkey, key1); -} - -#[test] -fn export_same_key_twice() { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - let key1 = pubkey(1); - - slashing_db.register_validator(key1).unwrap(); - - let export_single = slashing_db - .export_interchange_info(Hash256::zero(), Some(&[key1])) - .unwrap(); - let export_double = slashing_db - .export_interchange_info(Hash256::zero(), Some(&[key1, key1])) - .unwrap(); - - assert_eq!(export_single.data.len(), 1); - - // Allow the same data to be exported twice, this is harmless, albeit slightly inefficient. - assert_eq!(export_double.data.len(), 2); - assert_eq!(export_double.data[0], export_double.data[1]); - - // The data should be identical to the single export. - assert_eq!(export_double.data[0], export_single.data[0]); - - // The minified versions should be equal too. - assert_eq!( - export_single.minify().unwrap(), - export_double.minify().unwrap() - ); -} diff --git a/validator_client/slashing_protection/src/interchange.rs b/validator_client/slashing_protection/src/interchange.rs deleted file mode 100644 index 95a39c50e48..00000000000 --- a/validator_client/slashing_protection/src/interchange.rs +++ /dev/null @@ -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, - pub signed_attestations: Vec, -} - -#[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, -} - -#[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, -} - -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] -pub struct Interchange { - pub metadata: InterchangeMetadata, - pub data: Vec, -} - -impl Interchange { - pub fn from_json_str(json: &str) -> Result { - serde_json::from_str(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 - 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::>(); - let other_set = other.data.iter().collect::>(); - 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 { - // Map from pubkey to optional max block and max attestation. - let mut validator_data = - HashMap::, Option)>::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, - }) - } -} diff --git a/validator_client/slashing_protection/src/interchange_test.rs b/validator_client/slashing_protection/src/interchange_test.rs deleted file mode 100644 index e1ac841905f..00000000000 --- a/validator_client/slashing_protection/src/interchange_test.rs +++ /dev/null @@ -1,323 +0,0 @@ -use crate::{ - interchange::{Interchange, SignedAttestation, SignedBlock}, - test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT}, - SigningRoot, SlashingDatabase, -}; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use tempfile::tempdir; -use types::{Epoch, FixedBytesExtended, Hash256, PublicKeyBytes, Slot}; - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] -pub struct MultiTestCase { - pub name: String, - pub genesis_validators_root: Hash256, - pub steps: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] -pub struct TestCase { - pub should_succeed: bool, - pub contains_slashable_data: bool, - pub interchange: Interchange, - pub blocks: Vec, - pub attestations: Vec, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] -pub struct TestBlock { - pub pubkey: PublicKeyBytes, - pub slot: Slot, - pub signing_root: Hash256, - pub should_succeed: bool, - pub should_succeed_complete: bool, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] -pub struct TestAttestation { - pub pubkey: PublicKeyBytes, - pub source_epoch: Epoch, - pub target_epoch: Epoch, - pub signing_root: Hash256, - pub should_succeed: bool, - pub should_succeed_complete: bool, -} - -impl MultiTestCase { - pub fn new(name: &str, steps: Vec) -> Self { - MultiTestCase { - name: name.into(), - genesis_validators_root: DEFAULT_GENESIS_VALIDATORS_ROOT, - steps, - } - } - - pub fn single(name: &str, test_case: TestCase) -> Self { - Self::new(name, vec![test_case]) - } - - pub fn gvr(mut self, genesis_validators_root: Hash256) -> Self { - self.genesis_validators_root = genesis_validators_root; - self - } - - pub fn run(&self, minify: bool) { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - for test_case in &self.steps { - // If the test case is marked as containing slashable data, then the spec allows us to - // fail to import the file. However, we minify on import and ignore slashable data, so - // we should be capable of importing no matter what. - let allow_import_failure = false; - - let interchange = if minify { - let minified = test_case.interchange.minify().unwrap(); - check_minification_invariants(&test_case.interchange, &minified); - minified - } else { - test_case.interchange.clone() - }; - - match slashing_db.import_interchange_info(interchange, self.genesis_validators_root) { - Ok(import_outcomes) => { - let none_failed = import_outcomes.iter().all(|o| !o.failed()); - assert!( - none_failed, - "test `{}` failed to import some records: {:#?}", - self.name, import_outcomes - ); - if !test_case.should_succeed { - panic!( - "test `{}` succeeded on import when it should have failed", - self.name - ); - } - } - Err(e) => { - if test_case.should_succeed && !allow_import_failure { - panic!( - "test `{}` failed on import when it should have succeeded, error: {:?}", - self.name, e - ); - } - break; - } - } - - for (i, block) in test_case.blocks.iter().enumerate() { - match slashing_db.check_and_insert_block_signing_root( - &block.pubkey, - block.slot, - SigningRoot::from(block.signing_root), - ) { - Ok(safe) if !block.should_succeed => { - panic!( - "block {} from `{}` succeeded when it should have failed: {:?}", - i, self.name, safe - ); - } - Err(e) if block.should_succeed => { - panic!( - "block {} from `{}` failed when it should have succeeded: {:?}", - i, self.name, e - ); - } - _ => (), - } - } - - for (i, att) in test_case.attestations.iter().enumerate() { - match slashing_db.check_and_insert_attestation_signing_root( - &att.pubkey, - att.source_epoch, - att.target_epoch, - SigningRoot::from(att.signing_root), - ) { - Ok(safe) if !att.should_succeed => { - panic!( - "attestation {} from `{}` succeeded when it should have failed: {:?}", - i, self.name, safe - ); - } - Err(e) if att.should_succeed => { - panic!( - "attestation {} from `{}` failed when it should have succeeded: {:?}", - i, self.name, e - ); - } - _ => (), - } - } - } - } -} - -impl TestCase { - pub fn new(interchange: Interchange) -> Self { - TestCase { - should_succeed: true, - contains_slashable_data: false, - interchange, - blocks: vec![], - attestations: vec![], - } - } - - pub fn should_fail(mut self) -> Self { - self.should_succeed = false; - self - } - - pub fn contains_slashable_data(mut self) -> Self { - self.contains_slashable_data = true; - self - } - - pub fn with_blocks(self, blocks: impl IntoIterator) -> Self { - self.with_signing_root_blocks(blocks.into_iter().map( - |(index, slot, should_succeed, should_succeed_complete)| { - (index, slot, 0, should_succeed, should_succeed_complete) - }, - )) - } - - pub fn with_signing_root_blocks( - mut self, - blocks: impl IntoIterator, - ) -> Self { - self.blocks.extend(blocks.into_iter().map( - |(pk, slot, signing_root, should_succeed, should_succeed_complete)| { - assert!( - !should_succeed || should_succeed_complete, - "if should_succeed is true then should_succeed_complete must also be true" - ); - TestBlock { - pubkey: pubkey(pk), - slot: Slot::new(slot), - signing_root: Hash256::from_low_u64_be(signing_root), - should_succeed, - should_succeed_complete, - } - }, - )); - self - } - - pub fn with_attestations( - self, - attestations: impl IntoIterator, - ) -> Self { - self.with_signing_root_attestations(attestations.into_iter().map( - |(id, source, target, succeed, succeed_complete)| { - (id, source, target, 0, succeed, succeed_complete) - }, - )) - } - - pub fn with_signing_root_attestations( - mut self, - attestations: impl IntoIterator, - ) -> Self { - self.attestations.extend(attestations.into_iter().map( - |(pk, source, target, signing_root, should_succeed, should_succeed_complete)| { - assert!( - !should_succeed || should_succeed_complete, - "if should_succeed is true then should_succeed_complete must also be true" - ); - TestAttestation { - pubkey: pubkey(pk), - source_epoch: Epoch::new(source), - target_epoch: Epoch::new(target), - signing_root: Hash256::from_low_u64_be(signing_root), - should_succeed, - should_succeed_complete, - } - }, - )); - self - } -} - -pub fn check_minification_invariants(interchange: &Interchange, minified: &Interchange) { - // Metadata should be unchanged. - assert_eq!(interchange.metadata, minified.metadata); - - // Minified data should contain one entry per *unique* public key. - let uniq_pubkeys = get_uniq_pubkeys(interchange); - assert_eq!(uniq_pubkeys, get_uniq_pubkeys(minified)); - assert_eq!(uniq_pubkeys.len(), minified.data.len()); - - for &pubkey in uniq_pubkeys.iter() { - // Minified data should contain 1 block per validator, unless the validator never signed any - // blocks. All of those blocks should have slots <= the slot of the minified block. - let original_blocks = get_blocks_of_validator(interchange, pubkey); - let minified_blocks = get_blocks_of_validator(minified, pubkey); - - if original_blocks.is_empty() { - assert!(minified_blocks.is_empty()); - } else { - // Should have exactly 1 block. - assert_eq!(minified_blocks.len(), 1); - - // That block should have no signing root (it's synthetic). - let mini_block = minified_blocks.first().unwrap(); - assert_eq!(mini_block.signing_root, None); - - // All original blocks should have slots <= the mini block. - assert!(original_blocks - .iter() - .all(|block| block.slot <= mini_block.slot)); - } - - // Minified data should contain 1 attestation per validator, unless the validator never - // signed any attestations. All attestations should have source and target <= the source - // and target of the minified attestation. - let original_attestations = get_attestations_of_validator(interchange, pubkey); - let minified_attestations = get_attestations_of_validator(minified, pubkey); - - if original_attestations.is_empty() { - assert!(minified_attestations.is_empty()); - } else { - assert_eq!(minified_attestations.len(), 1); - - let mini_attestation = minified_attestations.first().unwrap(); - assert_eq!(mini_attestation.signing_root, None); - - assert!(original_attestations - .iter() - .all(|att| att.source_epoch <= mini_attestation.source_epoch - && att.target_epoch <= mini_attestation.target_epoch)); - } - } -} - -fn get_uniq_pubkeys(interchange: &Interchange) -> HashSet { - interchange.data.iter().map(|data| data.pubkey).collect() -} - -fn get_blocks_of_validator(interchange: &Interchange, pubkey: PublicKeyBytes) -> Vec<&SignedBlock> { - interchange - .data - .iter() - .filter(|data| data.pubkey == pubkey) - .flat_map(|data| data.signed_blocks.iter()) - .collect() -} - -fn get_attestations_of_validator( - interchange: &Interchange, - pubkey: PublicKeyBytes, -) -> Vec<&SignedAttestation> { - interchange - .data - .iter() - .filter(|data| data.pubkey == pubkey) - .flat_map(|data| data.signed_attestations.iter()) - .collect() -} diff --git a/validator_client/slashing_protection/src/lib.rs b/validator_client/slashing_protection/src/lib.rs deleted file mode 100644 index 825a34cabc7..00000000000 --- a/validator_client/slashing_protection/src/lib.rs +++ /dev/null @@ -1,149 +0,0 @@ -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, SlashingDatabase, - SUPPORTED_INTERCHANGE_FORMAT_VERSION, -}; -use rusqlite::Error as SQLError; -use std::fmt::Display; -use std::io::{Error as IOError, ErrorKind}; -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::{types::Type, Error}; - - 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; - - 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/parallel_tests.rs b/validator_client/slashing_protection/src/parallel_tests.rs deleted file mode 100644 index e3cc1a0d567..00000000000 --- a/validator_client/slashing_protection/src/parallel_tests.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Tests that stress the concurrency safety of the slashing protection DB. -#![cfg(test)] - -use crate::attestation_tests::attestation_data_builder; -use crate::block_tests::block; -use crate::test_utils::*; -use crate::*; -use rayon::prelude::*; -use tempfile::tempdir; - -#[test] -fn block_same_slot() { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - let pk = pubkey(0); - - slashing_db.register_validator(pk).unwrap(); - - // A stream of blocks all with the same slot. - let num_blocks = 10; - let results = (0..num_blocks) - .into_par_iter() - .map(|_| slashing_db.check_and_insert_block_proposal(&pk, &block(1), DEFAULT_DOMAIN)) - .collect::>(); - - let num_successes = results.iter().filter(|res| res.is_ok()).count(); - assert_eq!(num_successes, 1); -} - -#[test] -fn attestation_same_target() { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - let pk = pubkey(0); - - slashing_db.register_validator(pk).unwrap(); - - // A stream of attestations all with the same target. - let num_attestations = 10; - let results = (0..num_attestations) - .into_par_iter() - .map(|i| { - slashing_db.check_and_insert_attestation( - &pk, - &attestation_data_builder(i, num_attestations), - DEFAULT_DOMAIN, - ) - }) - .collect::>(); - - let num_successes = results.iter().filter(|res| res.is_ok()).count(); - assert_eq!(num_successes, 1); -} - -#[test] -fn attestation_surround_fest() { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - let pk = pubkey(0); - - slashing_db.register_validator(pk).unwrap(); - - // A stream of attestations that all surround each other. - let num_attestations = 10; - - let results = (0..num_attestations) - .into_par_iter() - .map(|i| { - let att = attestation_data_builder(i, 2 * num_attestations - i); - slashing_db.check_and_insert_attestation(&pk, &att, DEFAULT_DOMAIN) - }) - .collect::>(); - - let num_successes = results.iter().filter(|res| res.is_ok()).count(); - assert_eq!(num_successes, 1); -} diff --git a/validator_client/slashing_protection/src/registration_tests.rs b/validator_client/slashing_protection/src/registration_tests.rs deleted file mode 100644 index 472f41577d5..00000000000 --- a/validator_client/slashing_protection/src/registration_tests.rs +++ /dev/null @@ -1,74 +0,0 @@ -#![cfg(test)] - -use crate::test_utils::*; -use crate::*; -use std::iter; -use tempfile::tempdir; - -#[test] -fn double_register_validators() { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - let num_validators = 100u32; - let pubkeys = (0..num_validators as usize).map(pubkey).collect::>(); - - let get_validator_ids = || { - pubkeys - .iter() - .map(|pk| slashing_db.get_validator_id(pk).unwrap()) - .collect::>() - }; - - assert_eq!(slashing_db.num_validator_rows().unwrap(), 0); - - slashing_db.register_validators(pubkeys.iter()).unwrap(); - assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators); - let validator_ids = get_validator_ids(); - - slashing_db.register_validators(pubkeys.iter()).unwrap(); - assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators); - assert_eq!(validator_ids, get_validator_ids()); -} - -#[test] -fn reregister_validator() { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - let pk = pubkey(0); - - // Register validator. - slashing_db.register_validator(pk).unwrap(); - let id = slashing_db.get_validator_id(&pk).unwrap(); - - slashing_db - .with_transaction(|txn| { - // Disable. - slashing_db.update_validator_status(txn, id, false)?; - - // Fetching the validator as "registered" should now fail. - assert_eq!( - slashing_db.get_validator_id_in_txn(txn, &pk).unwrap_err(), - NotSafe::DisabledValidator(pk) - ); - - // Fetching its status should return false. - let (fetched_id, enabled) = - slashing_db.get_validator_id_with_status(txn, &pk)?.unwrap(); - assert_eq!(fetched_id, id); - assert!(!enabled); - - // Re-registering the validator should preserve its ID while changing its status to - // enabled. - slashing_db.register_validators_in_txn(iter::once(&pk), txn)?; - - let re_reg_id = slashing_db.get_validator_id_in_txn(txn, &pk)?; - assert_eq!(re_reg_id, id); - - Ok::<_, NotSafe>(()) - }) - .unwrap(); -} diff --git a/validator_client/slashing_protection/src/signed_attestation.rs b/validator_client/slashing_protection/src/signed_attestation.rs deleted file mode 100644 index 332f80c7045..00000000000 --- a/validator_client/slashing_protection/src/signed_attestation.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::{signing_root_from_row, SigningRoot}; -use types::{AttestationData, Epoch, Hash256, SignedRoot}; - -/// An attestation that has previously been signed. -#[derive(Clone, Debug, PartialEq)] -pub struct SignedAttestation { - pub source_epoch: Epoch, - pub target_epoch: Epoch, - pub signing_root: SigningRoot, -} - -/// Reasons why an attestation may be slashable (or invalid). -#[derive(PartialEq, Debug, Clone)] -pub enum InvalidAttestation { - /// The attestation has the same target epoch as an attestation from the DB (enclosed). - DoubleVote(SignedAttestation), - /// The attestation surrounds an existing attestation from the database (`prev`). - NewSurroundsPrev { prev: SignedAttestation }, - /// The attestation is surrounded by an existing attestation from the database (`prev`). - PrevSurroundsNew { prev: SignedAttestation }, - /// The attestation is invalid because its source epoch is greater than its target epoch. - SourceExceedsTarget, - /// The attestation is invalid because its source epoch is less than the lower bound on source - /// epochs for this validator. - SourceLessThanLowerBound { - source_epoch: Epoch, - bound_epoch: Epoch, - }, - /// The attestation is invalid because its target epoch is less than or equal to the lower - /// bound on target epochs for this validator. - TargetLessThanOrEqLowerBound { - target_epoch: Epoch, - bound_epoch: Epoch, - }, -} - -impl SignedAttestation { - pub fn new(source_epoch: Epoch, target_epoch: Epoch, signing_root: SigningRoot) -> Self { - Self { - source_epoch, - target_epoch, - signing_root, - } - } - - /// Create a `SignedAttestation` from attestation data and a domain. - pub fn from_attestation(attestation: &AttestationData, domain: Hash256) -> Self { - Self { - source_epoch: attestation.source.epoch, - target_epoch: attestation.target.epoch, - signing_root: attestation.signing_root(domain).into(), - } - } - - /// Create a `SignedAttestation` from an SQLite row of `(source, target, signing_root)`. - pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result { - let source = row.get(0)?; - let target = row.get(1)?; - let signing_root = signing_root_from_row(2, row)?; - Ok(SignedAttestation::new(source, target, signing_root)) - } -} diff --git a/validator_client/slashing_protection/src/signed_block.rs b/validator_client/slashing_protection/src/signed_block.rs deleted file mode 100644 index d46872529e9..00000000000 --- a/validator_client/slashing_protection/src/signed_block.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::{signing_root_from_row, SigningRoot}; -use types::{BeaconBlockHeader, Hash256, SignedRoot, Slot}; - -/// A block that has previously been signed. -#[derive(Clone, Debug, PartialEq)] -pub struct SignedBlock { - pub slot: Slot, - pub signing_root: SigningRoot, -} - -/// Reasons why a block may be slashable. -#[derive(PartialEq, Debug, Clone)] -pub enum InvalidBlock { - DoubleBlockProposal(SignedBlock), - SlotViolatesLowerBound { block_slot: Slot, bound_slot: Slot }, -} - -impl SignedBlock { - pub fn new(slot: Slot, signing_root: SigningRoot) -> Self { - Self { slot, signing_root } - } - - pub fn from_header(header: &BeaconBlockHeader, domain: Hash256) -> Self { - Self { - slot: header.slot, - signing_root: header.signing_root(domain).into(), - } - } - - /// Parse an SQLite row of `(slot, signing_root)`. - pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result { - let slot = row.get(0)?; - let signing_root = signing_root_from_row(1, row)?; - Ok(SignedBlock { slot, signing_root }) - } -} diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs deleted file mode 100644 index f4c844d3140..00000000000 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ /dev/null @@ -1,1251 +0,0 @@ -use crate::interchange::{ - Interchange, InterchangeData, InterchangeMetadata, SignedAttestation as InterchangeAttestation, - SignedBlock as InterchangeBlock, -}; -use crate::signed_attestation::InvalidAttestation; -use crate::signed_block::InvalidBlock; -use crate::{signing_root_from_row, NotSafe, Safe, SignedAttestation, SignedBlock, SigningRoot}; -use filesystem::restrict_file_permissions; -use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior}; -use std::fs::File; -use std::path::Path; -use std::time::Duration; -use types::{AttestationData, BeaconBlockHeader, Epoch, Hash256, PublicKeyBytes, SignedRoot, Slot}; - -type Pool = r2d2::Pool; - -/// We set the pool size to 1 for compatibility with locking_mode=EXCLUSIVE. -/// -/// This is perhaps overkill in the presence of exclusive transactions, but has -/// the added bonus of preventing other processes from trying to use our slashing database. -pub const POOL_SIZE: u32 = 1; -#[cfg(not(test))] -pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); -#[cfg(test)] -pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(1); - -/// Supported version of the interchange format. -pub const SUPPORTED_INTERCHANGE_FORMAT_VERSION: u64 = 5; - -/// Column ID of the `validators.enabled` column. -pub const VALIDATORS_ENABLED_CID: i64 = 2; - -#[derive(Debug, Clone)] -pub struct SlashingDatabase { - conn_pool: Pool, -} - -impl SlashingDatabase { - /// Open an existing database at the given `path`, or create one if none exists. - pub fn open_or_create(path: &Path) -> Result { - if path.exists() { - Self::open(path) - } else { - Self::create(path) - } - } - - /// Create a slashing database at the given path. - /// - /// Error if a database (or any file) already exists at `path`. - pub fn create(path: &Path) -> Result { - let _file = File::options() - .write(true) - .read(true) - .create_new(true) - .open(path)?; - - restrict_file_permissions(path).map_err(|_| NotSafe::PermissionsError)?; - let conn_pool = Self::open_conn_pool(path)?; - let mut conn = conn_pool.get()?; - - conn.execute( - "CREATE TABLE validators ( - id INTEGER PRIMARY KEY, - public_key BLOB NOT NULL UNIQUE - )", - params![], - )?; - - conn.execute( - "CREATE TABLE signed_blocks ( - validator_id INTEGER NOT NULL, - slot INTEGER NOT NULL, - signing_root BLOB NOT NULL, - FOREIGN KEY(validator_id) REFERENCES validators(id) - UNIQUE (validator_id, slot) - )", - params![], - )?; - - conn.execute( - "CREATE TABLE signed_attestations ( - validator_id INTEGER, - source_epoch INTEGER NOT NULL, - target_epoch INTEGER NOT NULL, - signing_root BLOB NOT NULL, - FOREIGN KEY(validator_id) REFERENCES validators(id) - UNIQUE (validator_id, target_epoch) - )", - params![], - )?; - - // The tables created above are for the v0 schema. We immediately update them - // to the latest schema without dropping the connection. - let txn = conn.transaction()?; - Self::apply_schema_migrations(&txn)?; - txn.commit()?; - - Ok(Self { conn_pool }) - } - - /// Open an existing `SlashingDatabase` from disk. - /// - /// This will automatically check for and apply the latest schema migrations. - pub fn open(path: &Path) -> Result { - let conn_pool = Self::open_conn_pool(path)?; - let db = Self { conn_pool }; - db.with_transaction(Self::apply_schema_migrations)?; - Ok(db) - } - - fn apply_schema_migrations(txn: &Transaction) -> Result<(), NotSafe> { - // Add the `enabled` column to the `validators` table if it does not already exist. - let enabled_col_exists = txn - .query_row( - "SELECT cid, name FROM pragma_table_info('validators') WHERE name = 'enabled'", - params![], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .optional()? - .map(|(cid, name): (i64, String)| { - // Check that the enabled column is in the correct position with the right name. - // This is a defensive check that shouldn't do anything in practice unless the - // slashing DB has been manually edited. - if cid == VALIDATORS_ENABLED_CID && name == "enabled" { - Ok(()) - } else { - Err(NotSafe::ConsistencyError) - } - }) - .transpose()? - .is_some(); - - if !enabled_col_exists { - txn.execute( - "ALTER TABLE validators ADD COLUMN enabled BOOL NOT NULL DEFAULT TRUE", - params![], - )?; - } - - Ok(()) - } - - /// Open a new connection pool with all of the necessary settings and tweaks. - fn open_conn_pool(path: &Path) -> Result { - let manager = SqliteConnectionManager::file(path) - .with_flags(rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE) - .with_init(Self::apply_pragmas); - let conn_pool = Pool::builder() - .max_size(POOL_SIZE) - .connection_timeout(CONNECTION_TIMEOUT) - .build(manager) - .map_err(|e| NotSafe::SQLError(format!("Unable to open database: {:?}", e)))?; - Ok(conn_pool) - } - - /// Apply the necessary settings to an SQLite connection. - /// - /// Most importantly, put the database into exclusive locking mode, so that threads are forced - /// to serialise all DB access (to prevent slashable data being checked and signed in parallel). - /// The exclusive locking mode also has the benefit of applying to other processes, so multiple - /// Lighthouse processes trying to access the same database will also be blocked. - fn apply_pragmas(conn: &mut rusqlite::Connection) -> Result<(), rusqlite::Error> { - conn.pragma_update(None, "foreign_keys", true)?; - conn.pragma_update(None, "locking_mode", "EXCLUSIVE")?; - Ok(()) - } - - /// Creates an empty transaction and drops it. Used to test whether the database is locked. - pub fn test_transaction(&self) -> Result<(), NotSafe> { - let mut conn = self.conn_pool.get()?; - Transaction::new(&mut conn, TransactionBehavior::Exclusive)?; - Ok(()) - } - - /// Execute a database transaction as a closure, committing if `f` returns `Ok`. - pub fn with_transaction(&self, f: F) -> Result - where - F: FnOnce(&Transaction) -> Result, - U: From, - { - let mut conn = self.conn_pool.get().map_err(NotSafe::from)?; - let txn = conn.transaction().map_err(NotSafe::from)?; - let value = f(&txn)?; - txn.commit().map_err(NotSafe::from)?; - Ok(value) - } - - /// Register a validator with the slashing protection database. - /// - /// This allows the validator to record their signatures in the database, and check - /// for slashings. - pub fn register_validator(&self, validator_pk: PublicKeyBytes) -> Result<(), NotSafe> { - self.register_validators(std::iter::once(&validator_pk)) - } - - /// Register multiple validators with the slashing protection database. - pub fn register_validators<'a>( - &self, - public_keys: impl Iterator, - ) -> Result<(), NotSafe> { - self.with_transaction(|txn| self.register_validators_in_txn(public_keys, txn)) - } - - /// Register multiple validators inside the given transaction. - /// - /// The caller must commit the transaction for the changes to be persisted. - pub fn register_validators_in_txn<'a>( - &self, - public_keys: impl Iterator, - txn: &Transaction, - ) -> Result<(), NotSafe> { - let mut stmt = - txn.prepare("INSERT INTO validators (public_key, enabled) VALUES (?1, TRUE)")?; - for pubkey in public_keys { - match self.get_validator_id_with_status(txn, pubkey)? { - None => { - stmt.execute([pubkey.as_hex_string()])?; - } - Some((validator_id, false)) => { - self.update_validator_status(txn, validator_id, true)?; - } - Some((_, true)) => { - // Validator already registered and enabled. - } - } - } - Ok(()) - } - - pub fn update_validator_status( - &self, - txn: &Transaction, - validator_id: i64, - status: bool, - ) -> Result<(), NotSafe> { - txn.execute( - "UPDATE validators SET enabled = ? WHERE id = ?", - params![status, validator_id], - )?; - Ok(()) - } - - /// Check that all of the given validators are registered. - pub fn check_validator_registrations<'a>( - &self, - mut public_keys: impl Iterator, - ) -> Result<(), NotSafe> { - let mut conn = self.conn_pool.get()?; - let txn = conn.transaction()?; - public_keys - .try_for_each(|public_key| self.get_validator_id_in_txn(&txn, public_key).map(|_| ())) - } - - /// List the internal validator ID and public key of every registered validator. - pub fn list_all_registered_validators( - &self, - txn: &Transaction, - ) -> Result, InterchangeError> { - txn.prepare("SELECT id, public_key FROM validators ORDER BY id ASC")? - .query_and_then(params![], |row| { - let validator_id = row.get(0)?; - let pubkey_str: String = row.get(1)?; - let pubkey = pubkey_str - .parse() - .map_err(InterchangeError::InvalidPubkey)?; - Ok((validator_id, pubkey)) - })? - .collect() - } - - /// Get the database-internal ID for an enabled validator. - /// - /// This is NOT the same as a validator index, and depends on the ordering that validators - /// are registered with the slashing protection database (and may vary between machines). - pub fn get_validator_id(&self, public_key: &PublicKeyBytes) -> Result { - let mut conn = self.conn_pool.get()?; - let txn = conn.transaction()?; - self.get_validator_id_in_txn(&txn, public_key) - } - - pub fn get_validator_id_in_txn( - &self, - txn: &Transaction, - public_key: &PublicKeyBytes, - ) -> Result { - let (validator_id, enabled) = self - .get_validator_id_with_status(txn, public_key)? - .ok_or(NotSafe::UnregisteredValidator(*public_key))?; - if enabled { - Ok(validator_id) - } else { - Err(NotSafe::DisabledValidator(*public_key)) - } - } - - /// Get validator ID regardless of whether or not it is enabled. - pub fn get_validator_id_ignoring_status( - &self, - txn: &Transaction, - public_key: &PublicKeyBytes, - ) -> Result { - let (validator_id, _) = self - .get_validator_id_with_status(txn, public_key)? - .ok_or(NotSafe::UnregisteredValidator(*public_key))?; - Ok(validator_id) - } - - pub fn get_validator_id_with_status( - &self, - txn: &Transaction, - public_key: &PublicKeyBytes, - ) -> Result, NotSafe> { - Ok(txn - .query_row( - "SELECT id, enabled FROM validators WHERE public_key = ?1", - params![&public_key.as_hex_string()], - |row| Ok((row.get(0)?, row.get(1)?)), - ) - .optional()?) - } - - /// Check a block proposal from `validator_pubkey` for slash safety. - fn check_block_proposal( - &self, - txn: &Transaction, - validator_pubkey: &PublicKeyBytes, - slot: Slot, - signing_root: SigningRoot, - ) -> Result { - let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; - - let existing_block = txn - .prepare( - "SELECT slot, signing_root - FROM signed_blocks - WHERE validator_id = ?1 AND slot = ?2", - )? - .query_row(params![validator_id, slot], SignedBlock::from_row) - .optional()?; - - if let Some(existing_block) = existing_block { - if existing_block.signing_root == signing_root { - // Same slot and same hash -> we're re-broadcasting a previously signed block - return Ok(Safe::SameData); - } else { - // Same epoch but not the same hash -> it's a DoubleBlockProposal - return Err(NotSafe::InvalidBlock(InvalidBlock::DoubleBlockProposal( - existing_block, - ))); - } - } - - let min_slot = txn - .prepare("SELECT MIN(slot) FROM signed_blocks WHERE validator_id = ?1")? - .query_row(params![validator_id], |row| row.get(0))?; - - if let Some(min_slot) = min_slot { - if slot <= min_slot { - return Err(NotSafe::InvalidBlock( - InvalidBlock::SlotViolatesLowerBound { - block_slot: slot, - bound_slot: min_slot, - }, - )); - } - } - - Ok(Safe::Valid) - } - - /// Check an attestation from `validator_pubkey` for slash safety. - fn check_attestation( - &self, - txn: &Transaction, - validator_pubkey: &PublicKeyBytes, - att_source_epoch: Epoch, - att_target_epoch: Epoch, - att_signing_root: SigningRoot, - ) -> Result { - // Although it's not required to avoid slashing, we disallow attestations - // which are obviously invalid by virtue of their source epoch exceeding their target. - if att_source_epoch > att_target_epoch { - return Err(NotSafe::InvalidAttestation( - InvalidAttestation::SourceExceedsTarget, - )); - } - - let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; - - // Check for a double vote. Namely, an existing attestation with the same target epoch, - // and a different signing root. - let same_target_att = txn - .prepare( - "SELECT source_epoch, target_epoch, signing_root - FROM signed_attestations - WHERE validator_id = ?1 AND target_epoch = ?2", - )? - .query_row( - params![validator_id, att_target_epoch], - SignedAttestation::from_row, - ) - .optional()?; - - if let Some(existing_attestation) = same_target_att { - // If the new attestation is identical to the existing attestation, then we already - // know that it is safe, and can return immediately. - if existing_attestation.signing_root == att_signing_root { - return Ok(Safe::SameData); - // Otherwise if the hashes are different, this is a double vote. - } else { - return Err(NotSafe::InvalidAttestation(InvalidAttestation::DoubleVote( - existing_attestation, - ))); - } - } - - // Check that no previous vote is surrounding `attestation`. - // If there is a surrounding attestation, we only return the most recent one. - let surrounding_attestation = txn - .prepare( - "SELECT source_epoch, target_epoch, signing_root - FROM signed_attestations - WHERE validator_id = ?1 AND source_epoch < ?2 AND target_epoch > ?3 - ORDER BY target_epoch DESC - LIMIT 1", - )? - .query_row( - params![validator_id, att_source_epoch, att_target_epoch], - SignedAttestation::from_row, - ) - .optional()?; - - if let Some(prev) = surrounding_attestation { - return Err(NotSafe::InvalidAttestation( - InvalidAttestation::PrevSurroundsNew { prev }, - )); - } - - // Check that no previous vote is surrounded by `attestation`. - // If there is a surrounded attestation, we only return the most recent one. - let surrounded_attestation = txn - .prepare( - "SELECT source_epoch, target_epoch, signing_root - FROM signed_attestations - WHERE validator_id = ?1 AND source_epoch > ?2 AND target_epoch < ?3 - ORDER BY target_epoch DESC - LIMIT 1", - )? - .query_row( - params![validator_id, att_source_epoch, att_target_epoch], - SignedAttestation::from_row, - ) - .optional()?; - - if let Some(prev) = surrounded_attestation { - return Err(NotSafe::InvalidAttestation( - InvalidAttestation::NewSurroundsPrev { prev }, - )); - } - - // Check lower bounds: ensure that source is greater than or equal to min source, - // and target is greater than min target. This allows pruning, and compatibility - // with the interchange format. - let min_source = txn - .prepare("SELECT MIN(source_epoch) FROM signed_attestations WHERE validator_id = ?1")? - .query_row(params![validator_id], |row| row.get(0))?; - - if let Some(min_source) = min_source { - if att_source_epoch < min_source { - return Err(NotSafe::InvalidAttestation( - InvalidAttestation::SourceLessThanLowerBound { - source_epoch: att_source_epoch, - bound_epoch: min_source, - }, - )); - } - } - - let min_target = txn - .prepare("SELECT MIN(target_epoch) FROM signed_attestations WHERE validator_id = ?1")? - .query_row(params![validator_id], |row| row.get(0))?; - - if let Some(min_target) = min_target { - if att_target_epoch <= min_target { - return Err(NotSafe::InvalidAttestation( - InvalidAttestation::TargetLessThanOrEqLowerBound { - target_epoch: att_target_epoch, - bound_epoch: min_target, - }, - )); - } - } - - // Everything has been checked, return Valid - Ok(Safe::Valid) - } - - /// Insert a block proposal into the slashing database. - /// - /// This should *only* be called in the same (exclusive) transaction as `check_block_proposal` - /// so that the check isn't invalidated by a concurrent mutation. - fn insert_block_proposal( - &self, - txn: &Transaction, - validator_pubkey: &PublicKeyBytes, - slot: Slot, - signing_root: SigningRoot, - ) -> Result<(), NotSafe> { - let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; - - txn.execute( - "INSERT INTO signed_blocks (validator_id, slot, signing_root) - VALUES (?1, ?2, ?3)", - params![validator_id, slot, signing_root.to_hash256_raw().as_slice()], - )?; - Ok(()) - } - - /// Insert an attestation into the slashing database. - /// - /// This should *only* be called in the same (exclusive) transaction as `check_attestation` - /// so that the check isn't invalidated by a concurrent mutation. - fn insert_attestation( - &self, - txn: &Transaction, - validator_pubkey: &PublicKeyBytes, - att_source_epoch: Epoch, - att_target_epoch: Epoch, - att_signing_root: SigningRoot, - ) -> Result<(), NotSafe> { - let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; - - txn.execute( - "INSERT INTO signed_attestations (validator_id, source_epoch, target_epoch, signing_root) - VALUES (?1, ?2, ?3, ?4)", - params![ - validator_id, - att_source_epoch, - att_target_epoch, - att_signing_root.to_hash256_raw().as_slice() - ], - )?; - Ok(()) - } - - /// Check a block proposal for slash safety, and if it is safe, record it in the database. - /// - /// The checking and inserting happen atomically and exclusively. We enforce exclusivity - /// to prevent concurrent checks and inserts from resulting in slashable data being inserted. - /// - /// This is the safe, externally-callable interface for checking block proposals. - pub fn check_and_insert_block_proposal( - &self, - validator_pubkey: &PublicKeyBytes, - block_header: &BeaconBlockHeader, - domain: Hash256, - ) -> Result { - self.check_and_insert_block_signing_root( - validator_pubkey, - block_header.slot, - block_header.signing_root(domain).into(), - ) - } - - /// As for `check_and_insert_block_proposal` but without requiring the whole `BeaconBlockHeader`. - pub fn check_and_insert_block_signing_root( - &self, - validator_pubkey: &PublicKeyBytes, - slot: Slot, - signing_root: SigningRoot, - ) -> Result { - let mut conn = self.conn_pool.get()?; - let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; - let safe = self.check_and_insert_block_signing_root_txn( - validator_pubkey, - slot, - signing_root, - &txn, - )?; - txn.commit()?; - Ok(safe) - } - - /// Transactional variant of `check_and_insert_block_signing_root`. - pub fn check_and_insert_block_signing_root_txn( - &self, - validator_pubkey: &PublicKeyBytes, - slot: Slot, - signing_root: SigningRoot, - txn: &Transaction, - ) -> Result { - let safe = self.check_block_proposal(txn, validator_pubkey, slot, signing_root)?; - - if safe != Safe::SameData { - self.insert_block_proposal(txn, validator_pubkey, slot, signing_root)?; - } - Ok(safe) - } - - /// Check an attestation for slash safety, and if it is safe, record it in the database. - /// - /// The checking and inserting happen atomically and exclusively. We enforce exclusivity - /// to prevent concurrent checks and inserts from resulting in slashable data being inserted. - /// - /// This is the safe, externally-callable interface for checking attestations. - pub fn check_and_insert_attestation( - &self, - validator_pubkey: &PublicKeyBytes, - attestation: &AttestationData, - domain: Hash256, - ) -> Result { - let attestation_signing_root = attestation.signing_root(domain).into(); - self.check_and_insert_attestation_signing_root( - validator_pubkey, - attestation.source.epoch, - attestation.target.epoch, - attestation_signing_root, - ) - } - - /// As for `check_and_insert_attestation` but without requiring the whole `AttestationData`. - pub fn check_and_insert_attestation_signing_root( - &self, - validator_pubkey: &PublicKeyBytes, - att_source_epoch: Epoch, - att_target_epoch: Epoch, - att_signing_root: SigningRoot, - ) -> Result { - let mut conn = self.conn_pool.get()?; - let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; - let safe = self.check_and_insert_attestation_signing_root_txn( - validator_pubkey, - att_source_epoch, - att_target_epoch, - att_signing_root, - &txn, - )?; - txn.commit()?; - Ok(safe) - } - - /// Transactional variant of `check_and_insert_attestation_signing_root`. - fn check_and_insert_attestation_signing_root_txn( - &self, - validator_pubkey: &PublicKeyBytes, - att_source_epoch: Epoch, - att_target_epoch: Epoch, - att_signing_root: SigningRoot, - txn: &Transaction, - ) -> Result { - let safe = self.check_attestation( - txn, - validator_pubkey, - att_source_epoch, - att_target_epoch, - att_signing_root, - )?; - - if safe != Safe::SameData { - self.insert_attestation( - txn, - validator_pubkey, - att_source_epoch, - att_target_epoch, - att_signing_root, - )?; - } - Ok(safe) - } - - /// Import slashing protection from another client in the interchange format. - /// - /// This function will atomically import the entire interchange, failing if *any* - /// record cannot be imported. - pub fn import_interchange_info( - &self, - interchange: Interchange, - genesis_validators_root: Hash256, - ) -> Result, InterchangeError> { - let version = interchange.metadata.interchange_format_version; - if version != SUPPORTED_INTERCHANGE_FORMAT_VERSION { - return Err(InterchangeError::UnsupportedVersion(version)); - } - - if genesis_validators_root != interchange.metadata.genesis_validators_root { - return Err(InterchangeError::GenesisValidatorsMismatch { - client: genesis_validators_root, - interchange_file: interchange.metadata.genesis_validators_root, - }); - } - - // Create a single transaction for the entire batch, which will only be committed if - // all records are imported successfully. - let mut conn = self.conn_pool.get()?; - let txn = conn.transaction()?; - - let mut import_outcomes = vec![]; - let mut commit = true; - - for record in interchange.data { - let pubkey = record.pubkey; - match self.import_interchange_record(record, &txn) { - Ok(summary) => { - import_outcomes.push(InterchangeImportOutcome::Success { pubkey, summary }); - } - Err(error) => { - import_outcomes.push(InterchangeImportOutcome::Failure { pubkey, error }); - commit = false; - } - } - } - - if commit { - txn.commit()?; - Ok(import_outcomes) - } else { - Err(InterchangeError::AtomicBatchAborted(import_outcomes)) - } - } - - pub fn import_interchange_record( - &self, - record: InterchangeData, - txn: &Transaction, - ) -> Result { - let pubkey = &record.pubkey; - - self.register_validators_in_txn(std::iter::once(pubkey), txn)?; - - // Summary of minimum and maximum messages pre-import. - let prev_summary = self.validator_summary(pubkey, txn)?; - - // If the interchange contains any blocks, update the database with the new max slot. - let max_block = record.signed_blocks.iter().max_by_key(|b| b.slot); - - if let Some(max_block) = max_block { - // Store new synthetic block with maximum slot and null signing root. Remove all other - // blocks. - let new_max_slot = max_or(prev_summary.max_block_slot, max_block.slot); - let signing_root = SigningRoot::default(); - - self.clear_signed_blocks(pubkey, txn)?; - self.insert_block_proposal(txn, pubkey, new_max_slot, signing_root)?; - } - - // Find the attestations with max source and max target. Unless the input contains slashable - // data these two attestations should be identical, but we also handle the case where they - // are not. - let max_source_attestation = record - .signed_attestations - .iter() - .max_by_key(|att| att.source_epoch); - let max_target_attestation = record - .signed_attestations - .iter() - .max_by_key(|att| att.target_epoch); - - if let (Some(max_source_att), Some(max_target_att)) = - (max_source_attestation, max_target_attestation) - { - let source_epoch = max_or( - prev_summary.max_attestation_source, - max_source_att.source_epoch, - ); - let target_epoch = max_or( - prev_summary.max_attestation_target, - max_target_att.target_epoch, - ); - let signing_root = SigningRoot::default(); - - // Clear existing attestations before insert to avoid running afoul of the target epoch - // uniqueness constraint. - self.clear_signed_attestations(pubkey, txn)?; - self.insert_attestation(txn, pubkey, source_epoch, target_epoch, signing_root)?; - } - - let summary = self.validator_summary(&record.pubkey, txn)?; - - // Check that the summary is consistent with having added the new data. - if summary.check_block_consistency(&prev_summary, !record.signed_blocks.is_empty()) - && summary.check_attestation_consistency( - &prev_summary, - !record.signed_attestations.is_empty(), - ) - { - Ok(summary) - } else { - // This should never occur and is indicative of a bug in the import code. - Err(NotSafe::ConsistencyError) - } - } - - pub fn export_all_interchange_info( - &self, - genesis_validators_root: Hash256, - ) -> Result { - self.export_interchange_info(genesis_validators_root, None) - } - - pub fn export_interchange_info( - &self, - genesis_validators_root: Hash256, - selected_pubkeys: Option<&[PublicKeyBytes]>, - ) -> Result { - let mut conn = self.conn_pool.get()?; - let txn = &conn.transaction()?; - self.export_interchange_info_in_txn(genesis_validators_root, selected_pubkeys, txn) - } - - pub fn export_interchange_info_in_txn( - &self, - genesis_validators_root: Hash256, - selected_pubkeys: Option<&[PublicKeyBytes]>, - txn: &Transaction, - ) -> Result { - // Determine the validator IDs and public keys to export data for. - let to_export = if let Some(selected_pubkeys) = selected_pubkeys { - selected_pubkeys - .iter() - .map(|pubkey| { - let id = self.get_validator_id_ignoring_status(txn, pubkey)?; - Ok((id, *pubkey)) - }) - .collect::>()? - } else { - self.list_all_registered_validators(txn)? - }; - - let data = to_export - .into_iter() - .map(|(validator_id, pubkey)| { - let signed_blocks = - self.export_interchange_blocks_for_validator(validator_id, txn)?; - let signed_attestations = - self.export_interchange_attestations_for_validator(validator_id, txn)?; - Ok(InterchangeData { - pubkey, - signed_blocks, - signed_attestations, - }) - }) - .collect::>()?; - - let metadata = InterchangeMetadata { - interchange_format_version: SUPPORTED_INTERCHANGE_FORMAT_VERSION, - genesis_validators_root, - }; - - Ok(Interchange { metadata, data }) - } - - fn export_interchange_blocks_for_validator( - &self, - validator_id: i64, - txn: &Transaction, - ) -> Result, InterchangeError> { - txn.prepare( - "SELECT slot, signing_root - FROM signed_blocks - WHERE signed_blocks.validator_id = ?1 - ORDER BY slot ASC", - )? - .query_and_then(params![validator_id], |row| { - let slot = row.get(0)?; - let signing_root = signing_root_from_row(1, row)?.to_hash256(); - Ok(InterchangeBlock { slot, signing_root }) - })? - .collect() - } - - fn export_interchange_attestations_for_validator( - &self, - validator_id: i64, - txn: &Transaction, - ) -> Result, InterchangeError> { - txn.prepare( - "SELECT source_epoch, target_epoch, signing_root - FROM signed_attestations - WHERE signed_attestations.validator_id = ?1 - ORDER BY source_epoch ASC, target_epoch ASC", - )? - .query_and_then(params![validator_id], |row| { - let source_epoch = row.get(0)?; - let target_epoch = row.get(1)?; - let signing_root = signing_root_from_row(2, row)?.to_hash256(); - let signed_attestation = InterchangeAttestation { - source_epoch, - target_epoch, - signing_root, - }; - Ok(signed_attestation) - })? - .collect() - } - - /// Remove all blocks for `public_key` with slots less than `new_min_slot`. - fn prune_signed_blocks( - &self, - public_key: &PublicKeyBytes, - new_min_slot: Slot, - txn: &Transaction, - ) -> Result<(), NotSafe> { - let validator_id = self.get_validator_id_in_txn(txn, public_key)?; - - txn.execute( - "DELETE FROM signed_blocks - WHERE - validator_id = ?1 AND - slot < ?2 AND - slot < (SELECT MAX(slot) - FROM signed_blocks - WHERE validator_id = ?1)", - params![validator_id, new_min_slot], - )?; - - Ok(()) - } - - /// Prune the signed blocks table for the given public keys. - pub fn prune_all_signed_blocks<'a>( - &self, - mut public_keys: impl Iterator, - new_min_slot: Slot, - ) -> Result<(), NotSafe> { - let mut conn = self.conn_pool.get()?; - let txn = conn.transaction()?; - public_keys.try_for_each(|pubkey| self.prune_signed_blocks(pubkey, new_min_slot, &txn))?; - txn.commit()?; - Ok(()) - } - - /// Remove all attestations for `public_key` with `target < new_min_target`. - /// - /// If the `new_min_target` was plucked out of thin air and doesn't necessarily correspond to - /// an extant attestation then this function is still safe. It will never delete *all* the - /// attestations in the database. - fn prune_signed_attestations( - &self, - public_key: &PublicKeyBytes, - new_min_target: Epoch, - txn: &Transaction, - ) -> Result<(), NotSafe> { - let validator_id = self.get_validator_id_in_txn(txn, public_key)?; - - // The following holds, because we never store mutually slashable attestations: - // a.target < new_min_target --> a.source <= new_min_source - // - // The `MAX(target_epoch)` acts as a guard to prevent accidentally clearing the DB. - txn.execute( - "DELETE FROM signed_attestations - WHERE - validator_id = ?1 AND - target_epoch < ?2 AND - target_epoch < (SELECT MAX(target_epoch) - FROM signed_attestations - WHERE validator_id = ?1)", - params![validator_id, new_min_target], - )?; - - Ok(()) - } - - /// Remove all attestations signed by a given `public_key`. - /// - /// This function is incredibly dangerous and should be used with extreme caution. Presently - /// we only use it one place: immediately before inserting a new maximum source/maximum target - /// attestation. Any future use should take care to respect the database's non-emptiness. - fn clear_signed_attestations( - &self, - public_key: &PublicKeyBytes, - txn: &Transaction, - ) -> Result<(), NotSafe> { - let validator_id = self.get_validator_id_in_txn(txn, public_key)?; - - txn.execute( - "DELETE FROM signed_attestations WHERE validator_id = ?1", - params![validator_id], - )?; - Ok(()) - } - - /// Remove all blocks signed by a given `public_key`. - /// - /// Dangerous, should only be used immediately before inserting a new block in the same - /// transacation. - fn clear_signed_blocks( - &self, - public_key: &PublicKeyBytes, - txn: &Transaction, - ) -> Result<(), NotSafe> { - let validator_id = self.get_validator_id_in_txn(txn, public_key)?; - txn.execute( - "DELETE FROM signed_blocks WHERE validator_id = ?1", - params![validator_id], - )?; - Ok(()) - } - - /// Prune the signed attestations table for the given validator keys. - pub fn prune_all_signed_attestations<'a>( - &self, - mut public_keys: impl Iterator, - new_min_target: Epoch, - ) -> Result<(), NotSafe> { - let mut conn = self.conn_pool.get()?; - let txn = conn.transaction()?; - public_keys - .try_for_each(|pubkey| self.prune_signed_attestations(pubkey, new_min_target, &txn))?; - txn.commit()?; - Ok(()) - } - - pub fn num_validator_rows(&self) -> Result { - let mut conn = self.conn_pool.get()?; - let txn = conn.transaction()?; - let count = txn - .prepare("SELECT COALESCE(COUNT(*), 0) FROM validators")? - .query_row(params![], |row| row.get(0))?; - Ok(count) - } - - /// Get a summary of a validator's slashing protection data including minimums and maximums. - pub fn validator_summary( - &self, - public_key: &PublicKeyBytes, - txn: &Transaction, - ) -> Result { - let validator_id = self.get_validator_id_in_txn(txn, public_key)?; - let (min_block_slot, max_block_slot) = txn - .prepare( - "SELECT MIN(slot), MAX(slot) - FROM signed_blocks - WHERE validator_id = ?1", - )? - .query_row(params![validator_id], |row| Ok((row.get(0)?, row.get(1)?)))?; - - let ( - min_attestation_source, - min_attestation_target, - max_attestation_source, - max_attestation_target, - ) = txn - .prepare( - "SELECT MIN(source_epoch), MIN(target_epoch), MAX(source_epoch), MAX(target_epoch) - FROM signed_attestations - WHERE validator_id = ?1", - )? - .query_row(params![validator_id], |row| { - Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)) - })?; - - Ok(ValidatorSummary { - min_block_slot, - max_block_slot, - min_attestation_source, - min_attestation_target, - max_attestation_source, - max_attestation_target, - }) - } -} - -/// 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)) -} - -/// 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 { .. }) - } -} - -#[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::*; - use tempfile::tempdir; - - #[test] - fn open_non_existent_error() { - let dir = tempdir().unwrap(); - let file = dir.path().join("db.sqlite"); - assert!(SlashingDatabase::open(&file).is_err()); - } - - // Due to the exclusive locking, trying to use an already open database should error. - #[test] - fn double_open_error() { - let dir = tempdir().unwrap(); - let file = dir.path().join("db.sqlite"); - let _db1 = SlashingDatabase::create(&file).unwrap(); - - SlashingDatabase::open(&file).unwrap_err(); - } - - // Attempting to create the same database twice should error. - #[test] - fn double_create_error() { - let dir = tempdir().unwrap(); - let file = dir.path().join("db.sqlite"); - let _db1 = SlashingDatabase::create(&file).unwrap(); - drop(_db1); - SlashingDatabase::create(&file).unwrap_err(); - } - - // Check that both `open` and `create` apply the same connection settings. - #[test] - fn connection_settings_applied() { - let dir = tempdir().unwrap(); - let file = dir.path().join("db.sqlite"); - - let check = |db: &SlashingDatabase| { - assert_eq!(db.conn_pool.max_size(), POOL_SIZE); - assert_eq!(db.conn_pool.connection_timeout(), CONNECTION_TIMEOUT); - let conn = db.conn_pool.get().unwrap(); - assert!(conn - .pragma_query_value(None, "foreign_keys", |row| { row.get::<_, bool>(0) }) - .unwrap()); - assert_eq!( - conn.pragma_query_value(None, "locking_mode", |row| { row.get::<_, String>(0) }) - .unwrap() - .to_uppercase(), - "EXCLUSIVE" - ); - }; - - let db1 = SlashingDatabase::create(&file).unwrap(); - check(&db1); - drop(db1); - let db2 = SlashingDatabase::open(&file).unwrap(); - check(&db2); - } - - #[test] - fn test_transaction_failure() { - let dir = tempdir().unwrap(); - let file = dir.path().join("db.sqlite"); - let db = SlashingDatabase::create(&file).unwrap(); - - db.with_transaction(|_| { - db.test_transaction().unwrap_err(); - Ok::<(), NotSafe>(()) - }) - .unwrap(); - } -} diff --git a/validator_client/slashing_protection/src/test_utils.rs b/validator_client/slashing_protection/src/test_utils.rs deleted file mode 100644 index 8cbca12a10b..00000000000 --- a/validator_client/slashing_protection/src/test_utils.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::*; -use tempfile::{tempdir, TempDir}; -use types::{test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader}; - -pub const DEFAULT_VALIDATOR_INDEX: usize = 0; -pub const DEFAULT_DOMAIN: Hash256 = Hash256::ZERO; -pub const DEFAULT_GENESIS_VALIDATORS_ROOT: Hash256 = Hash256::ZERO; - -pub fn pubkey(index: usize) -> PublicKeyBytes { - generate_deterministic_keypair(index).pk.compress() -} - -pub struct Test { - pubkey: PublicKeyBytes, - data: T, - domain: Hash256, - expected: Result, -} - -impl Test { - pub fn single(data: T) -> Self { - Self::with_pubkey(pubkey(DEFAULT_VALIDATOR_INDEX), data) - } - - pub fn with_pubkey(pubkey: PublicKeyBytes, data: T) -> Self { - Self { - pubkey, - data, - domain: DEFAULT_DOMAIN, - expected: Ok(Safe::Valid), - } - } - - pub fn with_domain(mut self, domain: Hash256) -> Self { - self.domain = domain; - self - } - - pub fn expect_result(mut self, result: Result) -> Self { - self.expected = result; - self - } - - pub fn expect_invalid_att(self, error: InvalidAttestation) -> Self { - self.expect_result(Err(NotSafe::InvalidAttestation(error))) - } - - pub fn expect_invalid_block(self, error: InvalidBlock) -> Self { - self.expect_result(Err(NotSafe::InvalidBlock(error))) - } - - pub fn expect_same_data(self) -> Self { - self.expect_result(Ok(Safe::SameData)) - } -} - -pub struct StreamTest { - /// Validators to register. - pub registered_validators: Vec, - /// Vector of cases and the value expected when calling `check_and_insert_X`. - pub cases: Vec>, -} - -impl Default for StreamTest { - fn default() -> Self { - Self { - registered_validators: vec![pubkey(DEFAULT_VALIDATOR_INDEX)], - cases: vec![], - } - } -} - -impl StreamTest { - pub fn run(&self) { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - for pubkey in &self.registered_validators { - slashing_db.register_validator(*pubkey).unwrap(); - } - - check_registration_invariants(&slashing_db, &self.registered_validators); - - for (i, test) in self.cases.iter().enumerate() { - assert_eq!( - slashing_db.check_and_insert_attestation(&test.pubkey, &test.data, test.domain), - test.expected, - "attestation {} not processed as expected", - i - ); - } - - roundtrip_database(&dir, &slashing_db, self.registered_validators.is_empty()); - } -} - -impl StreamTest { - pub fn run(&self) { - let dir = tempdir().unwrap(); - let slashing_db_file = dir.path().join("slashing_protection.sqlite"); - let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); - - for pubkey in &self.registered_validators { - slashing_db.register_validator(*pubkey).unwrap(); - } - - check_registration_invariants(&slashing_db, &self.registered_validators); - - for (i, test) in self.cases.iter().enumerate() { - assert_eq!( - slashing_db.check_and_insert_block_proposal(&test.pubkey, &test.data, test.domain), - test.expected, - "attestation {} not processed as expected", - i - ); - } - - roundtrip_database(&dir, &slashing_db, self.registered_validators.is_empty()); - } -} - -// This function roundtrips the database, but applies minification in order to be compatible with -// the implicit minification done on import. -fn roundtrip_database(dir: &TempDir, db: &SlashingDatabase, is_empty: bool) { - let exported = db - .export_all_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT) - .unwrap(); - let new_db = - SlashingDatabase::create(&dir.path().join("roundtrip_slashing_protection.sqlite")).unwrap(); - new_db - .import_interchange_info(exported.clone(), DEFAULT_GENESIS_VALIDATORS_ROOT) - .unwrap(); - let reexported = new_db - .export_all_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT) - .unwrap(); - - assert!(exported - .minify() - .unwrap() - .equiv(&reexported.minify().unwrap())); - assert_eq!(is_empty, exported.is_empty()); -} - -fn check_registration_invariants( - slashing_db: &SlashingDatabase, - registered_validators: &[PublicKeyBytes], -) { - slashing_db - .check_validator_registrations(registered_validators.iter()) - .unwrap(); - let registered_list = slashing_db - .with_transaction(|txn| slashing_db.list_all_registered_validators(txn)) - .unwrap() - .into_iter() - .map(|(_, pubkey)| pubkey) - .collect::>(); - assert_eq!(registered_validators, registered_list); -} diff --git a/validator_client/slashing_protection/tests/interop.rs b/validator_client/slashing_protection/tests/interop.rs deleted file mode 100644 index c32aab55a26..00000000000 --- a/validator_client/slashing_protection/tests/interop.rs +++ /dev/null @@ -1,51 +0,0 @@ -use slashing_protection::interchange_test::MultiTestCase; -use std::fs::File; -use std::path::PathBuf; -use std::sync::LazyLock; - -pub static TEST_ROOT_DIR: LazyLock = LazyLock::new(test_root_dir); - -fn download_tests() { - let make_output = std::process::Command::new("make") - .current_dir(std::env::var("CARGO_MANIFEST_DIR").unwrap()) - .output() - .expect("need `make` to succeed to download and untar slashing protection tests"); - if !make_output.status.success() { - eprintln!("{}", String::from_utf8_lossy(&make_output.stderr)); - panic!("Running `make` for slashing protection tests failed, see above"); - } -} - -fn test_root_dir() -> PathBuf { - download_tests(); - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("interchange-tests") - .join("tests") -} - -// NOTE: I've combined two tests together to avoid a race-condition which occurs when fighting over -// which test builds the TEST_ROOT_DIR lazy static. -#[test] -fn generated_and_with_minification() { - for entry in TEST_ROOT_DIR - .join("generated") - .read_dir() - .unwrap() - .map(Result::unwrap) - { - let file = File::open(entry.path()).unwrap(); - let test_case: MultiTestCase = serde_json::from_reader(&file).unwrap(); - test_case.run(false); - } - - for entry in TEST_ROOT_DIR - .join("generated") - .read_dir() - .unwrap() - .map(Result::unwrap) - { - let file = File::open(entry.path()).unwrap(); - let test_case: MultiTestCase = serde_json::from_reader(&file).unwrap(); - test_case.run(true); - } -} diff --git a/validator_client/slashing_protection/tests/main.rs b/validator_client/slashing_protection/tests/main.rs deleted file mode 100644 index 5b66bd87e61..00000000000 --- a/validator_client/slashing_protection/tests/main.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod interop; -mod migration; diff --git a/validator_client/slashing_protection/tests/migration.rs b/validator_client/slashing_protection/tests/migration.rs deleted file mode 100644 index 3d4ec7ea9a8..00000000000 --- a/validator_client/slashing_protection/tests/migration.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Tests for upgrading a previous version of the database to the latest schema. -use slashing_protection::{NotSafe, SlashingDatabase}; -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; -use tempfile::tempdir; -use types::{FixedBytesExtended, Hash256}; - -fn test_data_dir() -> PathBuf { - Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("migration-tests") -} - -/// Copy `filename` from the test data dir to the temporary `dest` for testing. -fn make_copy(filename: &str, dest: &Path) -> PathBuf { - let source_file = test_data_dir().join(filename); - let dest_file = dest.join(filename); - fs::copy(source_file, &dest_file).unwrap(); - dest_file -} - -#[test] -fn add_enabled_column() { - let tmp = tempdir().unwrap(); - - let path = make_copy("v0_no_enabled_column.sqlite", tmp.path()); - let num_expected_validators = 5; - - // Database should open without errors, indicating successfull application of migrations. - // The input file has no `enabled` column, which should get added when opening it here. - let db = SlashingDatabase::open(&path).unwrap(); - - // Check that exporting an interchange file lists all the validators. - let interchange = db.export_all_interchange_info(Hash256::zero()).unwrap(); - assert_eq!(interchange.data.len(), num_expected_validators); - - db.with_transaction(|txn| { - // Check that all the validators are enabled and unique. - let uniq_validator_ids = interchange - .data - .iter() - .map(|data| { - let (validator_id, enabled) = db - .get_validator_id_with_status(txn, &data.pubkey) - .unwrap() - .unwrap(); - assert!(enabled); - (validator_id, data.pubkey) - }) - .collect::>(); - - assert_eq!(uniq_validator_ids.len(), num_expected_validators); - - // Check that we can disable them all. - for (&validator_id, pubkey) in &uniq_validator_ids { - db.update_validator_status(txn, validator_id, false) - .unwrap(); - let (loaded_id, enabled) = db - .get_validator_id_with_status(txn, pubkey) - .unwrap() - .unwrap(); - assert_eq!(validator_id, loaded_id); - assert!(!enabled); - } - - Ok::<_, NotSafe>(()) - }) - .unwrap(); -} diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 73dcb793dc9..f27cc5b4a15 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -3,11 +3,12 @@ pub mod config; use crate::cli::ValidatorClient; pub use config::Config; +use eip_3076::SlashingDatabase; +use eip_3076::SLASHING_PROTECTION_FILENAME; use initialized_validators::InitializedValidators; use metrics::set_gauge; use monitoring_api::{MonitoringHttpClient, ProcessType}; use sensitive_url::SensitiveUrl; -use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use account_utils::validator_definitions::ValidatorDefinitions; use beacon_node_fallback::{ 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 c3b551c249b..96dac6cb3b5 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; From 7e7aaf17916f6f734388653c67c2381830178dd2 Mon Sep 17 00:00:00 2001 From: Aliaksei Misiukevich Date: Sun, 7 Sep 2025 19:28:24 +0200 Subject: [PATCH 2/2] preparing eip-3076 Signed-off-by: Aliaksei Misiukevich --- .../http_api/src/aggregate_attestation.rs | 2 +- common/eip_3076/Cargo.toml | 30 + common/eip_3076/Makefile | 43 + .../.github/workflows/check.yaml | 15 + common/eip_3076/interchange-tests/.gitignore | 1 + common/eip_3076/interchange-tests/Makefile | 9 + common/eip_3076/interchange-tests/README.md | 140 ++ common/eip_3076/interchange-tests/schema.json | 94 ++ .../schema_validator/.gitignore | 2 + .../schema_validator/Cargo.toml | 9 + .../schema_validator/src/main.rs | 44 + .../duplicate_pubkey_not_slashable.json | 93 ++ ...uplicate_pubkey_slashable_attestation.json | 74 + .../duplicate_pubkey_slashable_block.json | 63 + ...anges_multiple_validators_repeat_idem.json | 183 +++ ...es_overlapping_validators_merge_stale.json | 234 +++ ...es_overlapping_validators_repeat_idem.json | 217 +++ ...es_single_validator_fail_iff_imported.json | 70 + ...ngle_validator_first_surrounds_second.json | 95 ++ ...alidator_multiple_blocks_out_of_order.json | 82 ++ ...ngle_validator_second_surrounds_first.json | 95 ++ ...gle_validator_single_att_out_of_order.json | 87 ++ ...e_validator_single_block_out_of_order.json | 60 + ...s_single_validator_single_message_gap.json | 123 ++ ...tors_multiple_blocks_and_attestations.json | 287 ++++ .../multiple_validators_same_slot_blocks.json | 66 + .../single_validator_genesis_attestation.json | 39 + .../single_validator_import_only.json | 34 + ...gle_validator_multiple_block_attempts.json | 57 + ...ator_multiple_blocks_and_attestations.json | 165 +++ ...e_validator_out_of_order_attestations.json | 67 + .../single_validator_out_of_order_blocks.json | 54 + .../single_validator_resign_attestation.json | 72 + .../single_validator_resign_block.json | 102 ++ .../single_validator_single_attestation.json | 71 + .../single_validator_single_block.json | 58 + ...alidator_single_block_and_attestation.json | 104 ++ ...le_block_and_attestation_signing_root.json | 36 + ...or_slashable_attestations_double_vote.json | 36 + ...e_attestations_surrounded_by_existing.json | 34 + ...hable_attestations_surrounds_existing.json | 34 + .../single_validator_slashable_blocks.json | 34 + ...le_validator_slashable_blocks_no_root.json | 32 + ..._validator_source_greater_than_target.json | 30 + ...ter_than_target_sensible_iff_minified.json | 51 + ...source_greater_than_target_surrounded.json | 39 + ...ource_greater_than_target_surrounding.json | 39 + ..._validator_two_blocks_no_signing_root.json | 40 + .../wrong_genesis_validators_root.json | 19 + .../v0_no_enabled_column.sqlite | Bin 0 -> 28672 bytes common/eip_3076/src/attestation_tests.rs | 389 +++++ common/eip_3076/src/bin/test_generator.rs | 554 ++++++++ common/eip_3076/src/block_tests.rs | 124 ++ .../eip_3076/src/extra_interchange_tests.rs | 76 + common/eip_3076/src/interchange.rs | 159 +++ common/eip_3076/src/interchange_test.rs | 323 +++++ common/eip_3076/src/lib.rs | 149 ++ common/eip_3076/src/parallel_tests.rs | 82 ++ common/eip_3076/src/registration_tests.rs | 74 + common/eip_3076/src/signed_attestation.rs | 62 + common/eip_3076/src/signed_block.rs | 36 + common/eip_3076/src/slashing_database.rs | 1251 +++++++++++++++++ common/eip_3076/src/test_utils.rs | 159 +++ common/eip_3076/tests/interop.rs | 51 + common/eip_3076/tests/main.rs | 2 + common/eip_3076/tests/migration.rs | 68 + 66 files changed, 7023 insertions(+), 1 deletion(-) create mode 100644 common/eip_3076/Cargo.toml create mode 100644 common/eip_3076/Makefile create mode 100644 common/eip_3076/interchange-tests/.github/workflows/check.yaml create mode 100644 common/eip_3076/interchange-tests/.gitignore create mode 100644 common/eip_3076/interchange-tests/Makefile create mode 100644 common/eip_3076/interchange-tests/README.md create mode 100644 common/eip_3076/interchange-tests/schema.json create mode 100644 common/eip_3076/interchange-tests/schema_validator/.gitignore create mode 100644 common/eip_3076/interchange-tests/schema_validator/Cargo.toml create mode 100644 common/eip_3076/interchange-tests/schema_validator/src/main.rs create mode 100644 common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_not_slashable.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_slashable_attestation.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_slashable_block.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_multiple_validators_repeat_idem.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_overlapping_validators_merge_stale.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_overlapping_validators_repeat_idem.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_fail_iff_imported.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_first_surrounds_second.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_multiple_blocks_out_of_order.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_second_surrounds_first.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_att_out_of_order.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_block_out_of_order.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_message_gap.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_validators_multiple_blocks_and_attestations.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/multiple_validators_same_slot_blocks.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_genesis_attestation.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_import_only.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_multiple_block_attempts.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_multiple_blocks_and_attestations.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_out_of_order_attestations.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_out_of_order_blocks.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_resign_attestation.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_resign_block.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_single_attestation.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_single_block.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_single_block_and_attestation.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_single_block_and_attestation_signing_root.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_double_vote.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_surrounded_by_existing.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_surrounds_existing.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_blocks.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_blocks_no_root.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_sensible_iff_minified.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_surrounded.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_surrounding.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/single_validator_two_blocks_no_signing_root.json create mode 100644 common/eip_3076/interchange-tests/tests/generated/wrong_genesis_validators_root.json create mode 100644 common/eip_3076/migration-tests/v0_no_enabled_column.sqlite create mode 100644 common/eip_3076/src/attestation_tests.rs create mode 100644 common/eip_3076/src/bin/test_generator.rs create mode 100644 common/eip_3076/src/block_tests.rs create mode 100644 common/eip_3076/src/extra_interchange_tests.rs create mode 100644 common/eip_3076/src/interchange.rs create mode 100644 common/eip_3076/src/interchange_test.rs create mode 100644 common/eip_3076/src/lib.rs create mode 100644 common/eip_3076/src/parallel_tests.rs create mode 100644 common/eip_3076/src/registration_tests.rs create mode 100644 common/eip_3076/src/signed_attestation.rs create mode 100644 common/eip_3076/src/signed_block.rs create mode 100644 common/eip_3076/src/slashing_database.rs create mode 100644 common/eip_3076/src/test_utils.rs create mode 100644 common/eip_3076/tests/interop.rs create mode 100644 common/eip_3076/tests/main.rs create mode 100644 common/eip_3076/tests/migration.rs diff --git a/beacon_node/http_api/src/aggregate_attestation.rs b/beacon_node/http_api/src/aggregate_attestation.rs index 809f381139a..d62b3a0a4ac 100644 --- a/beacon_node/http_api/src/aggregate_attestation.rs +++ b/beacon_node/http_api/src/aggregate_attestation.rs @@ -63,6 +63,6 @@ pub fn get_aggregate_attestation( } else if endpoint_version == V1 { Ok(warp::reply::json(&GenericResponse::from(aggregate_attestation)).into_response()) } else { - return Err(unsupported_version_rejection(endpoint_version)); + Err(unsupported_version_rejection(endpoint_version)) } } diff --git a/common/eip_3076/Cargo.toml b/common/eip_3076/Cargo.toml new file mode 100644 index 00000000000..688a7237116 --- /dev/null +++ b/common/eip_3076/Cargo.toml @@ -0,0 +1,30 @@ +[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 } +filesystem = { workspace = true } +r2d2 = { workspace = true } +r2d2_sqlite = "0.21.0" +rusqlite = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tempfile = { workspace = true } +tracing = { workspace = true } +types = { workspace = true } + +[dev-dependencies] +rayon = { workspace = true } + +[[test]] +name = "eip_3076_tests" +path = "tests/main.rs" 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/common/eip_3076/interchange-tests/.github/workflows/check.yaml b/common/eip_3076/interchange-tests/.github/workflows/check.yaml new file mode 100644 index 00000000000..1da42929aa8 --- /dev/null +++ b/common/eip_3076/interchange-tests/.github/workflows/check.yaml @@ -0,0 +1,15 @@ +name: schema checks + +on: + push: + pull_request: + +jobs: + check_schema: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - run: make check diff --git a/common/eip_3076/interchange-tests/.gitignore b/common/eip_3076/interchange-tests/.gitignore new file mode 100644 index 00000000000..335ec9573de --- /dev/null +++ b/common/eip_3076/interchange-tests/.gitignore @@ -0,0 +1 @@ +*.tar.gz diff --git a/common/eip_3076/interchange-tests/Makefile b/common/eip_3076/interchange-tests/Makefile new file mode 100644 index 00000000000..e6f89c94c19 --- /dev/null +++ b/common/eip_3076/interchange-tests/Makefile @@ -0,0 +1,9 @@ +VERSION:=$(shell git describe --tags) + +check: + cargo run --manifest-path schema_validator/Cargo.toml --release -- schema.json tests/generated + +# Build a tarball for a release. +# To make a new release: `git tag -s vX.Y.Z` +release: + tar -czvf eip-3076-tests-$(VERSION).tar.gz tests/ diff --git a/common/eip_3076/interchange-tests/README.md b/common/eip_3076/interchange-tests/README.md new file mode 100644 index 00000000000..4343b2455b8 --- /dev/null +++ b/common/eip_3076/interchange-tests/README.md @@ -0,0 +1,140 @@ +# Slashing Protection Interchange Tests (EIP-3076) + +Tests for EIP-3076: + +https://eips.ethereum.org/EIPS/eip-3076 + +Discussion: + +https://ethereum-magicians.org/t/eip-3076-validator-client-interchange-format-slashing-protection/4883 + +## How to run + +Each test directory contains an interchange file and some extra data about how to test it. + +For example: + +```json +{ + "name": "single_validator_genesis_attestation", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "0" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "0", + "should_succeed": false + } + ] + } + ] +} +``` + +To run a test, first initialize a new (empty) slashing protection database. + +Then for each entry in `steps`, import the `interchange`, process the `blocks` and `attestations`, +and continue to the next step. + +Determine the test outcome according to the meanings of each of the fields, +which are as follows: + +* `name: string`: the name of the test-case, informational. +* `genesis_validators_root: Root`: the genesis validators root to use when + creating the empty slashing protection database, or to compare the import + against. +* `steps[i].should_succeed: bool`: whether the `steps[i].interchange` given is valid and should + be imported successfully. +* `steps[i].contains_slashable_data: bool`: whether the `steps[i].interchange` contains some + slashable data with respect to itself or the existing contents of the database. +* `steps[i].interchange: Interchange`: slashing protection interchange data as described + by the spec. +* `steps[i].blocks: [object]`: a list of block signings to be attempted **after** + importing the `interchange`, detailed below. +* `steps[i].attestations: [object]`: a list of attestation signings to be attempted **after** + importing the `interchange`, detailed below. + +Each block in `blocks` is structured as: + +```json +{ + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true +} +``` + +Your test-runner should attempt to sign a block with `signing_root` at the given slot from the given +`pubkey`. The `should_succeed` field describes whether this signing should be accepted (true) or +rejected (false) _by a client using a minimal strategy_. Clients using a complete strategy should +instead use the `should_succeed_complete` field which allows signing to succeed in more cases. If +the block is signed successfully it should be incorporated into the slashing protection database. + +Each attestation in `attestations` is structured as: + +```json +{ + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "11", + "target_epoch": "12", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true +} +``` + +Similarly to above, your test-runner should attempt to sign an attestation with these parameters +using the given `pubkey`, and succeed based on the value of +`should_succeed`/`should_succeed_complete`. Again, implementations that use the _complete_ strategy +should use `should_succeed_complete`. All implementations should incorporate signed attestations +into the database. + +Note that the top-level `genesis_validators_root` is not necessarily the same +as the GVR contained in the interchange, to allow us to test the case where +they are mismatched. + +## Handling Slashable Data + +The `contains_slashable_data` parameter is to be interpreted as follows: + +- If `should_succeed` is false, then `contains_slashable_data` is irrelevant +- If `contains_slashable_data` is false, then the given interchange **must** be imported + successfully, and the given block/attestation checks must pass. +- If `contains_slashable_data` is true, then implementations have the option to do one of two + things: + - Import the interchange successfully, working around the slashable data by minification + or some other mechanism. If the import succeeds, all checks must pass and the test + should continue to the next step. + - Reject the interchange (or partially import it), in which case the block/attestation + checks and all future steps should be ignored. + +## Downloading the tests + +The `tests` directory is released as a versioned `.tar.gz` on the [Releases](https://github.com/eth-clients/slashing-protection-interchange-tests/releases) page. + +Alternatively, you could use a git submodule. diff --git a/common/eip_3076/interchange-tests/schema.json b/common/eip_3076/interchange-tests/schema.json new file mode 100644 index 00000000000..ad41a50b28c --- /dev/null +++ b/common/eip_3076/interchange-tests/schema.json @@ -0,0 +1,94 @@ +{ + "title": "Signing history", + "description": "This schema provides a record of the blocks and attestations signed by a set of validators", + "type": "object", + "properties": { + "metadata": { + "type": "object", + "properties": { + "interchange_format_version": { + "type": "string", + "description": "The version of the interchange format that this document adheres to" + }, + "genesis_validators_root": { + "type": "string", + "description": "Calculated at Genesis time; serves to uniquely identify the chain" + } + }, + "required": [ + "interchange_format_version", + "genesis_validators_root" + ] + }, + "data": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "pubkey": { + "type": "string", + "description": "The BLS public key of the validator (encoded as a 0x-prefixed hex string)" + }, + "signed_blocks": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "slot": { + "type": "string", + "description": "The slot number of the block that was signed" + }, + "signing_root": { + "type": "string", + "description": "The output of compute_signing_root(block, domain)" + } + }, + "required": [ + "slot" + ] + } + ] + }, + "signed_attestations": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "source_epoch": { + "type": "string", + "description": "The attestation.data.source.epoch of the signed attestation" + }, + "target_epoch": { + "type": "string", + "description": "The attestation.data.target.epoch of the signed attestation" + }, + "signing_root": { + "type": "string", + "description": "The output of compute_signing_root(attestation, domain)" + } + }, + "required": [ + "source_epoch", + "target_epoch" + ] + } + ] + } + }, + "required": [ + "pubkey", + "signed_blocks", + "signed_attestations" + ] + } + ] + } + }, + "required": [ + "metadata", + "data" + ] +} diff --git a/common/eip_3076/interchange-tests/schema_validator/.gitignore b/common/eip_3076/interchange-tests/schema_validator/.gitignore new file mode 100644 index 00000000000..96ef6c0b944 --- /dev/null +++ b/common/eip_3076/interchange-tests/schema_validator/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/common/eip_3076/interchange-tests/schema_validator/Cargo.toml b/common/eip_3076/interchange-tests/schema_validator/Cargo.toml new file mode 100644 index 00000000000..95743c156f8 --- /dev/null +++ b/common/eip_3076/interchange-tests/schema_validator/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "schema_validator" +version = "0.1.0" +authors = ["Michael Sproul "] +edition = "2018" + +[dependencies] +jsonschema = { version = "0.3.1", default-features = false } +serde_json = "1.0.59" diff --git a/common/eip_3076/interchange-tests/schema_validator/src/main.rs b/common/eip_3076/interchange-tests/schema_validator/src/main.rs new file mode 100644 index 00000000000..48ccc6fbbb9 --- /dev/null +++ b/common/eip_3076/interchange-tests/schema_validator/src/main.rs @@ -0,0 +1,44 @@ +use jsonschema::{Draft, JSONSchema}; +use serde_json::Value; +use std::env; +use std::fs::{self, File}; + +fn main() { + let args: Vec<_> = env::args().collect(); + + let schema_file = File::open(&args[1]).unwrap(); + let schema_value = serde_json::from_reader(schema_file).unwrap(); + let schema = JSONSchema::compile(&schema_value, Some(Draft::Draft7)).unwrap(); + + let tests_dir = &args[2]; + + let mut success_all = true; + + fs::read_dir(tests_dir) + .expect("read_dir succeeds on test directory") + .map(|e| e.unwrap().path()) + .filter(|path| path.is_file()) + .for_each(|path| { + let test_file = File::open(&path).unwrap(); + let test_value: Value = serde_json::from_reader(test_file).unwrap(); + let filename = path.file_name().unwrap().to_str().unwrap(); + + let steps = test_value.get("steps").unwrap(); + let mut success = true; + + for (i, step) in steps.as_array().unwrap().iter().enumerate() { + let interchange_value = step.get("interchange").unwrap(); + if let Err(errors) = schema.validate(interchange_value) { + for e in errors { + println!("{} .steps[{}].interchange, error: {:?}", filename, i, e); + } + success = false; + success_all = false; + } + } + if success { + println!("{}, ok", filename); + } + }); + assert!(success_all, "one or more tests failed, see above"); +} diff --git a/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_not_slashable.json b/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_not_slashable.json new file mode 100644 index 00000000000..aa8e674a6cc --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_not_slashable.json @@ -0,0 +1,93 @@ +{ + "name": "duplicate_pubkey_not_slashable", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "10" + }, + { + "slot": "11" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "2" + } + ] + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "12" + }, + { + "slot": "13" + } + ], + "signed_attestations": [ + { + "source_epoch": "1", + "target_epoch": "3" + } + ] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "13", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "2", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "1", + "target_epoch": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_slashable_attestation.json b/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_slashable_attestation.json new file mode 100644 index 00000000000..4809c0cd0d6 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_slashable_attestation.json @@ -0,0 +1,74 @@ +{ + "name": "duplicate_pubkey_slashable_attestation", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000003" + } + ] + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "1", + "target_epoch": "2" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "2", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "1", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_slashable_block.json b/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_slashable_block.json new file mode 100644 index 00000000000..8b18de40107 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/duplicate_pubkey_slashable_block.json @@ -0,0 +1,63 @@ +{ + "name": "duplicate_pubkey_slashable_block", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "10" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "2" + } + ] + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "10" + } + ], + "signed_attestations": [ + { + "source_epoch": "1", + "target_epoch": "3" + } + ] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "11", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_multiple_validators_repeat_idem.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_multiple_validators_repeat_idem.json new file mode 100644 index 00000000000..09b43600b92 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_multiple_validators_repeat_idem.json @@ -0,0 +1,183 @@ +{ + "name": "multiple_interchanges_multiple_validators_repeat_idem", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "2" + }, + { + "slot": "4" + }, + { + "slot": "6" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "1", + "target_epoch": "2" + } + ] + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "signed_blocks": [ + { + "slot": "8" + }, + { + "slot": "10" + }, + { + "slot": "12" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "0", + "target_epoch": "3" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "2" + }, + { + "slot": "4" + }, + { + "slot": "6" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "1", + "target_epoch": "2" + } + ] + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "signed_blocks": [ + { + "slot": "8" + }, + { + "slot": "10" + }, + { + "slot": "12" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "0", + "target_epoch": "3" + } + ] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "0", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "7", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "slot": "0", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "source_epoch": "0", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_overlapping_validators_merge_stale.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_overlapping_validators_merge_stale.json new file mode 100644 index 00000000000..6b57c1a386f --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_overlapping_validators_merge_stale.json @@ -0,0 +1,234 @@ +{ + "name": "multiple_interchanges_overlapping_validators_merge_stale", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "100" + } + ], + "signed_attestations": [ + { + "source_epoch": "12", + "target_epoch": "13" + } + ] + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "signed_blocks": [ + { + "slot": "101" + } + ], + "signed_attestations": [ + { + "source_epoch": "12", + "target_epoch": "13" + } + ] + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "signed_blocks": [ + { + "slot": "4" + } + ], + "signed_attestations": [ + { + "source_epoch": "4", + "target_epoch": "5" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "2" + } + ], + "signed_attestations": [ + { + "source_epoch": "4", + "target_epoch": "5" + } + ] + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "signed_blocks": [ + { + "slot": "3" + } + ], + "signed_attestations": [ + { + "source_epoch": "3", + "target_epoch": "4" + } + ] + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "signed_blocks": [ + { + "slot": "102" + } + ], + "signed_attestations": [ + { + "source_epoch": "12", + "target_epoch": "13" + } + ] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "100", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "slot": "101", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "slot": "102", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "103", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "slot": "104", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "slot": "105", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "12", + "target_epoch": "13", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "11", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "source_epoch": "12", + "target_epoch": "13", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "source_epoch": "11", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "source_epoch": "12", + "target_epoch": "13", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "source_epoch": "11", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "12", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "source_epoch": "13", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "source_epoch": "13", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_overlapping_validators_repeat_idem.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_overlapping_validators_repeat_idem.json new file mode 100644 index 00000000000..8aa95d17473 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_overlapping_validators_repeat_idem.json @@ -0,0 +1,217 @@ +{ + "name": "multiple_interchanges_overlapping_validators_repeat_idem", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "2" + }, + { + "slot": "4" + }, + { + "slot": "6" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "1", + "target_epoch": "2" + } + ] + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "signed_blocks": [ + { + "slot": "8" + }, + { + "slot": "10" + }, + { + "slot": "12" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "0", + "target_epoch": "3" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "2" + }, + { + "slot": "4" + }, + { + "slot": "6" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "1", + "target_epoch": "2" + } + ] + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "signed_blocks": [ + { + "slot": "8" + }, + { + "slot": "10" + }, + { + "slot": "12" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "0", + "target_epoch": "3" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "signed_blocks": [ + { + "slot": "8" + }, + { + "slot": "10" + }, + { + "slot": "12" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "0", + "target_epoch": "3" + } + ] + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "signed_blocks": [ + { + "slot": "8" + }, + { + "slot": "10" + }, + { + "slot": "12" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "0", + "target_epoch": "3" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "source_epoch": "1", + "target_epoch": "2", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "source_epoch": "1", + "target_epoch": "2", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_fail_iff_imported.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_fail_iff_imported.json new file mode 100644 index 00000000000..fba8bad5c39 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_fail_iff_imported.json @@ -0,0 +1,70 @@ +{ + "name": "multiple_interchanges_single_validator_fail_iff_imported", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "40" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "20" + }, + { + "slot": "50" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "50", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_first_surrounds_second.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_first_surrounds_second.json new file mode 100644 index 00000000000..e029d1d5082 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_first_surrounds_second.json @@ -0,0 +1,95 @@ +{ + "name": "multiple_interchanges_single_validator_first_surrounds_second", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "9", + "target_epoch": "21" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "10", + "target_epoch": "20" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "21", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "9", + "target_epoch": "21", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "9", + "target_epoch": "22", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "22", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_multiple_blocks_out_of_order.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_multiple_blocks_out_of_order.json new file mode 100644 index 00000000000..7ce1fd0d8ed --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_multiple_blocks_out_of_order.json @@ -0,0 +1,82 @@ +{ + "name": "multiple_interchanges_single_validator_multiple_blocks_out_of_order", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "0" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "30", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "20" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "29", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_second_surrounds_first.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_second_surrounds_first.json new file mode 100644 index 00000000000..ff27b90ffe0 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_second_surrounds_first.json @@ -0,0 +1,95 @@ +{ + "name": "multiple_interchanges_single_validator_second_surrounds_first", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "10", + "target_epoch": "20" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "9", + "target_epoch": "21" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "21", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "9", + "target_epoch": "21", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "9", + "target_epoch": "22", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "22", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_att_out_of_order.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_att_out_of_order.json new file mode 100644 index 00000000000..850f98d8d5b --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_att_out_of_order.json @@ -0,0 +1,87 @@ +{ + "name": "multiple_interchanges_single_validator_single_att_out_of_order", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "12", + "target_epoch": "13" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "10", + "target_epoch": "11" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "12", + "target_epoch": "13", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "12", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "13", + "target_epoch": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_block_out_of_order.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_block_out_of_order.json new file mode 100644 index 00000000000..1473a5d7afc --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_block_out_of_order.json @@ -0,0 +1,60 @@ +{ + "name": "multiple_interchanges_single_validator_single_block_out_of_order", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "40" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "20" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_message_gap.json b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_message_gap.json new file mode 100644 index 00000000000..4e4be2269e8 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_interchanges_single_validator_single_message_gap.json @@ -0,0 +1,123 @@ +{ + "name": "multiple_interchanges_single_validator_single_message_gap", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "40" + } + ], + "signed_attestations": [ + { + "source_epoch": "2", + "target_epoch": "30" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + }, + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "50" + } + ], + "signed_attestations": [ + { + "source_epoch": "10", + "target_epoch": "50" + } + ] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "41", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "45", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "49", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "50", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "51", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "3", + "target_epoch": "31", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "9", + "target_epoch": "49", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "51", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_validators_multiple_blocks_and_attestations.json b/common/eip_3076/interchange-tests/tests/generated/multiple_validators_multiple_blocks_and_attestations.json new file mode 100644 index 00000000000..06215aa587c --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_validators_multiple_blocks_and_attestations.json @@ -0,0 +1,287 @@ +{ + "name": "multiple_validators_multiple_blocks_and_attestations", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "10" + }, + { + "slot": "15" + }, + { + "slot": "20" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "0", + "target_epoch": "2" + }, + { + "source_epoch": "1", + "target_epoch": "3" + }, + { + "source_epoch": "2", + "target_epoch": "4" + }, + { + "source_epoch": "4", + "target_epoch": "5" + } + ] + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "signed_blocks": [ + { + "slot": "3" + }, + { + "slot": "4" + }, + { + "slot": "100" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "0" + }, + { + "source_epoch": "0", + "target_epoch": "1" + }, + { + "source_epoch": "1", + "target_epoch": "2" + }, + { + "source_epoch": "2", + "target_epoch": "5" + }, + { + "source_epoch": "5", + "target_epoch": "6" + } + ] + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "signed_blocks": [ + { + "slot": "10" + }, + { + "slot": "15" + }, + { + "slot": "20" + } + ], + "signed_attestations": [ + { + "source_epoch": "1", + "target_epoch": "2" + }, + { + "source_epoch": "1", + "target_epoch": "3" + }, + { + "source_epoch": "2", + "target_epoch": "4" + } + ] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "9", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "21", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "11", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "slot": "2", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "slot": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "slot": "0", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "slot": "101", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "slot": "9", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "slot": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "slot": "22", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "5", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "3", + "target_epoch": "6", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "4", + "target_epoch": "6", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "5", + "target_epoch": "7", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "6", + "target_epoch": "8", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "source_epoch": "1", + "target_epoch": "7", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "source_epoch": "1", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "source_epoch": "5", + "target_epoch": "7", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "source_epoch": "0", + "target_epoch": "0", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "source_epoch": "0", + "target_epoch": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "source_epoch": "2", + "target_epoch": "5", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/multiple_validators_same_slot_blocks.json b/common/eip_3076/interchange-tests/tests/generated/multiple_validators_same_slot_blocks.json new file mode 100644 index 00000000000..c79e8c9c99f --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/multiple_validators_same_slot_blocks.json @@ -0,0 +1,66 @@ +{ + "name": "multiple_validators_same_slot_blocks", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "slot": "2", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "slot": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "signed_attestations": [] + }, + { + "pubkey": "0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b", + "signed_blocks": [ + { + "slot": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + { + "slot": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + ], + "signed_attestations": [] + }, + { + "pubkey": "0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b", + "signed_blocks": [ + { + "slot": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + { + "slot": "2", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000002" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_genesis_attestation.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_genesis_attestation.json new file mode 100644 index 00000000000..852f88ff22b --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_genesis_attestation.json @@ -0,0 +1,39 @@ +{ + "name": "single_validator_genesis_attestation", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "0" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "0", + "target_epoch": "0", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_import_only.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_import_only.json new file mode 100644 index 00000000000..fa2f51272ae --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_import_only.json @@ -0,0 +1,34 @@ +{ + "name": "single_validator_import_only", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "22" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "2" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_multiple_block_attempts.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_multiple_block_attempts.json new file mode 100644 index 00000000000..a71e4bf1c8a --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_multiple_block_attempts.json @@ -0,0 +1,57 @@ +{ + "name": "single_validator_multiple_block_attempts", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "15" + }, + { + "slot": "16" + }, + { + "slot": "17" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "16", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "16", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000001", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "16", + "signing_root": "0x000000000000000000000000000000000000000000000000ffffffffffffffff", + "should_succeed": false, + "should_succeed_complete": false + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_multiple_blocks_and_attestations.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_multiple_blocks_and_attestations.json new file mode 100644 index 00000000000..5b7b29c7345 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_multiple_blocks_and_attestations.json @@ -0,0 +1,165 @@ +{ + "name": "single_validator_multiple_blocks_and_attestations", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "2" + }, + { + "slot": "3" + }, + { + "slot": "10" + }, + { + "slot": "1200" + } + ], + "signed_attestations": [ + { + "source_epoch": "10", + "target_epoch": "11" + }, + { + "source_epoch": "12", + "target_epoch": "13" + }, + { + "source_epoch": "20", + "target_epoch": "24" + } + ] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "2", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "1200", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "256", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "1201", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "9", + "target_epoch": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "12", + "target_epoch": "13", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "11", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "21", + "target_epoch": "22", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "10", + "target_epoch": "24", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "11", + "target_epoch": "12", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "20", + "target_epoch": "25", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_out_of_order_attestations.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_out_of_order_attestations.json new file mode 100644 index 00000000000..ff31d4ce661 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_out_of_order_attestations.json @@ -0,0 +1,67 @@ +{ + "name": "single_validator_out_of_order_attestations", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "4", + "target_epoch": "5" + }, + { + "source_epoch": "3", + "target_epoch": "4" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "3", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "4", + "target_epoch": "5", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "1", + "target_epoch": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "3", + "target_epoch": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_out_of_order_blocks.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_out_of_order_blocks.json new file mode 100644 index 00000000000..ae9706b2f41 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_out_of_order_blocks.json @@ -0,0 +1,54 @@ +{ + "name": "single_validator_out_of_order_blocks", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "6" + }, + { + "slot": "5" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "5", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "6", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "7", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_resign_attestation.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_resign_attestation.json new file mode 100644 index 00000000000..4c2a0324871 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_resign_attestation.json @@ -0,0 +1,72 @@ +{ + "name": "single_validator_resign_attestation", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "5", + "target_epoch": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000203" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "5", + "target_epoch": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "5", + "target_epoch": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000001", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "5", + "target_epoch": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000203", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "6", + "target_epoch": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000267", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "5", + "target_epoch": "14", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000203", + "should_succeed": false, + "should_succeed_complete": false + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_resign_block.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_resign_block.json new file mode 100644 index 00000000000..a44b374ef17 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_resign_block.json @@ -0,0 +1,102 @@ +{ + "name": "single_validator_resign_block", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000097" + }, + { + "slot": "16", + "signing_root": "0x00000000000000000000000000000000000000000000000000000000000000a1" + }, + { + "slot": "17", + "signing_root": "0x00000000000000000000000000000000000000000000000000000000000000ab" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000097", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "16", + "signing_root": "0x00000000000000000000000000000000000000000000000000000000000000a1", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "17", + "signing_root": "0x00000000000000000000000000000000000000000000000000000000000000ab", + "should_succeed": false, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000098", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "15", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "16", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000097", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "17", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000097", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "18", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000097", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "14", + "signing_root": "0x00000000000000000000000000000000000000000000000000000000000000ab", + "should_succeed": false, + "should_succeed_complete": false + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_single_attestation.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_single_attestation.json new file mode 100644 index 00000000000..664154bc181 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_single_attestation.json @@ -0,0 +1,71 @@ +{ + "name": "single_validator_single_attestation", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "15", + "target_epoch": "20" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "3", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "14", + "target_epoch": "19", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "15", + "target_epoch": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "16", + "target_epoch": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "15", + "target_epoch": "21", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block.json new file mode 100644 index 00000000000..c202665dcb7 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block.json @@ -0,0 +1,58 @@ +{ + "name": "single_validator_single_block", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "32" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "32", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "33", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "31", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block_and_attestation.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block_and_attestation.json new file mode 100644 index 00000000000..a62bd153ad4 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block_and_attestation.json @@ -0,0 +1,104 @@ +{ + "name": "single_validator_single_block_and_attestation", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "32" + } + ], + "signed_attestations": [ + { + "source_epoch": "15", + "target_epoch": "20" + } + ] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "32", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "33", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "31", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "3", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "14", + "target_epoch": "19", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "15", + "target_epoch": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "16", + "target_epoch": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "15", + "target_epoch": "21", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block_and_attestation_signing_root.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block_and_attestation_signing_root.json new file mode 100644 index 00000000000..ac678201cbc --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_single_block_and_attestation_signing_root.json @@ -0,0 +1,36 @@ +{ + "name": "single_validator_single_block_and_attestation_signing_root", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "19", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + ], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000002" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_double_vote.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_double_vote.json new file mode 100644 index 00000000000..8b60e85081d --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_double_vote.json @@ -0,0 +1,36 @@ +{ + "name": "single_validator_slashable_attestations_double_vote", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "2", + "target_epoch": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "source_epoch": "2", + "target_epoch": "3", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_surrounded_by_existing.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_surrounded_by_existing.json new file mode 100644 index 00000000000..3b3870246a4 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_surrounded_by_existing.json @@ -0,0 +1,34 @@ +{ + "name": "single_validator_slashable_attestations_surrounded_by_existing", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "0", + "target_epoch": "4" + }, + { + "source_epoch": "2", + "target_epoch": "3" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_surrounds_existing.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_surrounds_existing.json new file mode 100644 index 00000000000..934b1cdf674 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_attestations_surrounds_existing.json @@ -0,0 +1,34 @@ +{ + "name": "single_validator_slashable_attestations_surrounds_existing", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "2", + "target_epoch": "3" + }, + { + "source_epoch": "0", + "target_epoch": "4" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_blocks.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_blocks.json new file mode 100644 index 00000000000..bc6512f1ef2 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_blocks.json @@ -0,0 +1,34 @@ +{ + "name": "single_validator_slashable_blocks", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "10", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "slot": "10", + "signing_root": "0x000000000000000000000000000000000000000000000000000000000000000b" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_blocks_no_root.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_blocks_no_root.json new file mode 100644 index 00000000000..485f140c447 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_slashable_blocks_no_root.json @@ -0,0 +1,32 @@ +{ + "name": "single_validator_slashable_blocks_no_root", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "10" + }, + { + "slot": "10" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target.json new file mode 100644 index 00000000000..db4ac97b5b5 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target.json @@ -0,0 +1,30 @@ +{ + "name": "single_validator_source_greater_than_target", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "8", + "target_epoch": "7" + } + ] + } + ] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_sensible_iff_minified.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_sensible_iff_minified.json new file mode 100644 index 00000000000..1d8cb9501c3 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_sensible_iff_minified.json @@ -0,0 +1,51 @@ +{ + "name": "single_validator_source_greater_than_target_sensible_iff_minified", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "5", + "target_epoch": "2" + }, + { + "source_epoch": "6", + "target_epoch": "7" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "5", + "target_epoch": "8", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + }, + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "6", + "target_epoch": "8", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": true, + "should_succeed_complete": true + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_surrounded.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_surrounded.json new file mode 100644 index 00000000000..e95130d1c73 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_surrounded.json @@ -0,0 +1,39 @@ +{ + "name": "single_validator_source_greater_than_target_surrounded", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "5", + "target_epoch": "2" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "6", + "target_epoch": "1", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_surrounding.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_surrounding.json new file mode 100644 index 00000000000..135dbccbdd8 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_source_greater_than_target_surrounding.json @@ -0,0 +1,39 @@ +{ + "name": "single_validator_source_greater_than_target_surrounding", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": true, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [], + "signed_attestations": [ + { + "source_epoch": "5", + "target_epoch": "2" + } + ] + } + ] + }, + "blocks": [], + "attestations": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "source_epoch": "3", + "target_epoch": "4", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/single_validator_two_blocks_no_signing_root.json b/common/eip_3076/interchange-tests/tests/generated/single_validator_two_blocks_no_signing_root.json new file mode 100644 index 00000000000..3dd77bab760 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/single_validator_two_blocks_no_signing_root.json @@ -0,0 +1,40 @@ +{ + "name": "single_validator_two_blocks_no_signing_root", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "steps": [ + { + "should_succeed": true, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "signed_blocks": [ + { + "slot": "10" + }, + { + "slot": "20" + } + ], + "signed_attestations": [] + } + ] + }, + "blocks": [ + { + "pubkey": "0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c", + "slot": "20", + "signing_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "should_succeed": false, + "should_succeed_complete": false + } + ], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/interchange-tests/tests/generated/wrong_genesis_validators_root.json b/common/eip_3076/interchange-tests/tests/generated/wrong_genesis_validators_root.json new file mode 100644 index 00000000000..3cf5e2a0a81 --- /dev/null +++ b/common/eip_3076/interchange-tests/tests/generated/wrong_genesis_validators_root.json @@ -0,0 +1,19 @@ +{ + "name": "wrong_genesis_validators_root", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000001", + "steps": [ + { + "should_succeed": false, + "contains_slashable_data": false, + "interchange": { + "metadata": { + "interchange_format_version": "5", + "genesis_validators_root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "data": [] + }, + "blocks": [], + "attestations": [] + } + ] +} diff --git a/common/eip_3076/migration-tests/v0_no_enabled_column.sqlite b/common/eip_3076/migration-tests/v0_no_enabled_column.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..5a95fe36e6e4e3058d08851e7148e195984e25c9 GIT binary patch literal 28672 zcmeI)&u$w<7y$77>%@(m;g;)00ymK=G_y0iJA;Ho8f~knZbIBtq9-%6GZUs#g3vVG}P zRKIt3uQDBTr&oP&7?rGVY$fYATdmXK1G%?t2G1Qn>}hv*yR+Bp4U)^Pjmu};`pQPT zT)VoSTq|!a9q;>c(k`!*?Q(s!ypcRP(O>H766zim$ll$ zE$VySU2X35KiYANtd6`p=#BO}6QDa}!oivQtKrPT#f$!D~+Z9X|Uxs&lcJTX0&X;53Zy7;Wo z9rdR}j_Sj;{Os^krQu$2xyZ2N*2oSlp4+}UMOB@!82uO#e_A2%8F{DD{FDC ztt=?bX`U&~WQy}#VIw&a+8E;QK_#=Mph6Q>5gQ(iM^!XWF>%+)l+j!e!_rFY)VfZ) zzl;}z6{%3la>Iq-lxLPGBHb9dDX_$bx$ZHM7g|}G8zu9i5|wg2!#FQ&aL|sCx#9xL zf?HPMtjH^_mFqn(3fBRGQ;}(sD_TijSnDL6jV8uj#-*?}!;}!rDq9F=pE+`q=BW{G zEW9GrSpT3MOe^L3&uo=Z;;J&I{M223&UyVmdj5~z^3X5n zd-Nqzt_lSNKmY_l00ck)1V8`;KmY_l00cnbi9j=se7_aPD^p4{8pcJ@l(3a%IE)L! zsd2$DE(oT@z3xZ#dTUzL==neT)kFWGztNxQH&=xM0w4eaAOHd&00JNY0w4eaAOHd& z@bU=ExbF%4IF3i#D2hhgFbqfAAP6G&{Xl*E{9kzJXY?KV0!buZo&iEnK>!3m00ck) z1V8`;KmY_l00cnb+y$C(?9a}&j{hM3#5SAJC~o+JduNtrIEo9-400#m2;#bHcUoA} zAKfmV7B_zW|IkDKpuf-`=pp)d+7)mf1V8`;KmY_l00ck)1V8`;KmY_l-~|(?MgFWG X#iLCajW$6TNA+1haR0tATaW(-ZhfP; literal 0 HcmV?d00001 diff --git a/common/eip_3076/src/attestation_tests.rs b/common/eip_3076/src/attestation_tests.rs new file mode 100644 index 00000000000..b577ccd9d85 --- /dev/null +++ b/common/eip_3076/src/attestation_tests.rs @@ -0,0 +1,389 @@ +#![cfg(test)] + +use crate::test_utils::*; +use crate::*; +use types::{AttestationData, Checkpoint, Epoch, FixedBytesExtended, Slot}; + +pub fn build_checkpoint(epoch_num: u64) -> Checkpoint { + Checkpoint { + epoch: Epoch::from(epoch_num), + root: Hash256::zero(), + } +} + +pub fn attestation_data_builder(source: u64, target: u64) -> AttestationData { + let source = build_checkpoint(source); + let target = build_checkpoint(target); + let index = 0u64; + let slot = Slot::from(0u64); + + AttestationData { + slot, + index, + beacon_block_root: Hash256::zero(), + source, + target, + } +} + +/// Create a signed attestation from `attestation`, assuming the default domain. +fn signed_att(attestation: &AttestationData) -> SignedAttestation { + SignedAttestation::from_attestation(attestation, DEFAULT_DOMAIN) +} + +#[test] +fn valid_empty_history() { + StreamTest { + cases: vec![Test::single(attestation_data_builder(2, 3))], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_genesis() { + StreamTest { + cases: vec![Test::single(attestation_data_builder(0, 0))], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_out_of_order_attestation() { + StreamTest { + cases: vec![ + Test::single(attestation_data_builder(0, 3)), + Test::single(attestation_data_builder(2, 5)), + Test::single(attestation_data_builder(1, 4)), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_repeat_attestation() { + StreamTest { + cases: vec![ + Test::single(attestation_data_builder(0, 1)), + Test::single(attestation_data_builder(0, 1)).expect_same_data(), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_source_from_first_entry() { + StreamTest { + cases: vec![ + Test::single(attestation_data_builder(6, 7)), + Test::single(attestation_data_builder(6, 8)), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_multiple_validators_double_vote() { + StreamTest { + registered_validators: vec![pubkey(0), pubkey(1)], + cases: vec![ + Test::with_pubkey(pubkey(0), attestation_data_builder(0, 1)), + Test::with_pubkey(pubkey(1), attestation_data_builder(0, 1)), + ], + } + .run() +} + +#[test] +fn valid_vote_chain_repeat_first() { + StreamTest { + cases: vec![ + Test::single(attestation_data_builder(0, 1)), + Test::single(attestation_data_builder(1, 2)), + Test::single(attestation_data_builder(2, 3)), + Test::single(attestation_data_builder(0, 1)).expect_same_data(), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_vote_chain_repeat_middle() { + StreamTest { + cases: vec![ + Test::single(attestation_data_builder(0, 1)), + Test::single(attestation_data_builder(1, 2)), + Test::single(attestation_data_builder(2, 3)), + Test::single(attestation_data_builder(1, 2)).expect_same_data(), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_vote_chain_repeat_last() { + StreamTest { + cases: vec![ + Test::single(attestation_data_builder(0, 1)), + Test::single(attestation_data_builder(1, 2)), + Test::single(attestation_data_builder(2, 3)), + Test::single(attestation_data_builder(2, 3)).expect_same_data(), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_multiple_validators_not_surrounding() { + // Attestations that would be problematic if they came from the same validator, but are OK + // coming from different validators. + StreamTest { + registered_validators: vec![pubkey(0), pubkey(1)], + cases: vec![ + Test::with_pubkey(pubkey(0), attestation_data_builder(0, 10)), + Test::with_pubkey(pubkey(0), attestation_data_builder(10, 20)), + Test::with_pubkey(pubkey(1), attestation_data_builder(1, 9)), + Test::with_pubkey(pubkey(1), attestation_data_builder(9, 21)), + ], + } + .run() +} + +#[test] +fn invalid_source_exceeds_target() { + StreamTest { + cases: vec![Test::single(attestation_data_builder(1, 0)) + .expect_invalid_att(InvalidAttestation::SourceExceedsTarget)], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_unregistered_validator() { + StreamTest { + registered_validators: vec![], + cases: vec![ + Test::single(attestation_data_builder(2, 3)).expect_result(Err( + NotSafe::UnregisteredValidator(pubkey(DEFAULT_VALIDATOR_INDEX)), + )), + ], + } + .run() +} + +#[test] +fn invalid_double_vote_diff_source() { + let first = attestation_data_builder(0, 2); + StreamTest { + cases: vec![ + Test::single(first.clone()), + Test::single(attestation_data_builder(1, 2)) + .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_double_vote_diff_target() { + let first = attestation_data_builder(0, 2); + let mut second = attestation_data_builder(0, 2); + second.target.root = Hash256::random(); + assert_ne!(first, second); + StreamTest { + cases: vec![ + Test::single(first.clone()), + Test::single(second) + .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_double_vote_diff_data() { + let first = attestation_data_builder(0, 2); + let mut second = attestation_data_builder(0, 2); + second.beacon_block_root = Hash256::random(); + assert_ne!(first, second); + StreamTest { + cases: vec![ + Test::single(first.clone()), + Test::single(second) + .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_double_vote_diff_domain() { + let first = attestation_data_builder(0, 2); + let domain1 = Hash256::from_low_u64_le(1); + let domain2 = Hash256::from_low_u64_le(2); + + StreamTest { + cases: vec![ + Test::single(first.clone()).with_domain(domain1), + Test::single(first.clone()) + .with_domain(domain2) + .expect_invalid_att(InvalidAttestation::DoubleVote( + SignedAttestation::from_attestation(&first, domain1), + )), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_double_vote_diff_source_multi() { + let first = attestation_data_builder(0, 2); + let second = attestation_data_builder(1, 3); + let third = attestation_data_builder(2, 4); + StreamTest { + cases: vec![ + Test::single(first.clone()), + Test::single(second.clone()), + Test::single(third.clone()), + Test::single(attestation_data_builder(1, 2)) + .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&first))), + Test::single(attestation_data_builder(2, 3)) + .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&second))), + Test::single(attestation_data_builder(3, 4)) + .expect_invalid_att(InvalidAttestation::DoubleVote(signed_att(&third))), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_surrounding_single() { + let first = attestation_data_builder(2, 3); + let second = attestation_data_builder(4, 5); + let third = attestation_data_builder(6, 7); + StreamTest { + cases: vec![ + Test::single(first.clone()), + Test::single(second.clone()), + Test::single(third.clone()), + Test::single(attestation_data_builder(1, 4)).expect_invalid_att( + InvalidAttestation::NewSurroundsPrev { + prev: signed_att(&first), + }, + ), + Test::single(attestation_data_builder(3, 6)).expect_invalid_att( + InvalidAttestation::NewSurroundsPrev { + prev: signed_att(&second), + }, + ), + Test::single(attestation_data_builder(5, 8)).expect_invalid_att( + InvalidAttestation::NewSurroundsPrev { + prev: signed_att(&third), + }, + ), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_surrounding_from_first_source() { + let first = attestation_data_builder(2, 3); + let second = attestation_data_builder(3, 4); + StreamTest { + cases: vec![ + Test::single(first), + Test::single(second.clone()), + Test::single(attestation_data_builder(2, 5)).expect_invalid_att( + InvalidAttestation::NewSurroundsPrev { + prev: signed_att(&second), + }, + ), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_surrounding_multiple_votes() { + let first = attestation_data_builder(0, 1); + let second = attestation_data_builder(1, 2); + let third = attestation_data_builder(2, 3); + StreamTest { + cases: vec![ + Test::single(first), + Test::single(second), + Test::single(third.clone()), + Test::single(attestation_data_builder(0, 4)).expect_invalid_att( + InvalidAttestation::NewSurroundsPrev { + prev: signed_att(&third), + }, + ), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_prev_surrounds_new() { + let first = attestation_data_builder(0, 7); + StreamTest { + cases: vec![ + Test::single(first.clone()), + Test::single(attestation_data_builder(1, 6)).expect_invalid_att( + InvalidAttestation::PrevSurroundsNew { + prev: signed_att(&first), + }, + ), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_prev_surrounds_new_multiple() { + let first = attestation_data_builder(0, 4); + let second = attestation_data_builder(1, 7); + let third = attestation_data_builder(8, 10); + StreamTest { + cases: vec![ + Test::single(first.clone()), + Test::single(second.clone()), + Test::single(third.clone()), + Test::single(attestation_data_builder(9, 9)).expect_invalid_att( + InvalidAttestation::PrevSurroundsNew { + prev: signed_att(&third), + }, + ), + Test::single(attestation_data_builder(2, 6)).expect_invalid_att( + InvalidAttestation::PrevSurroundsNew { + prev: signed_att(&second), + }, + ), + Test::single(attestation_data_builder(1, 2)).expect_invalid_att( + InvalidAttestation::PrevSurroundsNew { + prev: signed_att(&first), + }, + ), + ], + ..StreamTest::default() + } + .run() +} diff --git a/common/eip_3076/src/bin/test_generator.rs b/common/eip_3076/src/bin/test_generator.rs new file mode 100644 index 00000000000..bb12c56d548 --- /dev/null +++ b/common/eip_3076/src/bin/test_generator.rs @@ -0,0 +1,554 @@ +use eip_3076::interchange::{ + Interchange, InterchangeData, InterchangeMetadata, SignedAttestation, SignedBlock, +}; +use eip_3076::interchange_test::{MultiTestCase, TestCase}; +use eip_3076::test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT}; +use eip_3076::SUPPORTED_INTERCHANGE_FORMAT_VERSION; +use std::fs::{self, File}; +use std::io::Write; +use std::path::Path; +use types::{Epoch, FixedBytesExtended, Hash256, Slot}; + +fn metadata(genesis_validators_root: Hash256) -> InterchangeMetadata { + InterchangeMetadata { + interchange_format_version: SUPPORTED_INTERCHANGE_FORMAT_VERSION, + genesis_validators_root, + } +} + +type TestPubkey = usize; +type TestBlocks = Vec; +type TestBlocksWithRoots = Vec<(u64, Option)>; +type TestAttestations = Vec<(u64, u64)>; +type TestAttestationsWithRoots = Vec<(u64, u64, Option)>; + +fn interchange(data: Vec<(TestPubkey, TestBlocks, TestAttestations)>) -> Interchange { + let data = data + .into_iter() + .map(|(pk, blocks, attestations)| { + ( + pk, + blocks.into_iter().map(|slot| (slot, None)).collect(), + attestations + .into_iter() + .map(|(source, target)| (source, target, None)) + .collect(), + ) + }) + .collect(); + interchange_with_signing_roots(data) +} + +fn interchange_with_signing_roots( + data: Vec<(TestPubkey, TestBlocksWithRoots, TestAttestationsWithRoots)>, +) -> Interchange { + let data = data + .into_iter() + .map(|(pk, blocks, attestations)| InterchangeData { + pubkey: pubkey(pk), + signed_blocks: blocks + .into_iter() + .map(|(slot, signing_root)| SignedBlock { + slot: Slot::new(slot), + signing_root: signing_root.map(Hash256::from_low_u64_be), + }) + .collect(), + signed_attestations: attestations + .into_iter() + .map(|(source, target, signing_root)| SignedAttestation { + source_epoch: Epoch::new(source), + target_epoch: Epoch::new(target), + signing_root: signing_root.map(Hash256::from_low_u64_be), + }) + .collect(), + }) + .collect(); + Interchange { + metadata: metadata(DEFAULT_GENESIS_VALIDATORS_ROOT), + data, + } +} + +fn main() { + let single_validator_blocks = vec![ + (0, 32, false, false), + (0, 33, true, true), + (0, 31, false, false), + (0, 1, false, false), + ]; + let single_validator_attestations = vec![ + (0, 3, 4, false, false), + (0, 14, 19, false, false), + (0, 15, 20, false, false), + (0, 16, 20, false, false), + (0, 15, 21, true, true), + ]; + + let tests = vec![ + MultiTestCase::single( + "single_validator_import_only", + TestCase::new(interchange(vec![(0, vec![22], vec![(0, 2)])])), + ), + MultiTestCase::single( + "single_validator_single_block", + TestCase::new(interchange(vec![(0, vec![32], vec![])])) + .with_blocks(single_validator_blocks.clone()), + ), + MultiTestCase::single( + "single_validator_single_attestation", + TestCase::new(interchange(vec![(0, vec![], vec![(15, 20)])])) + .with_attestations(single_validator_attestations.clone()), + ), + MultiTestCase::single( + "single_validator_single_block_and_attestation", + TestCase::new(interchange(vec![(0, vec![32], vec![(15, 20)])])) + .with_blocks(single_validator_blocks) + .with_attestations(single_validator_attestations), + ), + MultiTestCase::single( + "single_validator_genesis_attestation", + TestCase::new(interchange(vec![(0, vec![], vec![(0, 0)])])) + .with_attestations(vec![(0, 0, 0, false, false)]), + ), + MultiTestCase::single( + "single_validator_multiple_blocks_and_attestations", + TestCase::new(interchange(vec![( + 0, + vec![2, 3, 10, 1200], + vec![(10, 11), (12, 13), (20, 24)], + )])) + .with_blocks(vec![ + (0, 1, false, false), + (0, 2, false, false), + (0, 3, false, false), + (0, 10, false, false), + (0, 1200, false, false), + (0, 4, false, true), + (0, 256, false, true), + (0, 1201, true, true), + ]) + .with_attestations(vec![ + (0, 9, 10, false, false), + (0, 12, 13, false, false), + (0, 11, 14, false, false), + (0, 21, 22, false, false), + (0, 10, 24, false, false), + (0, 11, 12, false, true), + (0, 20, 25, true, true), + ]), + ), + MultiTestCase::single( + "single_validator_single_block_and_attestation_signing_root", + TestCase::new(interchange_with_signing_roots(vec![( + 0, + vec![(19, Some(1))], + vec![(0, 1, Some(2))], + )])), + ), + MultiTestCase::single( + "multiple_validators_multiple_blocks_and_attestations", + TestCase::new(interchange(vec![ + ( + 0, + vec![10, 15, 20], + vec![(0, 1), (0, 2), (1, 3), (2, 4), (4, 5)], + ), + ( + 1, + vec![3, 4, 100], + vec![(0, 0), (0, 1), (1, 2), (2, 5), (5, 6)], + ), + (2, vec![10, 15, 20], vec![(1, 2), (1, 3), (2, 4)]), + ])) + .with_blocks(vec![ + (0, 9, false, false), + (0, 10, false, false), + (0, 21, true, true), + (0, 11, false, true), + (1, 2, false, false), + (1, 3, false, false), + (1, 0, false, false), + (1, 101, true, true), + (2, 9, false, false), + (2, 10, false, false), + (2, 22, true, true), + ]) + .with_attestations(vec![ + (0, 0, 5, false, false), + (0, 3, 6, false, false), + (0, 4, 6, true, true), + (0, 5, 7, true, true), + (0, 6, 8, true, true), + (1, 1, 7, false, false), + (1, 1, 4, false, true), + (1, 5, 7, true, true), + (2, 0, 0, false, false), + (2, 0, 1, false, false), + (2, 2, 5, true, true), + ]), + ), + MultiTestCase::single( + "multiple_validators_same_slot_blocks", + TestCase::new(interchange_with_signing_roots(vec![ + (0, vec![(1, Some(0)), (2, Some(0)), (3, Some(0))], vec![]), + (1, vec![(1, Some(1)), (3, Some(1))], vec![]), + (2, vec![(1, Some(2)), (2, Some(2))], vec![]), + ])), + ), + MultiTestCase::single( + "wrong_genesis_validators_root", + TestCase::new(interchange(vec![])).should_fail(), + ) + .gvr(Hash256::from_low_u64_be(1)), + MultiTestCase::new( + "multiple_interchanges_single_validator_single_message_gap", + vec![ + TestCase::new(interchange(vec![(0, vec![40], vec![(2, 30)])])), + TestCase::new(interchange(vec![(0, vec![50], vec![(10, 50)])])) + .with_blocks(vec![ + (0, 41, false, true), + (0, 45, false, true), + (0, 49, false, true), + (0, 50, false, false), + (0, 51, true, true), + ]) + .with_attestations(vec![ + (0, 3, 31, false, true), + (0, 9, 49, false, true), + (0, 10, 51, true, true), + ]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_single_validator_single_block_out_of_order", + vec![ + TestCase::new(interchange(vec![(0, vec![40], vec![])])), + TestCase::new(interchange(vec![(0, vec![20], vec![])])) + .contains_slashable_data() + .with_blocks(vec![(0, 20, false, false)]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_single_validator_multiple_blocks_out_of_order", + vec![ + TestCase::new(interchange(vec![(0, vec![0], vec![])])).with_blocks(vec![ + (0, 10, true, true), + (0, 20, true, true), + (0, 30, true, true), + ]), + TestCase::new(interchange(vec![(0, vec![20], vec![])])) + .contains_slashable_data() + .with_blocks(vec![(0, 29, false, true)]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_single_validator_fail_iff_imported", + vec![ + TestCase::new(interchange(vec![(0, vec![40], vec![])])), + TestCase::new(interchange(vec![(0, vec![20, 50], vec![])])) + .contains_slashable_data() + .with_blocks(vec![(0, 20, false, false), (0, 50, false, false)]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_single_validator_single_att_out_of_order", + vec![ + TestCase::new(interchange(vec![(0, vec![], vec![(12, 13)])])), + TestCase::new(interchange(vec![(0, vec![], vec![(10, 11)])])) + .contains_slashable_data() + .with_attestations(vec![ + (0, 10, 14, false, false), + (0, 12, 13, false, false), + (0, 12, 14, true, true), + (0, 13, 15, true, true), + ]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_single_validator_second_surrounds_first", + vec![ + TestCase::new(interchange(vec![(0, vec![], vec![(10, 20)])])), + TestCase::new(interchange(vec![(0, vec![], vec![(9, 21)])])) + .contains_slashable_data() + .with_attestations(vec![ + (0, 10, 20, false, false), + (0, 10, 21, false, false), + (0, 9, 21, false, false), + (0, 9, 22, false, false), + (0, 10, 22, true, true), + ]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_single_validator_first_surrounds_second", + vec![ + TestCase::new(interchange(vec![(0, vec![], vec![(9, 21)])])), + TestCase::new(interchange(vec![(0, vec![], vec![(10, 20)])])) + .contains_slashable_data() + .with_attestations(vec![ + (0, 10, 20, false, false), + (0, 10, 21, false, false), + (0, 9, 21, false, false), + (0, 9, 22, false, false), + (0, 10, 22, true, true), + ]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_multiple_validators_repeat_idem", + vec![ + TestCase::new(interchange(vec![ + (0, vec![2, 4, 6], vec![(0, 1), (1, 2)]), + (1, vec![8, 10, 12], vec![(0, 1), (0, 3)]), + ])), + TestCase::new(interchange(vec![ + (0, vec![2, 4, 6], vec![(0, 1), (1, 2)]), + (1, vec![8, 10, 12], vec![(0, 1), (0, 3)]), + ])) + .contains_slashable_data() + .with_blocks(vec![ + (0, 0, false, false), + (0, 3, false, true), + (0, 7, true, true), + (0, 3, false, true), + (1, 0, false, false), + ]) + .with_attestations(vec![(0, 0, 4, false, false), (1, 0, 4, true, true)]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_overlapping_validators_repeat_idem", + vec![ + TestCase::new(interchange(vec![ + (0, vec![2, 4, 6], vec![(0, 1), (1, 2)]), + (1, vec![8, 10, 12], vec![(0, 1), (0, 3)]), + ])), + TestCase::new(interchange(vec![ + (0, vec![2, 4, 6], vec![(0, 1), (1, 2)]), + (2, vec![8, 10, 12], vec![(0, 1), (0, 3)]), + ])) + .contains_slashable_data(), + TestCase::new(interchange(vec![ + (1, vec![8, 10, 12], vec![(0, 1), (0, 3)]), + (2, vec![8, 10, 12], vec![(0, 1), (0, 3)]), + ])) + .contains_slashable_data() + .with_attestations(vec![ + (0, 0, 4, false, false), + (1, 1, 2, false, false), + (2, 1, 2, false, false), + ]), + ], + ), + MultiTestCase::new( + "multiple_interchanges_overlapping_validators_merge_stale", + vec![ + TestCase::new(interchange(vec![ + (0, vec![100], vec![(12, 13)]), + (1, vec![101], vec![(12, 13)]), + (2, vec![4], vec![(4, 5)]), + ])), + TestCase::new(interchange(vec![ + (0, vec![2], vec![(4, 5)]), + (1, vec![3], vec![(3, 4)]), + (2, vec![102], vec![(12, 13)]), + ])) + .contains_slashable_data() + .with_blocks(vec![ + (0, 100, false, false), + (1, 101, false, false), + (2, 102, false, false), + (0, 103, true, true), + (1, 104, true, true), + (2, 105, true, true), + ]) + .with_attestations(vec![ + (0, 12, 13, false, false), + (0, 11, 14, false, false), + (1, 12, 13, false, false), + (1, 11, 14, false, false), + (2, 12, 13, false, false), + (2, 11, 14, false, false), + (0, 12, 14, true, true), + (1, 13, 14, true, true), + (2, 13, 14, true, true), + ]), + ], + ), + MultiTestCase::single( + "single_validator_source_greater_than_target", + TestCase::new(interchange(vec![(0, vec![], vec![(8, 7)])])).contains_slashable_data(), + ), + MultiTestCase::single( + "single_validator_source_greater_than_target_surrounding", + TestCase::new(interchange(vec![(0, vec![], vec![(5, 2)])])) + .contains_slashable_data() + .with_attestations(vec![(0, 3, 4, false, false)]), + ), + MultiTestCase::single( + "single_validator_source_greater_than_target_surrounded", + TestCase::new(interchange(vec![(0, vec![], vec![(5, 2)])])) + .contains_slashable_data() + .with_attestations(vec![(0, 6, 1, false, false)]), + ), + MultiTestCase::single( + "single_validator_source_greater_than_target_sensible_iff_minified", + TestCase::new(interchange(vec![(0, vec![], vec![(5, 2), (6, 7)])])) + .contains_slashable_data() + .with_attestations(vec![(0, 5, 8, false, false), (0, 6, 8, true, true)]), + ), + MultiTestCase::single( + "single_validator_out_of_order_blocks", + TestCase::new(interchange(vec![(0, vec![6, 5], vec![])])).with_blocks(vec![ + (0, 5, false, false), + (0, 6, false, false), + (0, 7, true, true), + ]), + ), + MultiTestCase::single( + "single_validator_out_of_order_attestations", + TestCase::new(interchange(vec![(0, vec![], vec![(4, 5), (3, 4)])])).with_attestations( + vec![ + (0, 3, 4, false, false), + (0, 4, 5, false, false), + (0, 1, 10, false, false), + (0, 3, 3, false, false), + ], + ), + ), + // Ensure that it's not just the minimum bound check preventing blocks at the same slot + // from being signed. + MultiTestCase::single( + "single_validator_two_blocks_no_signing_root", + TestCase::new(interchange(vec![(0, vec![10, 20], vec![])])) + .with_blocks(vec![(0, 20, false, false)]), + ), + MultiTestCase::single( + "single_validator_multiple_block_attempts", + TestCase::new(interchange(vec![(0, vec![15, 16, 17], vec![])])) + .with_signing_root_blocks(vec![ + (0, 16, 0, false, false), + (0, 16, 1, false, false), + (0, 16, u64::MAX, false, false), + ]), + ), + MultiTestCase::single( + "single_validator_resign_block", + TestCase::new(interchange_with_signing_roots(vec![( + 0, + vec![(15, Some(151)), (16, Some(161)), (17, Some(171))], + vec![], + )])) + .with_signing_root_blocks(vec![ + (0, 15, 151, false, true), + (0, 16, 161, false, true), + (0, 17, 171, false, true), + (0, 15, 152, false, false), + (0, 15, 0, false, false), + (0, 16, 151, false, false), + (0, 17, 151, false, false), + (0, 18, 151, true, true), + (0, 14, 171, false, false), + ]), + ), + MultiTestCase::single( + "single_validator_resign_attestation", + TestCase::new(interchange_with_signing_roots(vec![( + 0, + vec![], + vec![(5, 15, Some(515))], + )])) + .with_signing_root_attestations(vec![ + (0, 5, 15, 0, false, false), + (0, 5, 15, 1, false, false), + (0, 5, 15, 515, false, true), + (0, 6, 15, 615, false, false), + (0, 5, 14, 515, false, false), + ]), + ), + MultiTestCase::single( + "single_validator_slashable_blocks", + TestCase::new(interchange_with_signing_roots(vec![( + 0, + vec![(10, Some(0)), (10, Some(11))], + vec![], + )])) + .contains_slashable_data(), + ), + MultiTestCase::single( + "single_validator_slashable_blocks_no_root", + TestCase::new(interchange(vec![(0, vec![10, 10], vec![])])).contains_slashable_data(), + ), + MultiTestCase::single( + "single_validator_slashable_attestations_double_vote", + TestCase::new(interchange_with_signing_roots(vec![( + 0, + vec![], + vec![(2, 3, Some(0)), (2, 3, Some(1))], + )])) + .contains_slashable_data(), + ), + MultiTestCase::single( + "single_validator_slashable_attestations_surrounds_existing", + TestCase::new(interchange(vec![(0, vec![], vec![(2, 3), (0, 4)])])) + .contains_slashable_data(), + ), + MultiTestCase::single( + "single_validator_slashable_attestations_surrounded_by_existing", + TestCase::new(interchange(vec![(0, vec![], vec![(0, 4), (2, 3)])])) + .contains_slashable_data(), + ), + MultiTestCase::single( + "duplicate_pubkey_not_slashable", + TestCase::new(interchange(vec![ + (0, vec![10, 11], vec![(0, 2)]), + (0, vec![12, 13], vec![(1, 3)]), + ])) + .with_blocks(vec![ + (0, 10, false, false), + (0, 13, false, false), + (0, 14, true, true), + ]) + .with_attestations(vec![(0, 0, 2, false, false), (0, 1, 3, false, false)]), + ), + MultiTestCase::single( + "duplicate_pubkey_slashable_block", + TestCase::new(interchange(vec![ + (0, vec![10], vec![(0, 2)]), + (0, vec![10], vec![(1, 3)]), + ])) + .contains_slashable_data() + .with_blocks(vec![(0, 10, false, false), (0, 11, true, true)]), + ), + MultiTestCase::single( + "duplicate_pubkey_slashable_attestation", + TestCase::new(interchange_with_signing_roots(vec![ + (0, vec![], vec![(0, 3, Some(3))]), + (0, vec![], vec![(1, 2, None)]), + ])) + .contains_slashable_data() + .with_attestations(vec![ + (0, 0, 1, false, false), + (0, 0, 2, false, false), + (0, 0, 4, false, false), + (0, 1, 4, true, true), + ]), + ), + ]; + + let args = std::env::args().collect::>(); + let output_dir = Path::new(&args[1]); + fs::create_dir_all(output_dir).unwrap(); + + for test in tests { + // Check that test case passes without minification + test.run(false); + + // Check that test case passes with minification + test.run(true); + + let f = File::create(output_dir.join(format!("{}.json", test.name))).unwrap(); + serde_json::to_writer_pretty(&f, &test).unwrap(); + writeln!(&f).unwrap(); + } +} diff --git a/common/eip_3076/src/block_tests.rs b/common/eip_3076/src/block_tests.rs new file mode 100644 index 00000000000..b3273015f42 --- /dev/null +++ b/common/eip_3076/src/block_tests.rs @@ -0,0 +1,124 @@ +#![cfg(test)] + +use super::*; +use crate::test_utils::*; +use types::{BeaconBlockHeader, FixedBytesExtended, Slot}; + +pub fn block(slot: u64) -> BeaconBlockHeader { + BeaconBlockHeader { + slot: Slot::new(slot), + proposer_index: 0, + parent_root: Hash256::random(), + state_root: Hash256::random(), + body_root: Hash256::random(), + } +} + +#[test] +fn valid_empty_history() { + StreamTest { + cases: vec![Test::single(block(1))], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_blocks() { + StreamTest { + cases: vec![ + Test::single(block(1)), + Test::single(block(2)), + Test::single(block(3)), + Test::single(block(4)), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_same_block() { + let block = block(100); + StreamTest { + cases: vec![ + Test::single(block.clone()), + Test::single(block).expect_same_data(), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn valid_same_slot_different_validator() { + StreamTest { + registered_validators: vec![pubkey(0), pubkey(1)], + cases: vec![ + Test::with_pubkey(pubkey(0), block(100)), + Test::with_pubkey(pubkey(1), block(100)), + ], + } + .run() +} + +#[test] +fn valid_same_block_different_validator() { + let block = block(100); + StreamTest { + registered_validators: vec![pubkey(0), pubkey(1)], + cases: vec![ + Test::with_pubkey(pubkey(0), block.clone()), + Test::with_pubkey(pubkey(1), block), + ], + } + .run() +} + +#[test] +fn invalid_double_block_proposal() { + let first_block = block(1); + StreamTest { + cases: vec![ + Test::single(first_block.clone()), + Test::single(block(1)).expect_invalid_block(InvalidBlock::DoubleBlockProposal( + SignedBlock::from_header(&first_block, DEFAULT_DOMAIN), + )), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_double_block_proposal_diff_domain() { + let first_block = block(1); + let domain1 = Hash256::from_low_u64_be(1); + let domain2 = Hash256::from_low_u64_be(2); + StreamTest { + cases: vec![ + Test::single(first_block.clone()).with_domain(domain1), + Test::single(first_block.clone()) + .with_domain(domain2) + .expect_invalid_block(InvalidBlock::DoubleBlockProposal(SignedBlock::from_header( + &first_block, + domain1, + ))), + ], + ..StreamTest::default() + } + .run() +} + +#[test] +fn invalid_unregistered_validator() { + StreamTest { + registered_validators: vec![], + cases: vec![ + Test::single(block(0)).expect_result(Err(NotSafe::UnregisteredValidator(pubkey( + DEFAULT_VALIDATOR_INDEX, + )))), + ], + } + .run() +} diff --git a/common/eip_3076/src/extra_interchange_tests.rs b/common/eip_3076/src/extra_interchange_tests.rs new file mode 100644 index 00000000000..0f88ec8b1dc --- /dev/null +++ b/common/eip_3076/src/extra_interchange_tests.rs @@ -0,0 +1,76 @@ +#![cfg(test)] + +use crate::test_utils::pubkey; +use crate::*; +use tempfile::tempdir; +use types::FixedBytesExtended; + +#[test] +fn export_non_existent_key() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let key1 = pubkey(1); + let key2 = pubkey(2); + + // Exporting two non-existent keys should fail on the first one. + let err = slashing_db + .export_interchange_info(Hash256::zero(), Some(&[key1, key2])) + .unwrap_err(); + assert!(matches!( + err, + InterchangeError::NotSafe(NotSafe::UnregisteredValidator(k)) if k == key1 + )); + + slashing_db.register_validator(key1).unwrap(); + + // Exporting one key that exists and one that doesn't should fail on the one that doesn't. + let err = slashing_db + .export_interchange_info(Hash256::zero(), Some(&[key1, key2])) + .unwrap_err(); + assert!(matches!( + err, + InterchangeError::NotSafe(NotSafe::UnregisteredValidator(k)) if k == key2 + )); + + // Exporting only keys that exist should work. + let interchange = slashing_db + .export_interchange_info(Hash256::zero(), Some(&[key1])) + .unwrap(); + assert_eq!(interchange.data.len(), 1); + assert_eq!(interchange.data[0].pubkey, key1); +} + +#[test] +fn export_same_key_twice() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let key1 = pubkey(1); + + slashing_db.register_validator(key1).unwrap(); + + let export_single = slashing_db + .export_interchange_info(Hash256::zero(), Some(&[key1])) + .unwrap(); + let export_double = slashing_db + .export_interchange_info(Hash256::zero(), Some(&[key1, key1])) + .unwrap(); + + assert_eq!(export_single.data.len(), 1); + + // Allow the same data to be exported twice, this is harmless, albeit slightly inefficient. + assert_eq!(export_double.data.len(), 2); + assert_eq!(export_double.data[0], export_double.data[1]); + + // The data should be identical to the single export. + assert_eq!(export_double.data[0], export_single.data[0]); + + // The minified versions should be equal too. + assert_eq!( + export_single.minify().unwrap(), + export_double.minify().unwrap() + ); +} diff --git a/common/eip_3076/src/interchange.rs b/common/eip_3076/src/interchange.rs new file mode 100644 index 00000000000..95a39c50e48 --- /dev/null +++ b/common/eip_3076/src/interchange.rs @@ -0,0 +1,159 @@ +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, + pub signed_attestations: Vec, +} + +#[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, +} + +#[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, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +pub struct Interchange { + pub metadata: InterchangeMetadata, + pub data: Vec, +} + +impl Interchange { + pub fn from_json_str(json: &str) -> Result { + serde_json::from_str(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 + 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::>(); + let other_set = other.data.iter().collect::>(); + 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 { + // Map from pubkey to optional max block and max attestation. + let mut validator_data = + HashMap::, Option)>::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, + }) + } +} diff --git a/common/eip_3076/src/interchange_test.rs b/common/eip_3076/src/interchange_test.rs new file mode 100644 index 00000000000..e1ac841905f --- /dev/null +++ b/common/eip_3076/src/interchange_test.rs @@ -0,0 +1,323 @@ +use crate::{ + interchange::{Interchange, SignedAttestation, SignedBlock}, + test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT}, + SigningRoot, SlashingDatabase, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use tempfile::tempdir; +use types::{Epoch, FixedBytesExtended, Hash256, PublicKeyBytes, Slot}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +pub struct MultiTestCase { + pub name: String, + pub genesis_validators_root: Hash256, + pub steps: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +pub struct TestCase { + pub should_succeed: bool, + pub contains_slashable_data: bool, + pub interchange: Interchange, + pub blocks: Vec, + pub attestations: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +pub struct TestBlock { + pub pubkey: PublicKeyBytes, + pub slot: Slot, + pub signing_root: Hash256, + pub should_succeed: bool, + pub should_succeed_complete: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +pub struct TestAttestation { + pub pubkey: PublicKeyBytes, + pub source_epoch: Epoch, + pub target_epoch: Epoch, + pub signing_root: Hash256, + pub should_succeed: bool, + pub should_succeed_complete: bool, +} + +impl MultiTestCase { + pub fn new(name: &str, steps: Vec) -> Self { + MultiTestCase { + name: name.into(), + genesis_validators_root: DEFAULT_GENESIS_VALIDATORS_ROOT, + steps, + } + } + + pub fn single(name: &str, test_case: TestCase) -> Self { + Self::new(name, vec![test_case]) + } + + pub fn gvr(mut self, genesis_validators_root: Hash256) -> Self { + self.genesis_validators_root = genesis_validators_root; + self + } + + pub fn run(&self, minify: bool) { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + for test_case in &self.steps { + // If the test case is marked as containing slashable data, then the spec allows us to + // fail to import the file. However, we minify on import and ignore slashable data, so + // we should be capable of importing no matter what. + let allow_import_failure = false; + + let interchange = if minify { + let minified = test_case.interchange.minify().unwrap(); + check_minification_invariants(&test_case.interchange, &minified); + minified + } else { + test_case.interchange.clone() + }; + + match slashing_db.import_interchange_info(interchange, self.genesis_validators_root) { + Ok(import_outcomes) => { + let none_failed = import_outcomes.iter().all(|o| !o.failed()); + assert!( + none_failed, + "test `{}` failed to import some records: {:#?}", + self.name, import_outcomes + ); + if !test_case.should_succeed { + panic!( + "test `{}` succeeded on import when it should have failed", + self.name + ); + } + } + Err(e) => { + if test_case.should_succeed && !allow_import_failure { + panic!( + "test `{}` failed on import when it should have succeeded, error: {:?}", + self.name, e + ); + } + break; + } + } + + for (i, block) in test_case.blocks.iter().enumerate() { + match slashing_db.check_and_insert_block_signing_root( + &block.pubkey, + block.slot, + SigningRoot::from(block.signing_root), + ) { + Ok(safe) if !block.should_succeed => { + panic!( + "block {} from `{}` succeeded when it should have failed: {:?}", + i, self.name, safe + ); + } + Err(e) if block.should_succeed => { + panic!( + "block {} from `{}` failed when it should have succeeded: {:?}", + i, self.name, e + ); + } + _ => (), + } + } + + for (i, att) in test_case.attestations.iter().enumerate() { + match slashing_db.check_and_insert_attestation_signing_root( + &att.pubkey, + att.source_epoch, + att.target_epoch, + SigningRoot::from(att.signing_root), + ) { + Ok(safe) if !att.should_succeed => { + panic!( + "attestation {} from `{}` succeeded when it should have failed: {:?}", + i, self.name, safe + ); + } + Err(e) if att.should_succeed => { + panic!( + "attestation {} from `{}` failed when it should have succeeded: {:?}", + i, self.name, e + ); + } + _ => (), + } + } + } + } +} + +impl TestCase { + pub fn new(interchange: Interchange) -> Self { + TestCase { + should_succeed: true, + contains_slashable_data: false, + interchange, + blocks: vec![], + attestations: vec![], + } + } + + pub fn should_fail(mut self) -> Self { + self.should_succeed = false; + self + } + + pub fn contains_slashable_data(mut self) -> Self { + self.contains_slashable_data = true; + self + } + + pub fn with_blocks(self, blocks: impl IntoIterator) -> Self { + self.with_signing_root_blocks(blocks.into_iter().map( + |(index, slot, should_succeed, should_succeed_complete)| { + (index, slot, 0, should_succeed, should_succeed_complete) + }, + )) + } + + pub fn with_signing_root_blocks( + mut self, + blocks: impl IntoIterator, + ) -> Self { + self.blocks.extend(blocks.into_iter().map( + |(pk, slot, signing_root, should_succeed, should_succeed_complete)| { + assert!( + !should_succeed || should_succeed_complete, + "if should_succeed is true then should_succeed_complete must also be true" + ); + TestBlock { + pubkey: pubkey(pk), + slot: Slot::new(slot), + signing_root: Hash256::from_low_u64_be(signing_root), + should_succeed, + should_succeed_complete, + } + }, + )); + self + } + + pub fn with_attestations( + self, + attestations: impl IntoIterator, + ) -> Self { + self.with_signing_root_attestations(attestations.into_iter().map( + |(id, source, target, succeed, succeed_complete)| { + (id, source, target, 0, succeed, succeed_complete) + }, + )) + } + + pub fn with_signing_root_attestations( + mut self, + attestations: impl IntoIterator, + ) -> Self { + self.attestations.extend(attestations.into_iter().map( + |(pk, source, target, signing_root, should_succeed, should_succeed_complete)| { + assert!( + !should_succeed || should_succeed_complete, + "if should_succeed is true then should_succeed_complete must also be true" + ); + TestAttestation { + pubkey: pubkey(pk), + source_epoch: Epoch::new(source), + target_epoch: Epoch::new(target), + signing_root: Hash256::from_low_u64_be(signing_root), + should_succeed, + should_succeed_complete, + } + }, + )); + self + } +} + +pub fn check_minification_invariants(interchange: &Interchange, minified: &Interchange) { + // Metadata should be unchanged. + assert_eq!(interchange.metadata, minified.metadata); + + // Minified data should contain one entry per *unique* public key. + let uniq_pubkeys = get_uniq_pubkeys(interchange); + assert_eq!(uniq_pubkeys, get_uniq_pubkeys(minified)); + assert_eq!(uniq_pubkeys.len(), minified.data.len()); + + for &pubkey in uniq_pubkeys.iter() { + // Minified data should contain 1 block per validator, unless the validator never signed any + // blocks. All of those blocks should have slots <= the slot of the minified block. + let original_blocks = get_blocks_of_validator(interchange, pubkey); + let minified_blocks = get_blocks_of_validator(minified, pubkey); + + if original_blocks.is_empty() { + assert!(minified_blocks.is_empty()); + } else { + // Should have exactly 1 block. + assert_eq!(minified_blocks.len(), 1); + + // That block should have no signing root (it's synthetic). + let mini_block = minified_blocks.first().unwrap(); + assert_eq!(mini_block.signing_root, None); + + // All original blocks should have slots <= the mini block. + assert!(original_blocks + .iter() + .all(|block| block.slot <= mini_block.slot)); + } + + // Minified data should contain 1 attestation per validator, unless the validator never + // signed any attestations. All attestations should have source and target <= the source + // and target of the minified attestation. + let original_attestations = get_attestations_of_validator(interchange, pubkey); + let minified_attestations = get_attestations_of_validator(minified, pubkey); + + if original_attestations.is_empty() { + assert!(minified_attestations.is_empty()); + } else { + assert_eq!(minified_attestations.len(), 1); + + let mini_attestation = minified_attestations.first().unwrap(); + assert_eq!(mini_attestation.signing_root, None); + + assert!(original_attestations + .iter() + .all(|att| att.source_epoch <= mini_attestation.source_epoch + && att.target_epoch <= mini_attestation.target_epoch)); + } + } +} + +fn get_uniq_pubkeys(interchange: &Interchange) -> HashSet { + interchange.data.iter().map(|data| data.pubkey).collect() +} + +fn get_blocks_of_validator(interchange: &Interchange, pubkey: PublicKeyBytes) -> Vec<&SignedBlock> { + interchange + .data + .iter() + .filter(|data| data.pubkey == pubkey) + .flat_map(|data| data.signed_blocks.iter()) + .collect() +} + +fn get_attestations_of_validator( + interchange: &Interchange, + pubkey: PublicKeyBytes, +) -> Vec<&SignedAttestation> { + interchange + .data + .iter() + .filter(|data| data.pubkey == pubkey) + .flat_map(|data| data.signed_attestations.iter()) + .collect() +} diff --git a/common/eip_3076/src/lib.rs b/common/eip_3076/src/lib.rs new file mode 100644 index 00000000000..90947f65a57 --- /dev/null +++ b/common/eip_3076/src/lib.rs @@ -0,0 +1,149 @@ +mod attestation_tests; +mod block_tests; +mod extra_interchange_tests; +pub mod interchange_test; +mod parallel_tests; + +pub mod interchange; +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, SlashingDatabase, + SUPPORTED_INTERCHANGE_FORMAT_VERSION, +}; +use rusqlite::Error as SQLError; +use std::fmt::Display; +use std::io::{Error as IOError, ErrorKind}; +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::{types::Type, Error}; + + 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/common/eip_3076/src/parallel_tests.rs b/common/eip_3076/src/parallel_tests.rs new file mode 100644 index 00000000000..e3cc1a0d567 --- /dev/null +++ b/common/eip_3076/src/parallel_tests.rs @@ -0,0 +1,82 @@ +//! Tests that stress the concurrency safety of the slashing protection DB. +#![cfg(test)] + +use crate::attestation_tests::attestation_data_builder; +use crate::block_tests::block; +use crate::test_utils::*; +use crate::*; +use rayon::prelude::*; +use tempfile::tempdir; + +#[test] +fn block_same_slot() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let pk = pubkey(0); + + slashing_db.register_validator(pk).unwrap(); + + // A stream of blocks all with the same slot. + let num_blocks = 10; + let results = (0..num_blocks) + .into_par_iter() + .map(|_| slashing_db.check_and_insert_block_proposal(&pk, &block(1), DEFAULT_DOMAIN)) + .collect::>(); + + let num_successes = results.iter().filter(|res| res.is_ok()).count(); + assert_eq!(num_successes, 1); +} + +#[test] +fn attestation_same_target() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let pk = pubkey(0); + + slashing_db.register_validator(pk).unwrap(); + + // A stream of attestations all with the same target. + let num_attestations = 10; + let results = (0..num_attestations) + .into_par_iter() + .map(|i| { + slashing_db.check_and_insert_attestation( + &pk, + &attestation_data_builder(i, num_attestations), + DEFAULT_DOMAIN, + ) + }) + .collect::>(); + + let num_successes = results.iter().filter(|res| res.is_ok()).count(); + assert_eq!(num_successes, 1); +} + +#[test] +fn attestation_surround_fest() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let pk = pubkey(0); + + slashing_db.register_validator(pk).unwrap(); + + // A stream of attestations that all surround each other. + let num_attestations = 10; + + let results = (0..num_attestations) + .into_par_iter() + .map(|i| { + let att = attestation_data_builder(i, 2 * num_attestations - i); + slashing_db.check_and_insert_attestation(&pk, &att, DEFAULT_DOMAIN) + }) + .collect::>(); + + let num_successes = results.iter().filter(|res| res.is_ok()).count(); + assert_eq!(num_successes, 1); +} diff --git a/common/eip_3076/src/registration_tests.rs b/common/eip_3076/src/registration_tests.rs new file mode 100644 index 00000000000..472f41577d5 --- /dev/null +++ b/common/eip_3076/src/registration_tests.rs @@ -0,0 +1,74 @@ +#![cfg(test)] + +use crate::test_utils::*; +use crate::*; +use std::iter; +use tempfile::tempdir; + +#[test] +fn double_register_validators() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let num_validators = 100u32; + let pubkeys = (0..num_validators as usize).map(pubkey).collect::>(); + + let get_validator_ids = || { + pubkeys + .iter() + .map(|pk| slashing_db.get_validator_id(pk).unwrap()) + .collect::>() + }; + + assert_eq!(slashing_db.num_validator_rows().unwrap(), 0); + + slashing_db.register_validators(pubkeys.iter()).unwrap(); + assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators); + let validator_ids = get_validator_ids(); + + slashing_db.register_validators(pubkeys.iter()).unwrap(); + assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators); + assert_eq!(validator_ids, get_validator_ids()); +} + +#[test] +fn reregister_validator() { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + let pk = pubkey(0); + + // Register validator. + slashing_db.register_validator(pk).unwrap(); + let id = slashing_db.get_validator_id(&pk).unwrap(); + + slashing_db + .with_transaction(|txn| { + // Disable. + slashing_db.update_validator_status(txn, id, false)?; + + // Fetching the validator as "registered" should now fail. + assert_eq!( + slashing_db.get_validator_id_in_txn(txn, &pk).unwrap_err(), + NotSafe::DisabledValidator(pk) + ); + + // Fetching its status should return false. + let (fetched_id, enabled) = + slashing_db.get_validator_id_with_status(txn, &pk)?.unwrap(); + assert_eq!(fetched_id, id); + assert!(!enabled); + + // Re-registering the validator should preserve its ID while changing its status to + // enabled. + slashing_db.register_validators_in_txn(iter::once(&pk), txn)?; + + let re_reg_id = slashing_db.get_validator_id_in_txn(txn, &pk)?; + assert_eq!(re_reg_id, id); + + Ok::<_, NotSafe>(()) + }) + .unwrap(); +} diff --git a/common/eip_3076/src/signed_attestation.rs b/common/eip_3076/src/signed_attestation.rs new file mode 100644 index 00000000000..332f80c7045 --- /dev/null +++ b/common/eip_3076/src/signed_attestation.rs @@ -0,0 +1,62 @@ +use crate::{signing_root_from_row, SigningRoot}; +use types::{AttestationData, Epoch, Hash256, SignedRoot}; + +/// An attestation that has previously been signed. +#[derive(Clone, Debug, PartialEq)] +pub struct SignedAttestation { + pub source_epoch: Epoch, + pub target_epoch: Epoch, + pub signing_root: SigningRoot, +} + +/// Reasons why an attestation may be slashable (or invalid). +#[derive(PartialEq, Debug, Clone)] +pub enum InvalidAttestation { + /// The attestation has the same target epoch as an attestation from the DB (enclosed). + DoubleVote(SignedAttestation), + /// The attestation surrounds an existing attestation from the database (`prev`). + NewSurroundsPrev { prev: SignedAttestation }, + /// The attestation is surrounded by an existing attestation from the database (`prev`). + PrevSurroundsNew { prev: SignedAttestation }, + /// The attestation is invalid because its source epoch is greater than its target epoch. + SourceExceedsTarget, + /// The attestation is invalid because its source epoch is less than the lower bound on source + /// epochs for this validator. + SourceLessThanLowerBound { + source_epoch: Epoch, + bound_epoch: Epoch, + }, + /// The attestation is invalid because its target epoch is less than or equal to the lower + /// bound on target epochs for this validator. + TargetLessThanOrEqLowerBound { + target_epoch: Epoch, + bound_epoch: Epoch, + }, +} + +impl SignedAttestation { + pub fn new(source_epoch: Epoch, target_epoch: Epoch, signing_root: SigningRoot) -> Self { + Self { + source_epoch, + target_epoch, + signing_root, + } + } + + /// Create a `SignedAttestation` from attestation data and a domain. + pub fn from_attestation(attestation: &AttestationData, domain: Hash256) -> Self { + Self { + source_epoch: attestation.source.epoch, + target_epoch: attestation.target.epoch, + signing_root: attestation.signing_root(domain).into(), + } + } + + /// Create a `SignedAttestation` from an SQLite row of `(source, target, signing_root)`. + pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result { + let source = row.get(0)?; + let target = row.get(1)?; + let signing_root = signing_root_from_row(2, row)?; + Ok(SignedAttestation::new(source, target, signing_root)) + } +} diff --git a/common/eip_3076/src/signed_block.rs b/common/eip_3076/src/signed_block.rs new file mode 100644 index 00000000000..d46872529e9 --- /dev/null +++ b/common/eip_3076/src/signed_block.rs @@ -0,0 +1,36 @@ +use crate::{signing_root_from_row, SigningRoot}; +use types::{BeaconBlockHeader, Hash256, SignedRoot, Slot}; + +/// A block that has previously been signed. +#[derive(Clone, Debug, PartialEq)] +pub struct SignedBlock { + pub slot: Slot, + pub signing_root: SigningRoot, +} + +/// Reasons why a block may be slashable. +#[derive(PartialEq, Debug, Clone)] +pub enum InvalidBlock { + DoubleBlockProposal(SignedBlock), + SlotViolatesLowerBound { block_slot: Slot, bound_slot: Slot }, +} + +impl SignedBlock { + pub fn new(slot: Slot, signing_root: SigningRoot) -> Self { + Self { slot, signing_root } + } + + pub fn from_header(header: &BeaconBlockHeader, domain: Hash256) -> Self { + Self { + slot: header.slot, + signing_root: header.signing_root(domain).into(), + } + } + + /// Parse an SQLite row of `(slot, signing_root)`. + pub fn from_row(row: &rusqlite::Row) -> rusqlite::Result { + let slot = row.get(0)?; + let signing_root = signing_root_from_row(1, row)?; + Ok(SignedBlock { slot, signing_root }) + } +} diff --git a/common/eip_3076/src/slashing_database.rs b/common/eip_3076/src/slashing_database.rs new file mode 100644 index 00000000000..f4c844d3140 --- /dev/null +++ b/common/eip_3076/src/slashing_database.rs @@ -0,0 +1,1251 @@ +use crate::interchange::{ + Interchange, InterchangeData, InterchangeMetadata, SignedAttestation as InterchangeAttestation, + SignedBlock as InterchangeBlock, +}; +use crate::signed_attestation::InvalidAttestation; +use crate::signed_block::InvalidBlock; +use crate::{signing_root_from_row, NotSafe, Safe, SignedAttestation, SignedBlock, SigningRoot}; +use filesystem::restrict_file_permissions; +use r2d2_sqlite::SqliteConnectionManager; +use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior}; +use std::fs::File; +use std::path::Path; +use std::time::Duration; +use types::{AttestationData, BeaconBlockHeader, Epoch, Hash256, PublicKeyBytes, SignedRoot, Slot}; + +type Pool = r2d2::Pool; + +/// We set the pool size to 1 for compatibility with locking_mode=EXCLUSIVE. +/// +/// This is perhaps overkill in the presence of exclusive transactions, but has +/// the added bonus of preventing other processes from trying to use our slashing database. +pub const POOL_SIZE: u32 = 1; +#[cfg(not(test))] +pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); +#[cfg(test)] +pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(1); + +/// Supported version of the interchange format. +pub const SUPPORTED_INTERCHANGE_FORMAT_VERSION: u64 = 5; + +/// Column ID of the `validators.enabled` column. +pub const VALIDATORS_ENABLED_CID: i64 = 2; + +#[derive(Debug, Clone)] +pub struct SlashingDatabase { + conn_pool: Pool, +} + +impl SlashingDatabase { + /// Open an existing database at the given `path`, or create one if none exists. + pub fn open_or_create(path: &Path) -> Result { + if path.exists() { + Self::open(path) + } else { + Self::create(path) + } + } + + /// Create a slashing database at the given path. + /// + /// Error if a database (or any file) already exists at `path`. + pub fn create(path: &Path) -> Result { + let _file = File::options() + .write(true) + .read(true) + .create_new(true) + .open(path)?; + + restrict_file_permissions(path).map_err(|_| NotSafe::PermissionsError)?; + let conn_pool = Self::open_conn_pool(path)?; + let mut conn = conn_pool.get()?; + + conn.execute( + "CREATE TABLE validators ( + id INTEGER PRIMARY KEY, + public_key BLOB NOT NULL UNIQUE + )", + params![], + )?; + + conn.execute( + "CREATE TABLE signed_blocks ( + validator_id INTEGER NOT NULL, + slot INTEGER NOT NULL, + signing_root BLOB NOT NULL, + FOREIGN KEY(validator_id) REFERENCES validators(id) + UNIQUE (validator_id, slot) + )", + params![], + )?; + + conn.execute( + "CREATE TABLE signed_attestations ( + validator_id INTEGER, + source_epoch INTEGER NOT NULL, + target_epoch INTEGER NOT NULL, + signing_root BLOB NOT NULL, + FOREIGN KEY(validator_id) REFERENCES validators(id) + UNIQUE (validator_id, target_epoch) + )", + params![], + )?; + + // The tables created above are for the v0 schema. We immediately update them + // to the latest schema without dropping the connection. + let txn = conn.transaction()?; + Self::apply_schema_migrations(&txn)?; + txn.commit()?; + + Ok(Self { conn_pool }) + } + + /// Open an existing `SlashingDatabase` from disk. + /// + /// This will automatically check for and apply the latest schema migrations. + pub fn open(path: &Path) -> Result { + let conn_pool = Self::open_conn_pool(path)?; + let db = Self { conn_pool }; + db.with_transaction(Self::apply_schema_migrations)?; + Ok(db) + } + + fn apply_schema_migrations(txn: &Transaction) -> Result<(), NotSafe> { + // Add the `enabled` column to the `validators` table if it does not already exist. + let enabled_col_exists = txn + .query_row( + "SELECT cid, name FROM pragma_table_info('validators') WHERE name = 'enabled'", + params![], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional()? + .map(|(cid, name): (i64, String)| { + // Check that the enabled column is in the correct position with the right name. + // This is a defensive check that shouldn't do anything in practice unless the + // slashing DB has been manually edited. + if cid == VALIDATORS_ENABLED_CID && name == "enabled" { + Ok(()) + } else { + Err(NotSafe::ConsistencyError) + } + }) + .transpose()? + .is_some(); + + if !enabled_col_exists { + txn.execute( + "ALTER TABLE validators ADD COLUMN enabled BOOL NOT NULL DEFAULT TRUE", + params![], + )?; + } + + Ok(()) + } + + /// Open a new connection pool with all of the necessary settings and tweaks. + fn open_conn_pool(path: &Path) -> Result { + let manager = SqliteConnectionManager::file(path) + .with_flags(rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE) + .with_init(Self::apply_pragmas); + let conn_pool = Pool::builder() + .max_size(POOL_SIZE) + .connection_timeout(CONNECTION_TIMEOUT) + .build(manager) + .map_err(|e| NotSafe::SQLError(format!("Unable to open database: {:?}", e)))?; + Ok(conn_pool) + } + + /// Apply the necessary settings to an SQLite connection. + /// + /// Most importantly, put the database into exclusive locking mode, so that threads are forced + /// to serialise all DB access (to prevent slashable data being checked and signed in parallel). + /// The exclusive locking mode also has the benefit of applying to other processes, so multiple + /// Lighthouse processes trying to access the same database will also be blocked. + fn apply_pragmas(conn: &mut rusqlite::Connection) -> Result<(), rusqlite::Error> { + conn.pragma_update(None, "foreign_keys", true)?; + conn.pragma_update(None, "locking_mode", "EXCLUSIVE")?; + Ok(()) + } + + /// Creates an empty transaction and drops it. Used to test whether the database is locked. + pub fn test_transaction(&self) -> Result<(), NotSafe> { + let mut conn = self.conn_pool.get()?; + Transaction::new(&mut conn, TransactionBehavior::Exclusive)?; + Ok(()) + } + + /// Execute a database transaction as a closure, committing if `f` returns `Ok`. + pub fn with_transaction(&self, f: F) -> Result + where + F: FnOnce(&Transaction) -> Result, + U: From, + { + let mut conn = self.conn_pool.get().map_err(NotSafe::from)?; + let txn = conn.transaction().map_err(NotSafe::from)?; + let value = f(&txn)?; + txn.commit().map_err(NotSafe::from)?; + Ok(value) + } + + /// Register a validator with the slashing protection database. + /// + /// This allows the validator to record their signatures in the database, and check + /// for slashings. + pub fn register_validator(&self, validator_pk: PublicKeyBytes) -> Result<(), NotSafe> { + self.register_validators(std::iter::once(&validator_pk)) + } + + /// Register multiple validators with the slashing protection database. + pub fn register_validators<'a>( + &self, + public_keys: impl Iterator, + ) -> Result<(), NotSafe> { + self.with_transaction(|txn| self.register_validators_in_txn(public_keys, txn)) + } + + /// Register multiple validators inside the given transaction. + /// + /// The caller must commit the transaction for the changes to be persisted. + pub fn register_validators_in_txn<'a>( + &self, + public_keys: impl Iterator, + txn: &Transaction, + ) -> Result<(), NotSafe> { + let mut stmt = + txn.prepare("INSERT INTO validators (public_key, enabled) VALUES (?1, TRUE)")?; + for pubkey in public_keys { + match self.get_validator_id_with_status(txn, pubkey)? { + None => { + stmt.execute([pubkey.as_hex_string()])?; + } + Some((validator_id, false)) => { + self.update_validator_status(txn, validator_id, true)?; + } + Some((_, true)) => { + // Validator already registered and enabled. + } + } + } + Ok(()) + } + + pub fn update_validator_status( + &self, + txn: &Transaction, + validator_id: i64, + status: bool, + ) -> Result<(), NotSafe> { + txn.execute( + "UPDATE validators SET enabled = ? WHERE id = ?", + params![status, validator_id], + )?; + Ok(()) + } + + /// Check that all of the given validators are registered. + pub fn check_validator_registrations<'a>( + &self, + mut public_keys: impl Iterator, + ) -> Result<(), NotSafe> { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + public_keys + .try_for_each(|public_key| self.get_validator_id_in_txn(&txn, public_key).map(|_| ())) + } + + /// List the internal validator ID and public key of every registered validator. + pub fn list_all_registered_validators( + &self, + txn: &Transaction, + ) -> Result, InterchangeError> { + txn.prepare("SELECT id, public_key FROM validators ORDER BY id ASC")? + .query_and_then(params![], |row| { + let validator_id = row.get(0)?; + let pubkey_str: String = row.get(1)?; + let pubkey = pubkey_str + .parse() + .map_err(InterchangeError::InvalidPubkey)?; + Ok((validator_id, pubkey)) + })? + .collect() + } + + /// Get the database-internal ID for an enabled validator. + /// + /// This is NOT the same as a validator index, and depends on the ordering that validators + /// are registered with the slashing protection database (and may vary between machines). + pub fn get_validator_id(&self, public_key: &PublicKeyBytes) -> Result { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + self.get_validator_id_in_txn(&txn, public_key) + } + + pub fn get_validator_id_in_txn( + &self, + txn: &Transaction, + public_key: &PublicKeyBytes, + ) -> Result { + let (validator_id, enabled) = self + .get_validator_id_with_status(txn, public_key)? + .ok_or(NotSafe::UnregisteredValidator(*public_key))?; + if enabled { + Ok(validator_id) + } else { + Err(NotSafe::DisabledValidator(*public_key)) + } + } + + /// Get validator ID regardless of whether or not it is enabled. + pub fn get_validator_id_ignoring_status( + &self, + txn: &Transaction, + public_key: &PublicKeyBytes, + ) -> Result { + let (validator_id, _) = self + .get_validator_id_with_status(txn, public_key)? + .ok_or(NotSafe::UnregisteredValidator(*public_key))?; + Ok(validator_id) + } + + pub fn get_validator_id_with_status( + &self, + txn: &Transaction, + public_key: &PublicKeyBytes, + ) -> Result, NotSafe> { + Ok(txn + .query_row( + "SELECT id, enabled FROM validators WHERE public_key = ?1", + params![&public_key.as_hex_string()], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional()?) + } + + /// Check a block proposal from `validator_pubkey` for slash safety. + fn check_block_proposal( + &self, + txn: &Transaction, + validator_pubkey: &PublicKeyBytes, + slot: Slot, + signing_root: SigningRoot, + ) -> Result { + let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; + + let existing_block = txn + .prepare( + "SELECT slot, signing_root + FROM signed_blocks + WHERE validator_id = ?1 AND slot = ?2", + )? + .query_row(params![validator_id, slot], SignedBlock::from_row) + .optional()?; + + if let Some(existing_block) = existing_block { + if existing_block.signing_root == signing_root { + // Same slot and same hash -> we're re-broadcasting a previously signed block + return Ok(Safe::SameData); + } else { + // Same epoch but not the same hash -> it's a DoubleBlockProposal + return Err(NotSafe::InvalidBlock(InvalidBlock::DoubleBlockProposal( + existing_block, + ))); + } + } + + let min_slot = txn + .prepare("SELECT MIN(slot) FROM signed_blocks WHERE validator_id = ?1")? + .query_row(params![validator_id], |row| row.get(0))?; + + if let Some(min_slot) = min_slot { + if slot <= min_slot { + return Err(NotSafe::InvalidBlock( + InvalidBlock::SlotViolatesLowerBound { + block_slot: slot, + bound_slot: min_slot, + }, + )); + } + } + + Ok(Safe::Valid) + } + + /// Check an attestation from `validator_pubkey` for slash safety. + fn check_attestation( + &self, + txn: &Transaction, + validator_pubkey: &PublicKeyBytes, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: SigningRoot, + ) -> Result { + // Although it's not required to avoid slashing, we disallow attestations + // which are obviously invalid by virtue of their source epoch exceeding their target. + if att_source_epoch > att_target_epoch { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::SourceExceedsTarget, + )); + } + + let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; + + // Check for a double vote. Namely, an existing attestation with the same target epoch, + // and a different signing root. + let same_target_att = txn + .prepare( + "SELECT source_epoch, target_epoch, signing_root + FROM signed_attestations + WHERE validator_id = ?1 AND target_epoch = ?2", + )? + .query_row( + params![validator_id, att_target_epoch], + SignedAttestation::from_row, + ) + .optional()?; + + if let Some(existing_attestation) = same_target_att { + // If the new attestation is identical to the existing attestation, then we already + // know that it is safe, and can return immediately. + if existing_attestation.signing_root == att_signing_root { + return Ok(Safe::SameData); + // Otherwise if the hashes are different, this is a double vote. + } else { + return Err(NotSafe::InvalidAttestation(InvalidAttestation::DoubleVote( + existing_attestation, + ))); + } + } + + // Check that no previous vote is surrounding `attestation`. + // If there is a surrounding attestation, we only return the most recent one. + let surrounding_attestation = txn + .prepare( + "SELECT source_epoch, target_epoch, signing_root + FROM signed_attestations + WHERE validator_id = ?1 AND source_epoch < ?2 AND target_epoch > ?3 + ORDER BY target_epoch DESC + LIMIT 1", + )? + .query_row( + params![validator_id, att_source_epoch, att_target_epoch], + SignedAttestation::from_row, + ) + .optional()?; + + if let Some(prev) = surrounding_attestation { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::PrevSurroundsNew { prev }, + )); + } + + // Check that no previous vote is surrounded by `attestation`. + // If there is a surrounded attestation, we only return the most recent one. + let surrounded_attestation = txn + .prepare( + "SELECT source_epoch, target_epoch, signing_root + FROM signed_attestations + WHERE validator_id = ?1 AND source_epoch > ?2 AND target_epoch < ?3 + ORDER BY target_epoch DESC + LIMIT 1", + )? + .query_row( + params![validator_id, att_source_epoch, att_target_epoch], + SignedAttestation::from_row, + ) + .optional()?; + + if let Some(prev) = surrounded_attestation { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::NewSurroundsPrev { prev }, + )); + } + + // Check lower bounds: ensure that source is greater than or equal to min source, + // and target is greater than min target. This allows pruning, and compatibility + // with the interchange format. + let min_source = txn + .prepare("SELECT MIN(source_epoch) FROM signed_attestations WHERE validator_id = ?1")? + .query_row(params![validator_id], |row| row.get(0))?; + + if let Some(min_source) = min_source { + if att_source_epoch < min_source { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::SourceLessThanLowerBound { + source_epoch: att_source_epoch, + bound_epoch: min_source, + }, + )); + } + } + + let min_target = txn + .prepare("SELECT MIN(target_epoch) FROM signed_attestations WHERE validator_id = ?1")? + .query_row(params![validator_id], |row| row.get(0))?; + + if let Some(min_target) = min_target { + if att_target_epoch <= min_target { + return Err(NotSafe::InvalidAttestation( + InvalidAttestation::TargetLessThanOrEqLowerBound { + target_epoch: att_target_epoch, + bound_epoch: min_target, + }, + )); + } + } + + // Everything has been checked, return Valid + Ok(Safe::Valid) + } + + /// Insert a block proposal into the slashing database. + /// + /// This should *only* be called in the same (exclusive) transaction as `check_block_proposal` + /// so that the check isn't invalidated by a concurrent mutation. + fn insert_block_proposal( + &self, + txn: &Transaction, + validator_pubkey: &PublicKeyBytes, + slot: Slot, + signing_root: SigningRoot, + ) -> Result<(), NotSafe> { + let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; + + txn.execute( + "INSERT INTO signed_blocks (validator_id, slot, signing_root) + VALUES (?1, ?2, ?3)", + params![validator_id, slot, signing_root.to_hash256_raw().as_slice()], + )?; + Ok(()) + } + + /// Insert an attestation into the slashing database. + /// + /// This should *only* be called in the same (exclusive) transaction as `check_attestation` + /// so that the check isn't invalidated by a concurrent mutation. + fn insert_attestation( + &self, + txn: &Transaction, + validator_pubkey: &PublicKeyBytes, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: SigningRoot, + ) -> Result<(), NotSafe> { + let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?; + + txn.execute( + "INSERT INTO signed_attestations (validator_id, source_epoch, target_epoch, signing_root) + VALUES (?1, ?2, ?3, ?4)", + params![ + validator_id, + att_source_epoch, + att_target_epoch, + att_signing_root.to_hash256_raw().as_slice() + ], + )?; + Ok(()) + } + + /// Check a block proposal for slash safety, and if it is safe, record it in the database. + /// + /// The checking and inserting happen atomically and exclusively. We enforce exclusivity + /// to prevent concurrent checks and inserts from resulting in slashable data being inserted. + /// + /// This is the safe, externally-callable interface for checking block proposals. + pub fn check_and_insert_block_proposal( + &self, + validator_pubkey: &PublicKeyBytes, + block_header: &BeaconBlockHeader, + domain: Hash256, + ) -> Result { + self.check_and_insert_block_signing_root( + validator_pubkey, + block_header.slot, + block_header.signing_root(domain).into(), + ) + } + + /// As for `check_and_insert_block_proposal` but without requiring the whole `BeaconBlockHeader`. + pub fn check_and_insert_block_signing_root( + &self, + validator_pubkey: &PublicKeyBytes, + slot: Slot, + signing_root: SigningRoot, + ) -> Result { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; + let safe = self.check_and_insert_block_signing_root_txn( + validator_pubkey, + slot, + signing_root, + &txn, + )?; + txn.commit()?; + Ok(safe) + } + + /// Transactional variant of `check_and_insert_block_signing_root`. + pub fn check_and_insert_block_signing_root_txn( + &self, + validator_pubkey: &PublicKeyBytes, + slot: Slot, + signing_root: SigningRoot, + txn: &Transaction, + ) -> Result { + let safe = self.check_block_proposal(txn, validator_pubkey, slot, signing_root)?; + + if safe != Safe::SameData { + self.insert_block_proposal(txn, validator_pubkey, slot, signing_root)?; + } + Ok(safe) + } + + /// Check an attestation for slash safety, and if it is safe, record it in the database. + /// + /// The checking and inserting happen atomically and exclusively. We enforce exclusivity + /// to prevent concurrent checks and inserts from resulting in slashable data being inserted. + /// + /// This is the safe, externally-callable interface for checking attestations. + pub fn check_and_insert_attestation( + &self, + validator_pubkey: &PublicKeyBytes, + attestation: &AttestationData, + domain: Hash256, + ) -> Result { + let attestation_signing_root = attestation.signing_root(domain).into(); + self.check_and_insert_attestation_signing_root( + validator_pubkey, + attestation.source.epoch, + attestation.target.epoch, + attestation_signing_root, + ) + } + + /// As for `check_and_insert_attestation` but without requiring the whole `AttestationData`. + pub fn check_and_insert_attestation_signing_root( + &self, + validator_pubkey: &PublicKeyBytes, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: SigningRoot, + ) -> Result { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?; + let safe = self.check_and_insert_attestation_signing_root_txn( + validator_pubkey, + att_source_epoch, + att_target_epoch, + att_signing_root, + &txn, + )?; + txn.commit()?; + Ok(safe) + } + + /// Transactional variant of `check_and_insert_attestation_signing_root`. + fn check_and_insert_attestation_signing_root_txn( + &self, + validator_pubkey: &PublicKeyBytes, + att_source_epoch: Epoch, + att_target_epoch: Epoch, + att_signing_root: SigningRoot, + txn: &Transaction, + ) -> Result { + let safe = self.check_attestation( + txn, + validator_pubkey, + att_source_epoch, + att_target_epoch, + att_signing_root, + )?; + + if safe != Safe::SameData { + self.insert_attestation( + txn, + validator_pubkey, + att_source_epoch, + att_target_epoch, + att_signing_root, + )?; + } + Ok(safe) + } + + /// Import slashing protection from another client in the interchange format. + /// + /// This function will atomically import the entire interchange, failing if *any* + /// record cannot be imported. + pub fn import_interchange_info( + &self, + interchange: Interchange, + genesis_validators_root: Hash256, + ) -> Result, InterchangeError> { + let version = interchange.metadata.interchange_format_version; + if version != SUPPORTED_INTERCHANGE_FORMAT_VERSION { + return Err(InterchangeError::UnsupportedVersion(version)); + } + + if genesis_validators_root != interchange.metadata.genesis_validators_root { + return Err(InterchangeError::GenesisValidatorsMismatch { + client: genesis_validators_root, + interchange_file: interchange.metadata.genesis_validators_root, + }); + } + + // Create a single transaction for the entire batch, which will only be committed if + // all records are imported successfully. + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + + let mut import_outcomes = vec![]; + let mut commit = true; + + for record in interchange.data { + let pubkey = record.pubkey; + match self.import_interchange_record(record, &txn) { + Ok(summary) => { + import_outcomes.push(InterchangeImportOutcome::Success { pubkey, summary }); + } + Err(error) => { + import_outcomes.push(InterchangeImportOutcome::Failure { pubkey, error }); + commit = false; + } + } + } + + if commit { + txn.commit()?; + Ok(import_outcomes) + } else { + Err(InterchangeError::AtomicBatchAborted(import_outcomes)) + } + } + + pub fn import_interchange_record( + &self, + record: InterchangeData, + txn: &Transaction, + ) -> Result { + let pubkey = &record.pubkey; + + self.register_validators_in_txn(std::iter::once(pubkey), txn)?; + + // Summary of minimum and maximum messages pre-import. + let prev_summary = self.validator_summary(pubkey, txn)?; + + // If the interchange contains any blocks, update the database with the new max slot. + let max_block = record.signed_blocks.iter().max_by_key(|b| b.slot); + + if let Some(max_block) = max_block { + // Store new synthetic block with maximum slot and null signing root. Remove all other + // blocks. + let new_max_slot = max_or(prev_summary.max_block_slot, max_block.slot); + let signing_root = SigningRoot::default(); + + self.clear_signed_blocks(pubkey, txn)?; + self.insert_block_proposal(txn, pubkey, new_max_slot, signing_root)?; + } + + // Find the attestations with max source and max target. Unless the input contains slashable + // data these two attestations should be identical, but we also handle the case where they + // are not. + let max_source_attestation = record + .signed_attestations + .iter() + .max_by_key(|att| att.source_epoch); + let max_target_attestation = record + .signed_attestations + .iter() + .max_by_key(|att| att.target_epoch); + + if let (Some(max_source_att), Some(max_target_att)) = + (max_source_attestation, max_target_attestation) + { + let source_epoch = max_or( + prev_summary.max_attestation_source, + max_source_att.source_epoch, + ); + let target_epoch = max_or( + prev_summary.max_attestation_target, + max_target_att.target_epoch, + ); + let signing_root = SigningRoot::default(); + + // Clear existing attestations before insert to avoid running afoul of the target epoch + // uniqueness constraint. + self.clear_signed_attestations(pubkey, txn)?; + self.insert_attestation(txn, pubkey, source_epoch, target_epoch, signing_root)?; + } + + let summary = self.validator_summary(&record.pubkey, txn)?; + + // Check that the summary is consistent with having added the new data. + if summary.check_block_consistency(&prev_summary, !record.signed_blocks.is_empty()) + && summary.check_attestation_consistency( + &prev_summary, + !record.signed_attestations.is_empty(), + ) + { + Ok(summary) + } else { + // This should never occur and is indicative of a bug in the import code. + Err(NotSafe::ConsistencyError) + } + } + + pub fn export_all_interchange_info( + &self, + genesis_validators_root: Hash256, + ) -> Result { + self.export_interchange_info(genesis_validators_root, None) + } + + pub fn export_interchange_info( + &self, + genesis_validators_root: Hash256, + selected_pubkeys: Option<&[PublicKeyBytes]>, + ) -> Result { + let mut conn = self.conn_pool.get()?; + let txn = &conn.transaction()?; + self.export_interchange_info_in_txn(genesis_validators_root, selected_pubkeys, txn) + } + + pub fn export_interchange_info_in_txn( + &self, + genesis_validators_root: Hash256, + selected_pubkeys: Option<&[PublicKeyBytes]>, + txn: &Transaction, + ) -> Result { + // Determine the validator IDs and public keys to export data for. + let to_export = if let Some(selected_pubkeys) = selected_pubkeys { + selected_pubkeys + .iter() + .map(|pubkey| { + let id = self.get_validator_id_ignoring_status(txn, pubkey)?; + Ok((id, *pubkey)) + }) + .collect::>()? + } else { + self.list_all_registered_validators(txn)? + }; + + let data = to_export + .into_iter() + .map(|(validator_id, pubkey)| { + let signed_blocks = + self.export_interchange_blocks_for_validator(validator_id, txn)?; + let signed_attestations = + self.export_interchange_attestations_for_validator(validator_id, txn)?; + Ok(InterchangeData { + pubkey, + signed_blocks, + signed_attestations, + }) + }) + .collect::>()?; + + let metadata = InterchangeMetadata { + interchange_format_version: SUPPORTED_INTERCHANGE_FORMAT_VERSION, + genesis_validators_root, + }; + + Ok(Interchange { metadata, data }) + } + + fn export_interchange_blocks_for_validator( + &self, + validator_id: i64, + txn: &Transaction, + ) -> Result, InterchangeError> { + txn.prepare( + "SELECT slot, signing_root + FROM signed_blocks + WHERE signed_blocks.validator_id = ?1 + ORDER BY slot ASC", + )? + .query_and_then(params![validator_id], |row| { + let slot = row.get(0)?; + let signing_root = signing_root_from_row(1, row)?.to_hash256(); + Ok(InterchangeBlock { slot, signing_root }) + })? + .collect() + } + + fn export_interchange_attestations_for_validator( + &self, + validator_id: i64, + txn: &Transaction, + ) -> Result, InterchangeError> { + txn.prepare( + "SELECT source_epoch, target_epoch, signing_root + FROM signed_attestations + WHERE signed_attestations.validator_id = ?1 + ORDER BY source_epoch ASC, target_epoch ASC", + )? + .query_and_then(params![validator_id], |row| { + let source_epoch = row.get(0)?; + let target_epoch = row.get(1)?; + let signing_root = signing_root_from_row(2, row)?.to_hash256(); + let signed_attestation = InterchangeAttestation { + source_epoch, + target_epoch, + signing_root, + }; + Ok(signed_attestation) + })? + .collect() + } + + /// Remove all blocks for `public_key` with slots less than `new_min_slot`. + fn prune_signed_blocks( + &self, + public_key: &PublicKeyBytes, + new_min_slot: Slot, + txn: &Transaction, + ) -> Result<(), NotSafe> { + let validator_id = self.get_validator_id_in_txn(txn, public_key)?; + + txn.execute( + "DELETE FROM signed_blocks + WHERE + validator_id = ?1 AND + slot < ?2 AND + slot < (SELECT MAX(slot) + FROM signed_blocks + WHERE validator_id = ?1)", + params![validator_id, new_min_slot], + )?; + + Ok(()) + } + + /// Prune the signed blocks table for the given public keys. + pub fn prune_all_signed_blocks<'a>( + &self, + mut public_keys: impl Iterator, + new_min_slot: Slot, + ) -> Result<(), NotSafe> { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + public_keys.try_for_each(|pubkey| self.prune_signed_blocks(pubkey, new_min_slot, &txn))?; + txn.commit()?; + Ok(()) + } + + /// Remove all attestations for `public_key` with `target < new_min_target`. + /// + /// If the `new_min_target` was plucked out of thin air and doesn't necessarily correspond to + /// an extant attestation then this function is still safe. It will never delete *all* the + /// attestations in the database. + fn prune_signed_attestations( + &self, + public_key: &PublicKeyBytes, + new_min_target: Epoch, + txn: &Transaction, + ) -> Result<(), NotSafe> { + let validator_id = self.get_validator_id_in_txn(txn, public_key)?; + + // The following holds, because we never store mutually slashable attestations: + // a.target < new_min_target --> a.source <= new_min_source + // + // The `MAX(target_epoch)` acts as a guard to prevent accidentally clearing the DB. + txn.execute( + "DELETE FROM signed_attestations + WHERE + validator_id = ?1 AND + target_epoch < ?2 AND + target_epoch < (SELECT MAX(target_epoch) + FROM signed_attestations + WHERE validator_id = ?1)", + params![validator_id, new_min_target], + )?; + + Ok(()) + } + + /// Remove all attestations signed by a given `public_key`. + /// + /// This function is incredibly dangerous and should be used with extreme caution. Presently + /// we only use it one place: immediately before inserting a new maximum source/maximum target + /// attestation. Any future use should take care to respect the database's non-emptiness. + fn clear_signed_attestations( + &self, + public_key: &PublicKeyBytes, + txn: &Transaction, + ) -> Result<(), NotSafe> { + let validator_id = self.get_validator_id_in_txn(txn, public_key)?; + + txn.execute( + "DELETE FROM signed_attestations WHERE validator_id = ?1", + params![validator_id], + )?; + Ok(()) + } + + /// Remove all blocks signed by a given `public_key`. + /// + /// Dangerous, should only be used immediately before inserting a new block in the same + /// transacation. + fn clear_signed_blocks( + &self, + public_key: &PublicKeyBytes, + txn: &Transaction, + ) -> Result<(), NotSafe> { + let validator_id = self.get_validator_id_in_txn(txn, public_key)?; + txn.execute( + "DELETE FROM signed_blocks WHERE validator_id = ?1", + params![validator_id], + )?; + Ok(()) + } + + /// Prune the signed attestations table for the given validator keys. + pub fn prune_all_signed_attestations<'a>( + &self, + mut public_keys: impl Iterator, + new_min_target: Epoch, + ) -> Result<(), NotSafe> { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + public_keys + .try_for_each(|pubkey| self.prune_signed_attestations(pubkey, new_min_target, &txn))?; + txn.commit()?; + Ok(()) + } + + pub fn num_validator_rows(&self) -> Result { + let mut conn = self.conn_pool.get()?; + let txn = conn.transaction()?; + let count = txn + .prepare("SELECT COALESCE(COUNT(*), 0) FROM validators")? + .query_row(params![], |row| row.get(0))?; + Ok(count) + } + + /// Get a summary of a validator's slashing protection data including minimums and maximums. + pub fn validator_summary( + &self, + public_key: &PublicKeyBytes, + txn: &Transaction, + ) -> Result { + let validator_id = self.get_validator_id_in_txn(txn, public_key)?; + let (min_block_slot, max_block_slot) = txn + .prepare( + "SELECT MIN(slot), MAX(slot) + FROM signed_blocks + WHERE validator_id = ?1", + )? + .query_row(params![validator_id], |row| Ok((row.get(0)?, row.get(1)?)))?; + + let ( + min_attestation_source, + min_attestation_target, + max_attestation_source, + max_attestation_target, + ) = txn + .prepare( + "SELECT MIN(source_epoch), MIN(target_epoch), MAX(source_epoch), MAX(target_epoch) + FROM signed_attestations + WHERE validator_id = ?1", + )? + .query_row(params![validator_id], |row| { + Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)) + })?; + + Ok(ValidatorSummary { + min_block_slot, + max_block_slot, + min_attestation_source, + min_attestation_target, + max_attestation_source, + max_attestation_target, + }) + } +} + +/// 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)) +} + +/// 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 { .. }) + } +} + +#[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::*; + use tempfile::tempdir; + + #[test] + fn open_non_existent_error() { + let dir = tempdir().unwrap(); + let file = dir.path().join("db.sqlite"); + assert!(SlashingDatabase::open(&file).is_err()); + } + + // Due to the exclusive locking, trying to use an already open database should error. + #[test] + fn double_open_error() { + let dir = tempdir().unwrap(); + let file = dir.path().join("db.sqlite"); + let _db1 = SlashingDatabase::create(&file).unwrap(); + + SlashingDatabase::open(&file).unwrap_err(); + } + + // Attempting to create the same database twice should error. + #[test] + fn double_create_error() { + let dir = tempdir().unwrap(); + let file = dir.path().join("db.sqlite"); + let _db1 = SlashingDatabase::create(&file).unwrap(); + drop(_db1); + SlashingDatabase::create(&file).unwrap_err(); + } + + // Check that both `open` and `create` apply the same connection settings. + #[test] + fn connection_settings_applied() { + let dir = tempdir().unwrap(); + let file = dir.path().join("db.sqlite"); + + let check = |db: &SlashingDatabase| { + assert_eq!(db.conn_pool.max_size(), POOL_SIZE); + assert_eq!(db.conn_pool.connection_timeout(), CONNECTION_TIMEOUT); + let conn = db.conn_pool.get().unwrap(); + assert!(conn + .pragma_query_value(None, "foreign_keys", |row| { row.get::<_, bool>(0) }) + .unwrap()); + assert_eq!( + conn.pragma_query_value(None, "locking_mode", |row| { row.get::<_, String>(0) }) + .unwrap() + .to_uppercase(), + "EXCLUSIVE" + ); + }; + + let db1 = SlashingDatabase::create(&file).unwrap(); + check(&db1); + drop(db1); + let db2 = SlashingDatabase::open(&file).unwrap(); + check(&db2); + } + + #[test] + fn test_transaction_failure() { + let dir = tempdir().unwrap(); + let file = dir.path().join("db.sqlite"); + let db = SlashingDatabase::create(&file).unwrap(); + + db.with_transaction(|_| { + db.test_transaction().unwrap_err(); + Ok::<(), NotSafe>(()) + }) + .unwrap(); + } +} diff --git a/common/eip_3076/src/test_utils.rs b/common/eip_3076/src/test_utils.rs new file mode 100644 index 00000000000..8cbca12a10b --- /dev/null +++ b/common/eip_3076/src/test_utils.rs @@ -0,0 +1,159 @@ +use crate::*; +use tempfile::{tempdir, TempDir}; +use types::{test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader}; + +pub const DEFAULT_VALIDATOR_INDEX: usize = 0; +pub const DEFAULT_DOMAIN: Hash256 = Hash256::ZERO; +pub const DEFAULT_GENESIS_VALIDATORS_ROOT: Hash256 = Hash256::ZERO; + +pub fn pubkey(index: usize) -> PublicKeyBytes { + generate_deterministic_keypair(index).pk.compress() +} + +pub struct Test { + pubkey: PublicKeyBytes, + data: T, + domain: Hash256, + expected: Result, +} + +impl Test { + pub fn single(data: T) -> Self { + Self::with_pubkey(pubkey(DEFAULT_VALIDATOR_INDEX), data) + } + + pub fn with_pubkey(pubkey: PublicKeyBytes, data: T) -> Self { + Self { + pubkey, + data, + domain: DEFAULT_DOMAIN, + expected: Ok(Safe::Valid), + } + } + + pub fn with_domain(mut self, domain: Hash256) -> Self { + self.domain = domain; + self + } + + pub fn expect_result(mut self, result: Result) -> Self { + self.expected = result; + self + } + + pub fn expect_invalid_att(self, error: InvalidAttestation) -> Self { + self.expect_result(Err(NotSafe::InvalidAttestation(error))) + } + + pub fn expect_invalid_block(self, error: InvalidBlock) -> Self { + self.expect_result(Err(NotSafe::InvalidBlock(error))) + } + + pub fn expect_same_data(self) -> Self { + self.expect_result(Ok(Safe::SameData)) + } +} + +pub struct StreamTest { + /// Validators to register. + pub registered_validators: Vec, + /// Vector of cases and the value expected when calling `check_and_insert_X`. + pub cases: Vec>, +} + +impl Default for StreamTest { + fn default() -> Self { + Self { + registered_validators: vec![pubkey(DEFAULT_VALIDATOR_INDEX)], + cases: vec![], + } + } +} + +impl StreamTest { + pub fn run(&self) { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + for pubkey in &self.registered_validators { + slashing_db.register_validator(*pubkey).unwrap(); + } + + check_registration_invariants(&slashing_db, &self.registered_validators); + + for (i, test) in self.cases.iter().enumerate() { + assert_eq!( + slashing_db.check_and_insert_attestation(&test.pubkey, &test.data, test.domain), + test.expected, + "attestation {} not processed as expected", + i + ); + } + + roundtrip_database(&dir, &slashing_db, self.registered_validators.is_empty()); + } +} + +impl StreamTest { + pub fn run(&self) { + let dir = tempdir().unwrap(); + let slashing_db_file = dir.path().join("slashing_protection.sqlite"); + let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap(); + + for pubkey in &self.registered_validators { + slashing_db.register_validator(*pubkey).unwrap(); + } + + check_registration_invariants(&slashing_db, &self.registered_validators); + + for (i, test) in self.cases.iter().enumerate() { + assert_eq!( + slashing_db.check_and_insert_block_proposal(&test.pubkey, &test.data, test.domain), + test.expected, + "attestation {} not processed as expected", + i + ); + } + + roundtrip_database(&dir, &slashing_db, self.registered_validators.is_empty()); + } +} + +// This function roundtrips the database, but applies minification in order to be compatible with +// the implicit minification done on import. +fn roundtrip_database(dir: &TempDir, db: &SlashingDatabase, is_empty: bool) { + let exported = db + .export_all_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT) + .unwrap(); + let new_db = + SlashingDatabase::create(&dir.path().join("roundtrip_slashing_protection.sqlite")).unwrap(); + new_db + .import_interchange_info(exported.clone(), DEFAULT_GENESIS_VALIDATORS_ROOT) + .unwrap(); + let reexported = new_db + .export_all_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT) + .unwrap(); + + assert!(exported + .minify() + .unwrap() + .equiv(&reexported.minify().unwrap())); + assert_eq!(is_empty, exported.is_empty()); +} + +fn check_registration_invariants( + slashing_db: &SlashingDatabase, + registered_validators: &[PublicKeyBytes], +) { + slashing_db + .check_validator_registrations(registered_validators.iter()) + .unwrap(); + let registered_list = slashing_db + .with_transaction(|txn| slashing_db.list_all_registered_validators(txn)) + .unwrap() + .into_iter() + .map(|(_, pubkey)| pubkey) + .collect::>(); + assert_eq!(registered_validators, registered_list); +} diff --git a/common/eip_3076/tests/interop.rs b/common/eip_3076/tests/interop.rs new file mode 100644 index 00000000000..e5f396b0e83 --- /dev/null +++ b/common/eip_3076/tests/interop.rs @@ -0,0 +1,51 @@ +use eip_3076::interchange_test::MultiTestCase; +use std::fs::File; +use std::path::PathBuf; +use std::sync::LazyLock; + +pub static TEST_ROOT_DIR: LazyLock = LazyLock::new(test_root_dir); + +fn download_tests() { + let make_output = std::process::Command::new("make") + .current_dir(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .output() + .expect("need `make` to succeed to download and untar slashing protection tests"); + if !make_output.status.success() { + eprintln!("{}", String::from_utf8_lossy(&make_output.stderr)); + panic!("Running `make` for slashing protection tests failed, see above"); + } +} + +fn test_root_dir() -> PathBuf { + download_tests(); + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("interchange-tests") + .join("tests") +} + +// NOTE: I've combined two tests together to avoid a race-condition which occurs when fighting over +// which test builds the TEST_ROOT_DIR lazy static. +#[test] +fn generated_and_with_minification() { + for entry in TEST_ROOT_DIR + .join("generated") + .read_dir() + .unwrap() + .map(Result::unwrap) + { + let file = File::open(entry.path()).unwrap(); + let test_case: MultiTestCase = serde_json::from_reader(&file).unwrap(); + test_case.run(false); + } + + for entry in TEST_ROOT_DIR + .join("generated") + .read_dir() + .unwrap() + .map(Result::unwrap) + { + let file = File::open(entry.path()).unwrap(); + let test_case: MultiTestCase = serde_json::from_reader(&file).unwrap(); + test_case.run(true); + } +} diff --git a/common/eip_3076/tests/main.rs b/common/eip_3076/tests/main.rs new file mode 100644 index 00000000000..5b66bd87e61 --- /dev/null +++ b/common/eip_3076/tests/main.rs @@ -0,0 +1,2 @@ +mod interop; +mod migration; diff --git a/common/eip_3076/tests/migration.rs b/common/eip_3076/tests/migration.rs new file mode 100644 index 00000000000..a1b980f65c9 --- /dev/null +++ b/common/eip_3076/tests/migration.rs @@ -0,0 +1,68 @@ +//! Tests for upgrading a previous version of the database to the latest schema. +use eip_3076::{NotSafe, SlashingDatabase}; +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use tempfile::tempdir; +use types::{FixedBytesExtended, Hash256}; + +fn test_data_dir() -> PathBuf { + Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("migration-tests") +} + +/// Copy `filename` from the test data dir to the temporary `dest` for testing. +fn make_copy(filename: &str, dest: &Path) -> PathBuf { + let source_file = test_data_dir().join(filename); + let dest_file = dest.join(filename); + fs::copy(source_file, &dest_file).unwrap(); + dest_file +} + +#[test] +fn add_enabled_column() { + let tmp = tempdir().unwrap(); + + let path = make_copy("v0_no_enabled_column.sqlite", tmp.path()); + let num_expected_validators = 5; + + // Database should open without errors, indicating successfull application of migrations. + // The input file has no `enabled` column, which should get added when opening it here. + let db = SlashingDatabase::open(&path).unwrap(); + + // Check that exporting an interchange file lists all the validators. + let interchange = db.export_all_interchange_info(Hash256::zero()).unwrap(); + assert_eq!(interchange.data.len(), num_expected_validators); + + db.with_transaction(|txn| { + // Check that all the validators are enabled and unique. + let uniq_validator_ids = interchange + .data + .iter() + .map(|data| { + let (validator_id, enabled) = db + .get_validator_id_with_status(txn, &data.pubkey) + .unwrap() + .unwrap(); + assert!(enabled); + (validator_id, data.pubkey) + }) + .collect::>(); + + assert_eq!(uniq_validator_ids.len(), num_expected_validators); + + // Check that we can disable them all. + for (&validator_id, pubkey) in &uniq_validator_ids { + db.update_validator_status(txn, validator_id, false) + .unwrap(); + let (loaded_id, enabled) = db + .get_validator_id_with_status(txn, pubkey) + .unwrap() + .unwrap(); + assert_eq!(validator_id, loaded_id); + assert!(!enabled); + } + + Ok::<_, NotSafe>(()) + }) + .unwrap(); +}