From ff739d56bea4ed2acd442d6a54f2c40bce01b9b8 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 18 Feb 2025 11:39:49 +1100 Subject: [PATCH 01/13] Fix light client merkle proofs (#7007) Fix a regression introduced in this PR: - https://github.com/sigp/lighthouse/pull/6361 We were indexing into the `MerkleTree` with raw generalized indices, which was incorrect and triggering `debug_assert` failures, as described here: - https://github.com/sigp/lighthouse/issues/7005 - Convert `generalized_index` to the correct leaf index prior to proof generation. - Add sanity checks on indices used in `BeaconState::generate_proof`. - Remove debug asserts from `MerkleTree::generate_proof` in favour of actual errors. This would have caught the bug earlier. - Refactor the EF tests so that the merkle validity tests are actually run. They were misconfigured in a way that resulted in them running silently with 0 test cases, and the `check_all_files_accessed.py` script still had an ignore that covered the test files, so this omission wasn't detected. --- consensus/merkle_proof/src/lib.rs | 15 ++++- consensus/types/src/beacon_block_body.rs | 4 +- consensus/types/src/beacon_state.rs | 30 +++++++--- testing/ef_tests/check_all_files_accessed.py | 4 +- .../src/cases/merkle_proof_validity.rs | 57 +++++++++++++++---- testing/ef_tests/src/handler.rs | 34 ++--------- testing/ef_tests/tests/tests.rs | 10 +--- 7 files changed, 93 insertions(+), 61 deletions(-) diff --git a/consensus/merkle_proof/src/lib.rs b/consensus/merkle_proof/src/lib.rs index b01f3f4429..271e676df1 100644 --- a/consensus/merkle_proof/src/lib.rs +++ b/consensus/merkle_proof/src/lib.rs @@ -34,6 +34,8 @@ pub enum MerkleTree { pub enum MerkleTreeError { // Trying to push in a leaf LeafReached, + // Trying to generate a proof for a non-leaf node + NonLeafProof, // No more space in the MerkleTree MerkleTreeFull, // MerkleTree is invalid @@ -313,8 +315,17 @@ impl MerkleTree { current_depth -= 1; } - debug_assert_eq!(proof.len(), depth); - debug_assert!(current_node.is_leaf()); + if proof.len() != depth { + // This should be unreachable regardless of how the method is called, because we push + // one proof element for each layer of `depth`. + return Err(MerkleTreeError::PleaseNotifyTheDevs); + } + + // Generating a proof for a non-leaf node is invalid and indicates an error on the part of + // the caller. + if !current_node.is_leaf() { + return Err(MerkleTreeError::NonLeafProof); + } // Put proof in bottom-up order. proof.reverse(); diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index 3f75790a35..410374c18f 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -277,9 +277,9 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, // https://github.com/ethereum/consensus-specs/blob/dev/specs/deneb/beacon-chain.md#beaconblockbody generalized_index .checked_sub(NUM_BEACON_BLOCK_BODY_HASH_TREE_ROOT_LEAVES) - .ok_or(Error::IndexNotSupported(generalized_index))? + .ok_or(Error::GeneralizedIndexNotSupported(generalized_index))? } - _ => return Err(Error::IndexNotSupported(generalized_index)), + _ => return Err(Error::GeneralizedIndexNotSupported(generalized_index)), }; let leaves = self.body_merkle_leaves(); diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 157271b227..4aed79898d 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -157,6 +157,7 @@ pub enum Error { current_fork: ForkName, }, TotalActiveBalanceDiffUninitialized, + GeneralizedIndexNotSupported(usize), IndexNotSupported(usize), InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), @@ -2580,11 +2581,12 @@ impl BeaconState { // for the internal nodes. Result should be 22 or 23, the field offset of the committee // in the `BeaconState`: // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#beaconstate - let field_index = if self.fork_name_unchecked().electra_enabled() { + let field_gindex = if self.fork_name_unchecked().electra_enabled() { light_client_update::CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA } else { light_client_update::CURRENT_SYNC_COMMITTEE_INDEX }; + let field_index = field_gindex.safe_sub(self.num_fields_pow2())?; let leaves = self.get_beacon_state_leaves(); self.generate_proof(field_index, &leaves) } @@ -2594,11 +2596,12 @@ impl BeaconState { // for the internal nodes. Result should be 22 or 23, the field offset of the committee // in the `BeaconState`: // https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#beaconstate - let field_index = if self.fork_name_unchecked().electra_enabled() { + let field_gindex = if self.fork_name_unchecked().electra_enabled() { light_client_update::NEXT_SYNC_COMMITTEE_INDEX_ELECTRA } else { light_client_update::NEXT_SYNC_COMMITTEE_INDEX }; + let field_index = field_gindex.safe_sub(self.num_fields_pow2())?; let leaves = self.get_beacon_state_leaves(); self.generate_proof(field_index, &leaves) } @@ -2606,17 +2609,24 @@ impl BeaconState { pub fn compute_finalized_root_proof(&self) -> Result, Error> { // Finalized root is the right child of `finalized_checkpoint`, divide by two to get // the generalized index of `state.finalized_checkpoint`. - let field_index = if self.fork_name_unchecked().electra_enabled() { - // Index should be 169/2 - 64 = 20 which matches the position - // of `finalized_checkpoint` in `BeaconState` + let checkpoint_root_gindex = if self.fork_name_unchecked().electra_enabled() { light_client_update::FINALIZED_ROOT_INDEX_ELECTRA } else { - // Index should be 105/2 - 32 = 20 which matches the position - // of `finalized_checkpoint` in `BeaconState` light_client_update::FINALIZED_ROOT_INDEX }; + let checkpoint_gindex = checkpoint_root_gindex / 2; + + // Convert gindex to index by subtracting 2**depth (gindex = 2**depth + index). + // + // After Electra, the index should be 169/2 - 64 = 20 which matches the position + // of `finalized_checkpoint` in `BeaconState`. + // + // Prior to Electra, the index should be 105/2 - 32 = 20 which matches the position + // of `finalized_checkpoint` in `BeaconState`. + let checkpoint_index = checkpoint_gindex.safe_sub(self.num_fields_pow2())?; + let leaves = self.get_beacon_state_leaves(); - let mut proof = self.generate_proof(field_index, &leaves)?; + let mut proof = self.generate_proof(checkpoint_index, &leaves)?; proof.insert(0, self.finalized_checkpoint().epoch.tree_hash_root()); Ok(proof) } @@ -2626,6 +2636,10 @@ impl BeaconState { field_index: usize, leaves: &[Hash256], ) -> Result, Error> { + if field_index >= leaves.len() { + return Err(Error::IndexNotSupported(field_index)); + } + let depth = self.num_fields_pow2().ilog2() as usize; let tree = merkle_proof::MerkleTree::create(leaves, depth); let (_, proof) = tree.generate_proof(field_index, depth)?; diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 8a662b72e3..4e744b797a 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -27,10 +27,8 @@ excluded_paths = [ "tests/.*/.*/ssz_static/PowBlock/", # We no longer implement merge logic. "tests/.*/bellatrix/fork_choice/on_merge_block", - # light_client - "tests/.*/.*/light_client/single_merkle_proof", + # Light client sync is not implemented "tests/.*/.*/light_client/sync", - "tests/.*/electra/light_client/update_ranking", # LightClientStore "tests/.*/.*/ssz_static/LightClientStore", # LightClientSnapshot diff --git a/testing/ef_tests/src/cases/merkle_proof_validity.rs b/testing/ef_tests/src/cases/merkle_proof_validity.rs index 109d2cc796..711974dd43 100644 --- a/testing/ef_tests/src/cases/merkle_proof_validity.rs +++ b/testing/ef_tests/src/cases/merkle_proof_validity.rs @@ -20,6 +20,12 @@ pub struct MerkleProof { pub branch: Vec, } +#[derive(Debug)] +pub enum GenericMerkleProofValidity { + BeaconState(BeaconStateMerkleProofValidity), + BeaconBlockBody(Box>), +} + #[derive(Debug, Clone, Deserialize)] #[serde(bound = "E: EthSpec")] pub struct BeaconStateMerkleProofValidity { @@ -28,6 +34,39 @@ pub struct BeaconStateMerkleProofValidity { pub merkle_proof: MerkleProof, } +impl LoadCase for GenericMerkleProofValidity { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + let path_components = path.iter().collect::>(); + + // The "suite" name is the 2nd last directory in the path. + assert!( + path_components.len() >= 2, + "path should have at least 2 components" + ); + let suite_name = path_components[path_components.len() - 2]; + + if suite_name == "BeaconState" { + BeaconStateMerkleProofValidity::load_from_dir(path, fork_name) + .map(GenericMerkleProofValidity::BeaconState) + } else if suite_name == "BeaconBlockBody" { + BeaconBlockBodyMerkleProofValidity::load_from_dir(path, fork_name) + .map(Box::new) + .map(GenericMerkleProofValidity::BeaconBlockBody) + } else { + panic!("unsupported type for merkle proof test: {:?}", suite_name) + } + } +} + +impl Case for GenericMerkleProofValidity { + fn result(&self, case_index: usize, fork_name: ForkName) -> Result<(), Error> { + match self { + Self::BeaconState(test) => test.result(case_index, fork_name), + Self::BeaconBlockBody(test) => test.result(case_index, fork_name), + } + } +} + impl LoadCase for BeaconStateMerkleProofValidity { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { let spec = &testing_spec::(fork_name); @@ -72,11 +111,9 @@ impl Case for BeaconStateMerkleProofValidity { } }; - let Ok(proof) = proof else { - return Err(Error::FailedToParseTest( - "Could not retrieve merkle proof".to_string(), - )); - }; + let proof = proof.map_err(|e| { + Error::FailedToParseTest(format!("Could not retrieve merkle proof: {e:?}")) + })?; let proof_len = proof.len(); let branch_len = self.merkle_proof.branch.len(); if proof_len != branch_len { @@ -273,11 +310,11 @@ impl Case for BeaconBlockBodyMerkleProofValidity { fn result(&self, _case_index: usize, _fork_name: ForkName) -> Result<(), Error> { let binding = self.block_body.clone(); let block_body = binding.to_ref(); - let Ok(proof) = block_body.block_body_merkle_proof(self.merkle_proof.leaf_index) else { - return Err(Error::FailedToParseTest( - "Could not retrieve merkle proof".to_string(), - )); - }; + let proof = block_body + .block_body_merkle_proof(self.merkle_proof.leaf_index) + .map_err(|e| { + Error::FailedToParseTest(format!("Could not retrieve merkle proof: {e:?}")) + })?; let proof_len = proof.len(); let branch_len = self.merkle_proof.branch.len(); if proof_len != branch_len { diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 481c9b2169..a375498239 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -1000,30 +1000,6 @@ impl Handler for KZGRecoverCellsAndKZGProofHandler { } } -#[derive(Derivative)] -#[derivative(Default(bound = ""))] -pub struct BeaconStateMerkleProofValidityHandler(PhantomData); - -impl Handler for BeaconStateMerkleProofValidityHandler { - type Case = cases::BeaconStateMerkleProofValidity; - - fn config_name() -> &'static str { - E::name() - } - - fn runner_name() -> &'static str { - "light_client" - } - - fn handler_name(&self) -> String { - "single_merkle_proof/BeaconState".into() - } - - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { - fork_name.altair_enabled() - } -} - #[derive(Derivative)] #[derivative(Default(bound = ""))] pub struct KzgInclusionMerkleProofValidityHandler(PhantomData); @@ -1054,10 +1030,10 @@ impl Handler for KzgInclusionMerkleProofValidityHandler(PhantomData); +pub struct MerkleProofValidityHandler(PhantomData); -impl Handler for BeaconBlockBodyMerkleProofValidityHandler { - type Case = cases::BeaconBlockBodyMerkleProofValidity; +impl Handler for MerkleProofValidityHandler { + type Case = cases::GenericMerkleProofValidity; fn config_name() -> &'static str { E::name() @@ -1068,11 +1044,11 @@ impl Handler for BeaconBlockBodyMerkleProofValidityHandle } fn handler_name(&self) -> String { - "single_merkle_proof/BeaconBlockBody".into() + "single_merkle_proof".into() } fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { - fork_name.capella_enabled() + fork_name.altair_enabled() } } diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 1f5a7dd997..3948708edf 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -955,13 +955,9 @@ fn kzg_recover_cells_and_proofs() { } #[test] -fn beacon_state_merkle_proof_validity() { - BeaconStateMerkleProofValidityHandler::::default().run(); -} - -#[test] -fn beacon_block_body_merkle_proof_validity() { - BeaconBlockBodyMerkleProofValidityHandler::::default().run(); +fn light_client_merkle_proof_validity() { + MerkleProofValidityHandler::::default().run(); + MerkleProofValidityHandler::::default().run(); } #[test] From b3b6aea1c54ebfa54aa8798c204375e2d2553cac Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Sun, 23 Feb 2025 18:36:13 -0800 Subject: [PATCH 02/13] Rust 1.85 lints (#7019) N/A 2 changes: 1. Replace Option::map_or(true, ...) with is_none_or(...) 2. Remove unnecessary `Into::into` blocks where the type conversion is apparent from the types --- .github/workflows/test-suite.yml | 4 +- .../src/attestation_verification.rs | 26 +++-- beacon_node/beacon_chain/src/beacon_chain.rs | 6 +- .../beacon_chain/src/block_times_cache.rs | 4 +- .../overflow_lru_cache.rs | 1 - .../beacon_chain/src/shuffling_cache.rs | 2 +- .../beacon_chain/src/validator_monitor.rs | 2 +- beacon_node/client/src/notifier.rs | 2 +- .../test_utils/execution_block_generator.rs | 2 +- .../genesis/src/eth1_genesis_service.rs | 2 +- beacon_node/http_api/src/lib.rs | 8 +- beacon_node/http_api/src/produce_block.rs | 4 +- beacon_node/http_api/src/validators.rs | 6 +- beacon_node/http_api/tests/tests.rs | 4 +- .../src/peer_manager/network_behaviour.rs | 4 +- .../src/network_beacon_processor/mod.rs | 4 +- .../src/bls_to_execution_changes.rs | 2 +- beacon_node/operation_pool/src/lib.rs | 2 +- .../operation_pool/src/reward_cache.rs | 2 +- beacon_node/store/src/hot_cold_store.rs | 4 +- beacon_node/store/src/lib.rs | 2 - .../src/validator_definitions.rs | 1 - .../block_signature_verifier.rs | 1 - consensus/types/src/beacon_block_body.rs | 1 + consensus/types/src/runtime_var_list.rs | 10 +- slasher/src/database.rs | 2 +- .../web3signer_tests/src/get_web3signer.rs | 95 ++++++++----------- testing/web3signer_tests/src/lib.rs | 9 +- validator_client/http_api/src/lib.rs | 2 +- .../src/slashing_database.rs | 2 +- .../validator_services/src/duties_service.rs | 6 +- .../validator_services/src/sync.rs | 4 +- validator_client/validator_store/src/lib.rs | 4 +- 33 files changed, 102 insertions(+), 128 deletions(-) diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 0f91c86617..574f1eda79 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -19,7 +19,9 @@ env: # Disable debug info (see https://github.com/sigp/lighthouse/issues/4005) RUSTFLAGS: "-D warnings -C debuginfo=0" # Prevent Github API rate limiting. - LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # NOTE: this token is a personal access token on Jimmy's account due to the default GITHUB_TOKEN + # not having access to other repositories. We should eventually devise a better solution here. + LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }} # Enable self-hosted runners for the sigp repo only. SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }} # Self-hosted runners need to reference a different host for `./watch` tests. diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index a70a2caa4f..00e8615487 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -1450,19 +1450,17 @@ where return Err(Error::UnknownTargetRoot(target.root)); } - chain - .with_committee_cache(target.root, attestation_epoch, |committee_cache, _| { - let committees_per_slot = committee_cache.committees_per_slot(); + chain.with_committee_cache(target.root, attestation_epoch, |committee_cache, _| { + let committees_per_slot = committee_cache.committees_per_slot(); - Ok(committee_cache - .get_beacon_committees_at_slot(attestation.data().slot) - .map(|committees| map_fn((committees, committees_per_slot))) - .unwrap_or_else(|_| { - Err(Error::NoCommitteeForSlotAndIndex { - slot: attestation.data().slot, - index: attestation.committee_index().unwrap_or(0), - }) - })) - }) - .map_err(BeaconChainError::from)? + Ok(committee_cache + .get_beacon_committees_at_slot(attestation.data().slot) + .map(|committees| map_fn((committees, committees_per_slot))) + .unwrap_or_else(|_| { + Err(Error::NoCommitteeForSlotAndIndex { + slot: attestation.data().slot, + index: attestation.committee_index().unwrap_or(0), + }) + })) + })? } diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ca21b519f1..eb731f7d6a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -6506,9 +6506,9 @@ impl BeaconChain { /// Returns `true` if the given slot is prior to the `bellatrix_fork_epoch`. pub fn slot_is_prior_to_bellatrix(&self, slot: Slot) -> bool { - self.spec.bellatrix_fork_epoch.map_or(true, |bellatrix| { - slot.epoch(T::EthSpec::slots_per_epoch()) < bellatrix - }) + self.spec + .bellatrix_fork_epoch + .is_none_or(|bellatrix| slot.epoch(T::EthSpec::slots_per_epoch()) < bellatrix) } /// Returns the value of `execution_optimistic` for `block`. diff --git a/beacon_node/beacon_chain/src/block_times_cache.rs b/beacon_node/beacon_chain/src/block_times_cache.rs index af122ccdc0..bd1adb7e40 100644 --- a/beacon_node/beacon_chain/src/block_times_cache.rs +++ b/beacon_node/beacon_chain/src/block_times_cache.rs @@ -173,7 +173,7 @@ impl BlockTimesCache { if block_times .timestamps .all_blobs_observed - .map_or(true, |prev| timestamp > prev) + .is_none_or(|prev| timestamp > prev) { block_times.timestamps.all_blobs_observed = Some(timestamp); } @@ -195,7 +195,7 @@ impl BlockTimesCache { .entry(block_root) .or_insert_with(|| BlockTimesCacheValue::new(slot)); let existing_timestamp = field(&mut block_times.timestamps); - if existing_timestamp.map_or(true, |prev| timestamp < prev) { + if existing_timestamp.is_none_or(|prev| timestamp < prev) { *existing_timestamp = Some(timestamp); } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index cd793c8394..da6372a43a 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -307,7 +307,6 @@ impl PendingComponents { .map(|b| b.map(|b| b.to_blob())) .take(num_blobs_expected) .collect::>>() - .map(Into::into) else { return Err(AvailabilityCheckError::Unexpected); }; diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index 67ca72254b..dec73a763f 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -138,7 +138,7 @@ impl ShufflingCache { .get(&key) // Replace the committee if it's not present or if it's a promise. A bird in the hand is // worth two in the promise-bush! - .map_or(true, CacheItem::is_promise) + .is_none_or(CacheItem::is_promise) { self.insert_cache_item( key, diff --git a/beacon_node/beacon_chain/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index f8a483c621..cb27f0727a 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -628,7 +628,7 @@ impl ValidatorMonitor { // the proposer shuffling cache lock when there are lots of missed blocks. if proposers_per_epoch .as_ref() - .map_or(true, |(_, cached_epoch)| *cached_epoch != slot_epoch) + .is_none_or(|(_, cached_epoch)| *cached_epoch != slot_epoch) { proposers_per_epoch = self .get_proposers_by_epoch_from_cache( diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 0c3b1578d6..c735d12538 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -187,7 +187,7 @@ pub fn spawn_notifier( let is_backfilling = matches!(current_sync_state, SyncState::BackFillSyncing { .. }); if is_backfilling && last_backfill_log_slot - .map_or(true, |slot| slot + BACKFILL_LOG_INTERVAL <= current_slot) + .is_none_or(|slot| slot + BACKFILL_LOG_INTERVAL <= current_slot) { last_backfill_log_slot = Some(current_slot); diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 9fa375b375..5a5c9e1fa9 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -448,7 +448,7 @@ impl ExecutionBlockGenerator { if self .head_block .as_ref() - .map_or(true, |head| head.block_hash() == last_block_hash) + .is_none_or(|head| head.block_hash() == last_block_hash) { self.head_block = Some(block.clone()); } diff --git a/beacon_node/genesis/src/eth1_genesis_service.rs b/beacon_node/genesis/src/eth1_genesis_service.rs index b5f4bd50ee..6e8f38627c 100644 --- a/beacon_node/genesis/src/eth1_genesis_service.rs +++ b/beacon_node/genesis/src/eth1_genesis_service.rs @@ -263,7 +263,7 @@ impl Eth1GenesisService { // again later. if eth1_service .highest_safe_block() - .map_or(true, |n| block.number > n) + .is_none_or(|n| block.number > n) { continue; } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index d6431fe729..5d75dc8c9a 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1939,10 +1939,10 @@ pub fn serve( query: api_types::AttestationPoolQuery| { task_spawner.blocking_response_task(Priority::P1, move || { let query_filter = |data: &AttestationData| { - query.slot.map_or(true, |slot| slot == data.slot) + query.slot.is_none_or(|slot| slot == data.slot) && query .committee_index - .map_or(true, |index| index == data.index) + .is_none_or(|index| index == data.index) }; let mut attestations = chain.op_pool.get_filtered_attestations(query_filter); @@ -3159,11 +3159,11 @@ pub fn serve( peer_info.connection_status(), ); - let state_matches = query.state.as_ref().map_or(true, |states| { + let state_matches = query.state.as_ref().is_none_or(|states| { states.iter().any(|state_param| *state_param == state) }); let direction_matches = - query.direction.as_ref().map_or(true, |directions| { + query.direction.as_ref().is_none_or(|directions| { directions.iter().any(|dir_param| *dir_param == direction) }); diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 0e24e8f175..22d6f0e7ae 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -147,7 +147,7 @@ pub async fn produce_blinded_block_v2( .produce_block_with_verification( randao_reveal, slot, - query.graffiti.map(Into::into), + query.graffiti, randao_verification, None, BlockProductionVersion::BlindedV2, @@ -178,7 +178,7 @@ pub async fn produce_block_v2( .produce_block_with_verification( randao_reveal, slot, - query.graffiti.map(Into::into), + query.graffiti, randao_verification, None, BlockProductionVersion::FullV2, diff --git a/beacon_node/http_api/src/validators.rs b/beacon_node/http_api/src/validators.rs index 93e63953ef..f3d78e6fcd 100644 --- a/beacon_node/http_api/src/validators.rs +++ b/beacon_node/http_api/src/validators.rs @@ -29,7 +29,7 @@ pub fn get_beacon_state_validators( .enumerate() // filter by validator id(s) if provided .filter(|(index, (validator, _))| { - ids_filter_set.as_ref().map_or(true, |ids_set| { + ids_filter_set.as_ref().is_none_or(|ids_set| { ids_set.contains(&ValidatorId::PublicKey(validator.pubkey)) || ids_set.contains(&ValidatorId::Index(*index as u64)) }) @@ -42,7 +42,7 @@ pub fn get_beacon_state_validators( far_future_epoch, ); - let status_matches = query_statuses.as_ref().map_or(true, |statuses| { + let status_matches = query_statuses.as_ref().is_none_or(|statuses| { statuses.contains(&status) || statuses.contains(&status.superstatus()) }); @@ -92,7 +92,7 @@ pub fn get_beacon_state_validator_balances( .enumerate() // filter by validator id(s) if provided .filter(|(index, (validator, _))| { - ids_filter_set.as_ref().map_or(true, |ids_set| { + ids_filter_set.as_ref().is_none_or(|ids_set| { ids_set.contains(&ValidatorId::PublicKey(validator.pubkey)) || ids_set.contains(&ValidatorId::Index(*index as u64)) }) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 556ddebad3..f7dbedc9ca 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2450,8 +2450,8 @@ impl ApiTester { }; let state_match = - states.map_or(true, |states| states.contains(&PeerState::Connected)); - let dir_match = dirs.map_or(true, |dirs| dirs.contains(&PeerDirection::Inbound)); + states.is_none_or(|states| states.contains(&PeerState::Connected)); + let dir_match = dirs.is_none_or(|dirs| dirs.contains(&PeerDirection::Inbound)); let mut expected_peers = Vec::new(); if state_match && dir_match { diff --git a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs index abafb200be..ee2a746142 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/network_behaviour.rs @@ -201,7 +201,7 @@ impl NetworkBehaviour for PeerManager { .peers .read() .peer_info(&peer_id) - .map_or(true, |peer| !peer.has_future_duty()) + .is_none_or(|peer| !peer.has_future_duty()) { return Err(ConnectionDenied::new( "Connection to peer rejected: too many connections", @@ -240,7 +240,7 @@ impl NetworkBehaviour for PeerManager { .peers .read() .peer_info(&peer_id) - .map_or(true, |peer| !peer.has_future_duty()) + .is_none_or(|peer| !peer.has_future_duty()) { return Err(ConnectionDenied::new( "Connection to peer rejected: too many connections", diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index c06a1f6ee3..13c7df8095 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -79,9 +79,7 @@ const BLOB_PUBLICATION_EXP_FACTOR: usize = 2; impl NetworkBeaconProcessor { fn try_send(&self, event: BeaconWorkEvent) -> Result<(), Error> { - self.beacon_processor_send - .try_send(event) - .map_err(Into::into) + self.beacon_processor_send.try_send(event) } /// Create a new `Work` event for some `SingleAttestation`. diff --git a/beacon_node/operation_pool/src/bls_to_execution_changes.rs b/beacon_node/operation_pool/src/bls_to_execution_changes.rs index cbab97e719..b36299b51a 100644 --- a/beacon_node/operation_pool/src/bls_to_execution_changes.rs +++ b/beacon_node/operation_pool/src/bls_to_execution_changes.rs @@ -112,7 +112,7 @@ impl BlsToExecutionChanges { head_state .validators() .get(validator_index as usize) - .map_or(true, |validator| { + .is_none_or(|validator| { let prune = validator.has_execution_withdrawal_credential(spec) && head_block .message() diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 835133a059..584a5f9f32 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -767,7 +767,7 @@ fn prune_validator_hash_map( && head_state .validators() .get(validator_index as usize) - .map_or(true, |validator| !prune_if(validator_index, validator)) + .is_none_or(|validator| !prune_if(validator_index, validator)) }); } diff --git a/beacon_node/operation_pool/src/reward_cache.rs b/beacon_node/operation_pool/src/reward_cache.rs index dd9902353f..adedcb5e39 100644 --- a/beacon_node/operation_pool/src/reward_cache.rs +++ b/beacon_node/operation_pool/src/reward_cache.rs @@ -83,7 +83,7 @@ impl RewardCache { if self .initialization .as_ref() - .map_or(true, |init| *init != new_init) + .is_none_or(|init| *init != new_init) { self.update_previous_epoch_participation(state) .map_err(OpPoolError::RewardCacheUpdatePrevEpoch)?; diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index e4a857b799..6dee0dc180 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -903,7 +903,7 @@ impl, Cold: ItemStore> HotColdDB state_root: &Hash256, summary: HotStateSummary, ) -> Result<(), Error> { - self.hot_db.put(state_root, &summary).map_err(Into::into) + self.hot_db.put(state_root, &summary) } /// Store a state in the store. @@ -1248,7 +1248,7 @@ impl, Cold: ItemStore> HotColdDB state_root.as_slice().to_vec(), )); - if slot.map_or(true, |slot| slot % E::slots_per_epoch() == 0) { + if slot.is_none_or(|slot| slot % E::slots_per_epoch() == 0) { key_value_batch.push(KeyValueStoreOp::DeleteKey( DBColumn::BeaconState, state_root.as_slice().to_vec(), diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 0cfc42ab15..2b5be03489 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -195,7 +195,6 @@ pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'stati let key = key.as_slice(); self.put_bytes(column, key, &item.as_store_bytes()) - .map_err(Into::into) } fn put_sync(&self, key: &Hash256, item: &I) -> Result<(), Error> { @@ -203,7 +202,6 @@ pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'stati let key = key.as_slice(); self.put_bytes_sync(column, key, &item.as_store_bytes()) - .map_err(Into::into) } /// Retrieve an item from `Self`. diff --git a/common/account_utils/src/validator_definitions.rs b/common/account_utils/src/validator_definitions.rs index 7337d6dfb4..25cf368c90 100644 --- a/common/account_utils/src/validator_definitions.rs +++ b/common/account_utils/src/validator_definitions.rs @@ -115,7 +115,6 @@ impl SigningDefinition { voting_keystore_password_path: Some(path), .. } => read_password_string(path) - .map(Into::into) .map(Option::Some) .map_err(Error::UnableToReadKeystorePassword), SigningDefinition::LocalKeystore { .. } => Err(Error::KeystoreWithoutPassword), diff --git a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs index 24cb51d755..8d4a544196 100644 --- a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -293,7 +293,6 @@ where )?); Ok(()) }) - .map_err(Error::into) } /// Includes all signatures in `self.block.body.voluntary_exits` for verification. diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index 410374c18f..10c1a11ede 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -971,6 +971,7 @@ impl From>> Option>, ) { + #[allow(clippy::useless_conversion)] // Not a useless conversion fn from(body: BeaconBlockBody>) -> Self { map_beacon_block_body!(body, |inner, cons| { let (block, payload) = inner.into(); diff --git a/consensus/types/src/runtime_var_list.rs b/consensus/types/src/runtime_var_list.rs index 857073b3b8..d6b1c10e99 100644 --- a/consensus/types/src/runtime_var_list.rs +++ b/consensus/types/src/runtime_var_list.rs @@ -134,13 +134,13 @@ impl RuntimeVariableList { ))); } - bytes - .chunks(::ssz_fixed_len()) - .try_fold(Vec::with_capacity(num_items), |mut vec, chunk| { + bytes.chunks(::ssz_fixed_len()).try_fold( + Vec::with_capacity(num_items), + |mut vec, chunk| { vec.push(::from_ssz_bytes(chunk)?); Ok(vec) - }) - .map(Into::into)? + }, + )? } else { ssz::decode_list_of_variable_length_items(bytes, Some(max_len))? }; diff --git a/slasher/src/database.rs b/slasher/src/database.rs index e2b49dca29..9e5e827034 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -665,7 +665,7 @@ impl SlasherDB { target: Epoch, prev_max_target: Option, ) -> Result, Error> { - if prev_max_target.map_or(true, |prev_max| target > prev_max) { + if prev_max_target.is_none_or(|prev_max| target > prev_max) { return Ok(None); } diff --git a/testing/web3signer_tests/src/get_web3signer.rs b/testing/web3signer_tests/src/get_web3signer.rs index 800feb204a..8c46a07a7d 100644 --- a/testing/web3signer_tests/src/get_web3signer.rs +++ b/testing/web3signer_tests/src/get_web3signer.rs @@ -1,65 +1,33 @@ //! This build script downloads the latest Web3Signer release and places it in the `OUT_DIR` so it //! can be used for integration testing. -use reqwest::{ - header::{self, HeaderValue}, - Client, -}; -use serde_json::Value; +use reqwest::Client; use std::env; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use zip::ZipArchive; /// Use `None` to download the latest Github release. /// Use `Some("21.8.1")` to download a specific version. const FIXED_VERSION_STRING: Option<&str> = None; -pub async fn download_binary(dest_dir: PathBuf, github_token: &str) { - let version_file = dest_dir.join("version"); - - let client = Client::builder() - // Github gives a 403 without a user agent. - .user_agent("web3signer_tests") - .build() - .unwrap(); - +// This function no longer makes any attempt to avoid downloads, because in practice we use it +// with a fresh temp directory every time we run the tests. We might want to change this in future +// to enable reproducible/offline testing. +pub async fn download_binary(dest_dir: PathBuf) { let version = if let Some(version) = FIXED_VERSION_STRING { version.to_string() } else if let Ok(env_version) = env::var("LIGHTHOUSE_WEB3SIGNER_VERSION") { env_version } else { - // Get the latest release of the web3 signer repo. - let mut token_header_value = HeaderValue::from_str(github_token).unwrap(); - token_header_value.set_sensitive(true); - let latest_response: Value = client - .get("https://api.github.com/repos/ConsenSys/web3signer/releases/latest") - .header(header::AUTHORIZATION, token_header_value) - .send() - .await - .unwrap() - .error_for_status() - .unwrap() - .json() - .await - .unwrap(); - latest_response - .get("tag_name") - .unwrap() - .as_str() - .unwrap() - .to_string() + // The Consenys artifact server resolves `latest` to the latest release. We previously hit + // the Github API to establish the version, but that is no longer necessary. + "latest".to_string() }; + eprintln!("Downloading web3signer version: {version}"); - if version_file.exists() && fs::read(&version_file).unwrap() == version.as_bytes() { - // The latest version is already downloaded, do nothing. - return; - } else { - // Ignore the result since we don't care if the version file already exists. - let _ = fs::remove_file(&version_file); - } - - // Download the latest release zip. + // Download the release zip. + let client = Client::builder().build().unwrap(); let zip_url = format!("https://artifacts.consensys.net/public/web3signer/raw/names/web3signer.zip/versions/{}/web3signer-{}.zip", version, version); let zip_response = client .get(zip_url) @@ -73,8 +41,9 @@ pub async fn download_binary(dest_dir: PathBuf, github_token: &str) { .unwrap(); // Write the zip to a file. - let zip_path = dest_dir.join(format!("{}.zip", version)); + let zip_path = dest_dir.join(format!("web3signer-{version}.zip")); fs::write(&zip_path, zip_response).unwrap(); + // Unzip the zip. let mut zip_file = fs::File::open(&zip_path).unwrap(); ZipArchive::new(&mut zip_file) @@ -88,15 +57,33 @@ pub async fn download_binary(dest_dir: PathBuf, github_token: &str) { if web3signer_dir.exists() { fs::remove_dir_all(&web3signer_dir).unwrap(); } - fs::rename( - dest_dir.join(format!("web3signer-{}", version)), - web3signer_dir, - ) - .unwrap(); - // Delete zip and unzipped dir. + let versioned_web3signer_dir = find_versioned_web3signer_dir(&dest_dir); + eprintln!( + "Renaming versioned web3signer dir at: {}", + versioned_web3signer_dir.display() + ); + + fs::rename(versioned_web3signer_dir, web3signer_dir).unwrap(); + + // Delete zip. fs::remove_file(&zip_path).unwrap(); - - // Update the version file to avoid duplicate downloads. - fs::write(&version_file, version).unwrap(); +} + +fn find_versioned_web3signer_dir(dest_dir: &Path) -> PathBuf { + for entry in fs::read_dir(dest_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + + if path + .file_name() + .and_then(|n| n.to_str()) + .map(|s| s.starts_with("web3signer-")) + .unwrap_or(false) + && entry.file_type().unwrap().is_dir() + { + return path; + } + } + panic!("no directory named web3signer-* found after ZIP extraction") } diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index e0dee9ceb4..659495b2b3 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -178,14 +178,7 @@ mod tests { pub async fn new(network: &str, listen_address: &str, listen_port: u16) -> Self { GET_WEB3SIGNER_BIN .get_or_init(|| async { - // Read a Github API token from the environment. This is intended to prevent rate-limits on CI. - // We use a name that is unlikely to accidentally collide with anything the user has configured. - let github_token = env::var("LIGHTHOUSE_GITHUB_TOKEN"); - download_binary( - TEMP_DIR.lock().path().to_path_buf(), - github_token.as_deref().unwrap_or(""), - ) - .await; + download_binary(TEMP_DIR.lock().path().to_path_buf()).await; }) .await; diff --git a/validator_client/http_api/src/lib.rs b/validator_client/http_api/src/lib.rs index 9c3e3da63d..ae50b6a927 100644 --- a/validator_client/http_api/src/lib.rs +++ b/validator_client/http_api/src/lib.rs @@ -767,7 +767,7 @@ pub fn serve( // Disabling an already disabled validator *with no other changes* is a // no-op. (Some(false), None) - if body.enabled.map_or(true, |enabled| !enabled) + if body.enabled.is_none_or(|enabled| !enabled) && body.gas_limit.is_none() && body.builder_boost_factor.is_none() && body.builder_proposals.is_none() diff --git a/validator_client/slashing_protection/src/slashing_database.rs b/validator_client/slashing_protection/src/slashing_database.rs index 71611339f9..f4c844d314 100644 --- a/validator_client/slashing_protection/src/slashing_database.rs +++ b/validator_client/slashing_protection/src/slashing_database.rs @@ -1113,7 +1113,7 @@ fn max_or(opt_x: Option, y: T) -> T { /// /// 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.map_or(true, |prev_val| new_val >= prev_val)) + 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. diff --git a/validator_client/validator_services/src/duties_service.rs b/validator_client/validator_services/src/duties_service.rs index 187eb4feb5..1c0fd338d2 100644 --- a/validator_client/validator_services/src/duties_service.rs +++ b/validator_client/validator_services/src/duties_service.rs @@ -843,10 +843,10 @@ async fn poll_beacon_attesters_for_epoch( local_pubkeys .iter() .filter(|pubkey| { - attesters.get(pubkey).map_or(true, |duties| { + attesters.get(pubkey).is_none_or(|duties| { duties .get(&epoch) - .map_or(true, |(prior, _)| *prior != dependent_root) + .is_none_or(|(prior, _)| *prior != dependent_root) }) }) .collect::>() @@ -974,7 +974,7 @@ fn get_uninitialized_validators( .filter(|pubkey| { attesters .get(pubkey) - .map_or(true, |duties| !duties.contains_key(epoch)) + .is_none_or(|duties| !duties.contains_key(epoch)) }) .filter_map(|pubkey| duties_service.validator_store.validator_index(pubkey)) .collect::>() diff --git a/validator_client/validator_services/src/sync.rs b/validator_client/validator_services/src/sync.rs index dd3e05088e..6c983b5430 100644 --- a/validator_client/validator_services/src/sync.rs +++ b/validator_client/validator_services/src/sync.rs @@ -297,7 +297,7 @@ pub async fn poll_sync_committee_duties( // If the Altair fork is yet to be activated, do not attempt to poll for duties. if spec .altair_fork_epoch - .map_or(true, |altair_epoch| current_epoch < altair_epoch) + .is_none_or(|altair_epoch| current_epoch < altair_epoch) { return Ok(()); } @@ -474,7 +474,7 @@ pub async fn poll_sync_committee_duties_for_period ValidatorStore { let mut validator_def = ValidatorDefinition::new_keystore_with_password( voting_keystore_path, password_storage, - graffiti.map(Into::into), + graffiti, suggested_fee_recipient, gas_limit, builder_proposals, @@ -327,7 +327,7 @@ impl ValidatorStore { .as_ref() // If there's no doppelganger service then we assume it is purposefully disabled and // declare that all keys are safe with regard to it. - .map_or(true, |doppelganger_service| { + .is_none_or(|doppelganger_service| { doppelganger_service .validator_status(validator_pubkey) .only_safe() From 522b3cbaab7ae5d4d3554768a90d83ce3fec48ae Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Sun, 23 Feb 2025 19:39:13 -0800 Subject: [PATCH 03/13] Fix builder API headers (#7009) Resolves https://github.com/sigp/lighthouse/issues/7000 Set the accept header on builder to the correct value when requesting ssz. This PR also adds a flag to disable ssz over the builder api altogether. In the case that builders/relays have an ssz bug, we can react quickly by asking clients to restart their nodes with the `--disable-ssz-builder` flag to force json. I'm not fully convinced if this is useful so open to removing it or opening another PR for it. Testing this currently. --- beacon_node/beacon_chain/src/test_utils.rs | 1 + beacon_node/builder_client/src/lib.rs | 106 ++++++++++++++++----- beacon_node/execution_layer/src/lib.rs | 22 ++++- beacon_node/src/cli.rs | 9 ++ beacon_node/src/config.rs | 2 + book/src/help_bn.md | 2 + lighthouse/tests/beacon_node.rs | 34 +++++++ 7 files changed, 149 insertions(+), 27 deletions(-) diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 8c9e3929f6..24c85b3e07 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -779,6 +779,7 @@ where SensitiveUrl::parse(format!("http://127.0.0.1:{port}").as_str()).unwrap(), None, None, + false, ) .unwrap(); diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index 5f64ac7e43..6d82542cef 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -29,6 +29,11 @@ pub const DEFAULT_GET_HEADER_TIMEOUT_MILLIS: u64 = 1000; /// Default user agent for HTTP requests. pub const DEFAULT_USER_AGENT: &str = lighthouse_version::VERSION; +/// The value we set on the `ACCEPT` http header to indicate a preference for ssz response. +pub const PREFERENCE_ACCEPT_VALUE: &str = "application/octet-stream;q=1.0,application/json;q=0.9"; +/// Only accept json responses. +pub const JSON_ACCEPT_VALUE: &str = "application/json"; + #[derive(Clone)] pub struct Timeouts { get_header: Duration, @@ -57,7 +62,12 @@ pub struct BuilderHttpClient { server: SensitiveUrl, timeouts: Timeouts, user_agent: String, - ssz_enabled: Arc, + /// Only use json for all requests/responses types. + disable_ssz: bool, + /// Indicates that the `get_header` response had content-type ssz + /// so we can set content-type header to ssz to make the `submit_blinded_blocks` + /// request. + ssz_available: Arc, } impl BuilderHttpClient { @@ -65,6 +75,7 @@ impl BuilderHttpClient { server: SensitiveUrl, user_agent: Option, builder_header_timeout: Option, + disable_ssz: bool, ) -> Result { let user_agent = user_agent.unwrap_or(DEFAULT_USER_AGENT.to_string()); let client = reqwest::Client::builder().user_agent(&user_agent).build()?; @@ -73,7 +84,8 @@ impl BuilderHttpClient { server, timeouts: Timeouts::new(builder_header_timeout), user_agent, - ssz_enabled: Arc::new(false.into()), + disable_ssz, + ssz_available: Arc::new(false.into()), }) } @@ -124,7 +136,7 @@ impl BuilderHttpClient { let Ok(Some(fork_name)) = self.fork_name_from_header(&headers) else { // if no fork version specified, attempt to fallback to JSON - self.ssz_enabled.store(false, Ordering::SeqCst); + self.ssz_available.store(false, Ordering::SeqCst); return serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson); }; @@ -132,7 +144,7 @@ impl BuilderHttpClient { match content_type { ContentType::Ssz => { - self.ssz_enabled.store(true, Ordering::SeqCst); + self.ssz_available.store(true, Ordering::SeqCst); T::from_ssz_bytes_by_fork(&response_bytes, fork_name) .map(|data| ForkVersionedResponse { version: Some(fork_name), @@ -142,15 +154,17 @@ impl BuilderHttpClient { .map_err(Error::InvalidSsz) } ContentType::Json => { - self.ssz_enabled.store(false, Ordering::SeqCst); + self.ssz_available.store(false, Ordering::SeqCst); serde_json::from_slice(&response_bytes).map_err(Error::InvalidJson) } } } /// Return `true` if the most recently received response from the builder had SSZ Content-Type. - pub fn is_ssz_enabled(&self) -> bool { - self.ssz_enabled.load(Ordering::SeqCst) + /// Return `false` otherwise. + /// Also returns `false` if we have explicitly disabled ssz. + pub fn is_ssz_available(&self) -> bool { + !self.disable_ssz && self.ssz_available.load(Ordering::SeqCst) } async fn get_with_timeout( @@ -213,7 +227,7 @@ impl BuilderHttpClient { &self, url: U, ssz_body: Vec, - mut headers: HeaderMap, + headers: HeaderMap, timeout: Option, ) -> Result { let mut builder = self.client.post(url); @@ -221,11 +235,6 @@ impl BuilderHttpClient { builder = builder.timeout(timeout); } - headers.insert( - CONTENT_TYPE_HEADER, - HeaderValue::from_static(SSZ_CONTENT_TYPE_HEADER), - ); - let response = builder .headers(headers) .body(ssz_body) @@ -292,9 +301,21 @@ impl BuilderHttpClient { .push("blinded_blocks"); let mut headers = HeaderMap::new(); - if let Ok(value) = HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) { - headers.insert(CONSENSUS_VERSION_HEADER, value); - } + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_str(SSZ_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + ACCEPT, + HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); let result = self .post_ssz_with_raw_response( @@ -326,9 +347,21 @@ impl BuilderHttpClient { .push("blinded_blocks"); let mut headers = HeaderMap::new(); - if let Ok(value) = HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) { - headers.insert(CONSENSUS_VERSION_HEADER, value); - } + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string()) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + CONTENT_TYPE_HEADER, + HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + headers.insert( + ACCEPT, + HeaderValue::from_str(JSON_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); Ok(self .post_with_raw_response( @@ -362,12 +395,20 @@ impl BuilderHttpClient { .push(pubkey.as_hex_string().as_str()); let mut headers = HeaderMap::new(); - if let Ok(ssz_content_type_header) = HeaderValue::from_str(&format!( - "{}; q=1.0,{}; q=0.9", - SSZ_CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER - )) { - headers.insert(ACCEPT, ssz_content_type_header); - }; + if self.disable_ssz { + headers.insert( + ACCEPT, + HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + } else { + // Indicate preference for ssz response in the accept header + headers.insert( + ACCEPT, + HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE) + .map_err(|e| Error::InvalidHeaders(format!("{}", e)))?, + ); + } let resp = self .get_with_header(path, self.timeouts.get_header, headers) @@ -395,3 +436,18 @@ impl BuilderHttpClient { .await } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_headers_no_panic() { + for fork in ForkName::list_all() { + assert!(HeaderValue::from_str(&fork.to_string()).is_ok()); + } + assert!(HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE).is_ok()); + assert!(HeaderValue::from_str(JSON_ACCEPT_VALUE).is_ok()); + assert!(HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER).is_ok()); + } +} diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 6e5e4fca01..4fd7188c20 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -441,6 +441,8 @@ pub struct Config { pub builder_header_timeout: Option, /// User agent to send with requests to the builder API. pub builder_user_agent: Option, + /// Disable ssz requests on builder. Only use json. + pub disable_builder_ssz_requests: bool, /// JWT secret for the above endpoint running the engine api. pub secret_file: Option, /// The default fee recipient to use on the beacon node if none if provided from @@ -470,6 +472,7 @@ impl ExecutionLayer { builder_url, builder_user_agent, builder_header_timeout, + disable_builder_ssz_requests, secret_file, suggested_fee_recipient, jwt_id, @@ -539,7 +542,12 @@ impl ExecutionLayer { }; if let Some(builder_url) = builder_url { - el.set_builder_url(builder_url, builder_user_agent, builder_header_timeout)?; + el.set_builder_url( + builder_url, + builder_user_agent, + builder_header_timeout, + disable_builder_ssz_requests, + )?; } Ok(el) @@ -562,11 +570,13 @@ impl ExecutionLayer { builder_url: SensitiveUrl, builder_user_agent: Option, builder_header_timeout: Option, + disable_ssz: bool, ) -> Result<(), Error> { let builder_client = BuilderHttpClient::new( builder_url.clone(), builder_user_agent, builder_header_timeout, + disable_ssz, ) .map_err(Error::Builder)?; info!( @@ -574,6 +584,7 @@ impl ExecutionLayer { "Using external block builder"; "builder_url" => ?builder_url, "local_user_agent" => builder_client.get_user_agent(), + "ssz_disabled" => disable_ssz ); self.inner.builder.swap(Some(Arc::new(builder_client))); Ok(()) @@ -1901,7 +1912,14 @@ impl ExecutionLayer { if let Some(builder) = self.builder() { let (payload_result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async { - if builder.is_ssz_enabled() { + let ssz_enabled = builder.is_ssz_available(); + debug!( + self.log(), + "Calling submit_blinded_block on builder"; + "block_root" => ?block_root, + "ssz" => ssz_enabled + ); + if ssz_enabled { builder .post_builder_blinded_blocks_ssz(block) .await diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 4c2daecdd3..2c8b271bd2 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1460,6 +1460,15 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("builder-disable-ssz") + .long("builder-disable-ssz") + .value_name("BOOLEAN") + .help("Disables sending requests using SSZ over the builder API.") + .requires("builder") + .action(ArgAction::SetTrue) + .display_order(0) + ) .arg( Arg::new("reset-payload-statuses") .long("reset-payload-statuses") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 24d569bea2..84320762d6 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -346,6 +346,8 @@ pub fn get_config( el_config.builder_header_timeout = clap_utils::parse_optional(cli_args, "builder-header-timeout")? .map(Duration::from_millis); + + el_config.disable_builder_ssz_requests = cli_args.get_flag("builder-disable-ssz"); } // Set config values from parse values. diff --git a/book/src/help_bn.md b/book/src/help_bn.md index cbcb1ec5a3..79c8d8ead8 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -28,6 +28,8 @@ Options: network. Multiaddr is also supported. --builder The URL of a service compatible with the MEV-boost API. + --builder-disable-ssz + Disables sending requests using SSZ over the builder API. --builder-fallback-epochs-since-finalization If this node is proposing a block and the chain has not finalized within this number of epochs, it will NOT query any connected diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 03314930b9..da10c2c4bd 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -720,6 +720,40 @@ fn builder_user_agent() { ); } +#[test] +fn test_builder_disable_ssz_flag() { + run_payload_builder_flag_test_with_config( + "builder", + "http://meow.cats", + None, + None, + |config| { + assert!( + !config + .execution_layer + .as_ref() + .unwrap() + .disable_builder_ssz_requests, + ); + }, + ); + run_payload_builder_flag_test_with_config( + "builder", + "http://meow.cats", + Some("builder-disable-ssz"), + None, + |config| { + assert!( + config + .execution_layer + .as_ref() + .unwrap() + .disable_builder_ssz_requests, + ); + }, + ); +} + fn run_jwt_optional_flags_test(jwt_flag: &str, jwt_id_flag: &str, jwt_version_flag: &str) { use sensitive_url::SensitiveUrl; From b4be5141823f9e828daf185c83e59c4c08e92ec1 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 24 Feb 2025 00:39:15 -0300 Subject: [PATCH 04/13] Add spamoor_blob in network_params.yaml (#7012) Have blobs by default in deneb runs of kurtosis --- scripts/local_testnet/network_params.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/local_testnet/network_params.yaml b/scripts/local_testnet/network_params.yaml index b53d88e52c..87ffeb8d22 100644 --- a/scripts/local_testnet/network_params.yaml +++ b/scripts/local_testnet/network_params.yaml @@ -14,4 +14,5 @@ global_log_level: debug snooper_enabled: false additional_services: - dora + - spamoor_blob - prometheus_grafana From 01df433dfd026a7de25a5b1275724a7e5a554a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Oliveira?= Date: Mon, 24 Feb 2025 03:39:18 +0000 Subject: [PATCH 05/13] update codeowners, to be more specific (#7021) I keep being notified for PR's like https://github.com/sigp/lighthouse/pull/7009 where it doesn't touch the specified directories in the `CODEOWNERS` file. After reading the [docs](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) not having a forward slash in beginning of the path means: > In this example, @octocat owns any file in an apps directory > anywhere in your repository. whereas with the slashes: > In this example, @doctocat owns any file in the `/docs` > directory in the root of your repository and any of its > subdirectories. this update makes it more rigid for the files in the jxs ownership --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f9478d1369..a8919337a9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -beacon_node/network/ @jxs -beacon_node/lighthouse_network/ @jxs +/beacon_node/network/ @jxs +/beacon_node/lighthouse_network/ @jxs From 60964fc7b5304152c784db706e00354db0961292 Mon Sep 17 00:00:00 2001 From: Daniel Knopik <107140945+dknopik@users.noreply.github.com> Date: Mon, 24 Feb 2025 04:39:20 +0100 Subject: [PATCH 06/13] Expose blst internals (#6829) --- crypto/bls/src/generic_secret_key.rs | 22 +++++++++++++++ crypto/bls/src/generic_signature.rs | 41 ++++++++++++++++++++++++++-- crypto/bls/src/impls/blst.rs | 10 ++++++- crypto/bls/src/impls/fake_crypto.rs | 12 +++++++- crypto/bls/src/lib.rs | 5 +++- crypto/bls/tests/tests.rs | 17 +++++++++++- 6 files changed, 101 insertions(+), 6 deletions(-) diff --git a/crypto/bls/src/generic_secret_key.rs b/crypto/bls/src/generic_secret_key.rs index a0a4331110..62bfc1467d 100644 --- a/crypto/bls/src/generic_secret_key.rs +++ b/crypto/bls/src/generic_secret_key.rs @@ -61,6 +61,11 @@ where GenericPublicKey::from_point(self.point.public_key()) } + /// Returns a reference to the underlying BLS point. + pub fn point(&self) -> &Sec { + &self.point + } + /// Serialize `self` as compressed bytes. /// /// ## Note @@ -89,3 +94,20 @@ where } } } + +impl GenericSecretKey +where + Sig: TSignature, + Pub: TPublicKey, + Sec: TSecretKey + Clone, +{ + /// Instantiates `Self` from a `point`. + /// Takes a reference, as moves might accidentally leave behind key material + pub fn from_point(point: &Sec) -> Self { + Self { + point: point.clone(), + _phantom_signature: PhantomData, + _phantom_public_key: PhantomData, + } + } +} diff --git a/crypto/bls/src/generic_signature.rs b/crypto/bls/src/generic_signature.rs index 05e0a222bd..0b375d3edd 100644 --- a/crypto/bls/src/generic_signature.rs +++ b/crypto/bls/src/generic_signature.rs @@ -14,6 +14,9 @@ use tree_hash::TreeHash; /// The byte-length of a BLS signature when serialized in compressed form. pub const SIGNATURE_BYTES_LEN: usize = 96; +/// The byte-length of a BLS signature when serialized in uncompressed form. +pub const SIGNATURE_UNCOMPRESSED_BYTES_LEN: usize = 192; + /// Represents the signature at infinity. pub const INFINITY_SIGNATURE: [u8; SIGNATURE_BYTES_LEN] = [ 0xc0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -22,6 +25,16 @@ pub const INFINITY_SIGNATURE: [u8; SIGNATURE_BYTES_LEN] = [ 0, ]; +pub const INFINITY_SIGNATURE_UNCOMPRESSED: [u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN] = [ + 0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, +]; + /// The compressed bytes used to represent `GenericSignature::empty()`. pub const NONE_SIGNATURE: [u8; SIGNATURE_BYTES_LEN] = [0; SIGNATURE_BYTES_LEN]; @@ -31,9 +44,15 @@ pub trait TSignature: Sized + Clone { /// Serialize `self` as compressed bytes. fn serialize(&self) -> [u8; SIGNATURE_BYTES_LEN]; + /// Serialize `self` as uncompressed bytes. + fn serialize_uncompressed(&self) -> [u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN]; + /// Deserialize `self` from compressed bytes. fn deserialize(bytes: &[u8]) -> Result; + /// Serialize `self` from uncompressed bytes. + fn deserialize_uncompressed(bytes: &[u8]) -> Result; + /// Returns `true` if `self` is a signature across `msg` by `pubkey`. fn verify(&self, pubkey: &GenericPublicKey, msg: Hash256) -> bool; } @@ -93,12 +112,12 @@ where } /// Returns a reference to the underlying BLS point. - pub(crate) fn point(&self) -> Option<&Sig> { + pub fn point(&self) -> Option<&Sig> { self.point.as_ref() } /// Instantiates `Self` from a `point`. - pub(crate) fn from_point(point: Sig, is_infinity: bool) -> Self { + pub fn from_point(point: Sig, is_infinity: bool) -> Self { Self { point: Some(point), is_infinity, @@ -115,6 +134,13 @@ where } } + /// Serialize `self` as compressed bytes. + pub fn serialize_uncompressed(&self) -> Option<[u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN]> { + self.point + .as_ref() + .map(|point| point.serialize_uncompressed()) + } + /// Deserialize `self` from compressed bytes. pub fn deserialize(bytes: &[u8]) -> Result { let point = if bytes == &NONE_SIGNATURE[..] { @@ -129,6 +155,17 @@ where _phantom: PhantomData, }) } + + /// Deserialize `self` from uncompressed bytes. + pub fn deserialize_uncompressed(bytes: &[u8]) -> Result { + // The "none signature" is a beacon chain concept. As we never directly deal with + // uncompressed signatures on the beacon chain, it does not apply here. + Ok(Self { + point: Some(Sig::deserialize_uncompressed(bytes)?), + is_infinity: bytes == &INFINITY_SIGNATURE_UNCOMPRESSED[..], + _phantom: PhantomData, + }) + } } impl GenericSignature diff --git a/crypto/bls/src/impls/blst.rs b/crypto/bls/src/impls/blst.rs index baa704e05a..6ca0fe09b2 100644 --- a/crypto/bls/src/impls/blst.rs +++ b/crypto/bls/src/impls/blst.rs @@ -5,7 +5,7 @@ use crate::{ GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, }, generic_secret_key::TSecretKey, - generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, + generic_signature::{TSignature, SIGNATURE_BYTES_LEN, SIGNATURE_UNCOMPRESSED_BYTES_LEN}, BlstError, Error, Hash256, ZeroizeHash, INFINITY_SIGNATURE, }; pub use blst::min_pk as blst_core; @@ -189,10 +189,18 @@ impl TSignature for blst_core::Signature { self.to_bytes() } + fn serialize_uncompressed(&self) -> [u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN] { + self.serialize() + } + fn deserialize(bytes: &[u8]) -> Result { Self::from_bytes(bytes).map_err(Into::into) } + fn deserialize_uncompressed(bytes: &[u8]) -> Result { + Self::deserialize(bytes).map_err(Into::into) + } + fn verify(&self, pubkey: &blst_core::PublicKey, msg: Hash256) -> bool { // Public keys have already been checked for subgroup and infinity // Check Signature inside function for subgroup diff --git a/crypto/bls/src/impls/fake_crypto.rs b/crypto/bls/src/impls/fake_crypto.rs index a09fb347e6..7273697597 100644 --- a/crypto/bls/src/impls/fake_crypto.rs +++ b/crypto/bls/src/impls/fake_crypto.rs @@ -5,7 +5,7 @@ use crate::{ GenericPublicKey, TPublicKey, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, }, generic_secret_key::{TSecretKey, SECRET_KEY_BYTES_LEN}, - generic_signature::{TSignature, SIGNATURE_BYTES_LEN}, + generic_signature::{TSignature, SIGNATURE_BYTES_LEN, SIGNATURE_UNCOMPRESSED_BYTES_LEN}, Error, Hash256, ZeroizeHash, INFINITY_PUBLIC_KEY, INFINITY_SIGNATURE, }; @@ -106,12 +106,22 @@ impl TSignature for Signature { self.0 } + fn serialize_uncompressed(&self) -> [u8; SIGNATURE_UNCOMPRESSED_BYTES_LEN] { + let mut ret = [0; SIGNATURE_UNCOMPRESSED_BYTES_LEN]; + ret[0..SIGNATURE_BYTES_LEN].copy_from_slice(&self.0); + ret + } + fn deserialize(bytes: &[u8]) -> Result { let mut signature = Self::infinity(); signature.0[..].copy_from_slice(&bytes[0..SIGNATURE_BYTES_LEN]); Ok(signature) } + fn deserialize_uncompressed(bytes: &[u8]) -> Result { + Self::deserialize(bytes) + } + fn verify(&self, _pubkey: &PublicKey, _msg: Hash256) -> bool { true } diff --git a/crypto/bls/src/lib.rs b/crypto/bls/src/lib.rs index 6ea85548c0..13b6dc2f2c 100644 --- a/crypto/bls/src/lib.rs +++ b/crypto/bls/src/lib.rs @@ -37,7 +37,10 @@ pub use generic_public_key::{ INFINITY_PUBLIC_KEY, PUBLIC_KEY_BYTES_LEN, PUBLIC_KEY_UNCOMPRESSED_BYTES_LEN, }; pub use generic_secret_key::SECRET_KEY_BYTES_LEN; -pub use generic_signature::{INFINITY_SIGNATURE, SIGNATURE_BYTES_LEN}; +pub use generic_signature::{ + INFINITY_SIGNATURE, INFINITY_SIGNATURE_UNCOMPRESSED, SIGNATURE_BYTES_LEN, + SIGNATURE_UNCOMPRESSED_BYTES_LEN, +}; pub use get_withdrawal_credentials::get_withdrawal_credentials; pub use zeroize_hash::ZeroizeHash; diff --git a/crypto/bls/tests/tests.rs b/crypto/bls/tests/tests.rs index 26215771b5..611dabbd64 100644 --- a/crypto/bls/tests/tests.rs +++ b/crypto/bls/tests/tests.rs @@ -1,4 +1,7 @@ -use bls::{FixedBytesExtended, Hash256, INFINITY_SIGNATURE, SECRET_KEY_BYTES_LEN}; +use bls::{ + FixedBytesExtended, Hash256, INFINITY_SIGNATURE, INFINITY_SIGNATURE_UNCOMPRESSED, + SECRET_KEY_BYTES_LEN, +}; use ssz::{Decode, Encode}; use std::borrow::Cow; use std::fmt::Debug; @@ -37,6 +40,18 @@ macro_rules! test_suite { assert!(AggregateSignature::infinity().is_infinity()); } + #[test] + fn infinity_sig_serializations_match() { + let sig = Signature::deserialize(&INFINITY_SIGNATURE).unwrap(); + assert_eq!( + sig.serialize_uncompressed().unwrap(), + INFINITY_SIGNATURE_UNCOMPRESSED + ); + let sig = + Signature::deserialize_uncompressed(&INFINITY_SIGNATURE_UNCOMPRESSED).unwrap(); + assert_eq!(sig.serialize(), INFINITY_SIGNATURE); + } + #[test] fn ssz_round_trip_multiple_types() { let mut agg_sig = AggregateSignature::infinity(); From 3fab6a2c0ba702c20c38d3083a3c533ea647dcac Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Mon, 24 Feb 2025 01:47:09 -0300 Subject: [PATCH 07/13] Block availability data enum (#6866) PeerDAS has undergone multiple refactors + the blending with the get_blobs optimization has generated technical debt. A function signature like this https://github.com/sigp/lighthouse/blob/f008b84079bbb6eb86de22bb3421dfc8263a5650/beacon_node/beacon_chain/src/beacon_chain.rs#L7171-L7178 Allows at least the following combination of states: - blobs: Some / None - data_columns: Some / None - data_column_recv: Some / None - Block has data? Yes / No - Block post-PeerDAS? Yes / No In reality, we don't have that many possible states, only: - `NoData`: pre-deneb, pre-PeerDAS with 0 blobs or post-PeerDAS with 0 blobs - `Blobs(BlobSidecarList)`: post-Deneb pre-PeerDAS with > 0 blobs - `DataColumns(DataColumnSidecarList)`: post-PeerDAS with > 0 blobs - `DataColumnsRecv(oneshot::Receiver>)`: post-PeerDAS with > 0 blobs, but we obtained the columns via reconstruction ^ this are the variants of the new `AvailableBlockData` enum So we go from 2^5 states to 4 well-defined. Downstream code benefits nicely from this clarity and I think it makes the whole feature much more maintainable. Currently `is_available` returns a bool, and then we construct the available block in `make_available`. In a way the availability condition is duplicated in both functions. Instead, this PR constructs `AvailableBlockData` in `is_available` so the availability conditions are written once ```rust if let Some(block_data) = is_available(..) { let available_block = make_available(block_data); } ``` --- beacon_node/beacon_chain/src/beacon_chain.rs | 110 ++--- .../beacon_chain/src/block_verification.rs | 1 - .../src/block_verification_types.rs | 16 +- .../src/data_availability_checker.rs | 153 ++++--- .../src/data_availability_checker/error.rs | 4 +- .../overflow_lru_cache.rs | 429 ++++++++++-------- .../state_lru_cache.rs | 1 - .../beacon_chain/src/early_attester_cache.rs | 17 +- .../beacon_chain/src/historical_blocks.rs | 38 +- .../tests/attestation_production.rs | 4 +- beacon_node/beacon_chain/tests/store_tests.rs | 20 +- 11 files changed, 432 insertions(+), 361 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b89dbe3dca..ad31e085ca 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -21,8 +21,8 @@ use crate::block_verification_types::{ pub use crate::canonical_head::CanonicalHead; use crate::chain_config::ChainConfig; use crate::data_availability_checker::{ - Availability, AvailabilityCheckError, AvailableBlock, DataAvailabilityChecker, - DataColumnReconstructionResult, + Availability, AvailabilityCheckError, AvailableBlock, AvailableBlockData, + DataAvailabilityChecker, DataColumnReconstructionResult, }; use crate::data_column_verification::{GossipDataColumnError, GossipVerifiedDataColumn}; use crate::early_attester_cache::EarlyAttesterCache; @@ -3169,7 +3169,14 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); + // process_engine_blobs is called for both pre and post PeerDAS. However, post PeerDAS + // consumers don't expect the blobs event to fire erratically. + if !self + .spec + .is_peer_das_enabled_for_epoch(slot.epoch(T::EthSpec::slots_per_epoch())) + { + self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); + } let r = self .check_engine_blob_availability_and_import(slot, block_root, blobs, data_column_recv) @@ -3640,9 +3647,12 @@ impl BeaconChain { data_column_recv: Option>>, ) -> Result { self.check_blobs_for_slashability(block_root, &blobs)?; - let availability = - self.data_availability_checker - .put_engine_blobs(block_root, blobs, data_column_recv)?; + let availability = self.data_availability_checker.put_engine_blobs( + block_root, + slot.epoch(T::EthSpec::slots_per_epoch()), + blobs, + data_column_recv, + )?; self.process_availability(slot, availability, || Ok(())) .await @@ -3727,7 +3737,6 @@ impl BeaconChain { parent_eth1_finalization_data, confirmed_state_roots, consensus_context, - data_column_recv, } = import_data; // Record the time at which this block's blobs became available. @@ -3755,7 +3764,6 @@ impl BeaconChain { parent_block, parent_eth1_finalization_data, consensus_context, - data_column_recv, ) }, "payload_verification_handle", @@ -3794,7 +3802,6 @@ impl BeaconChain { parent_block: SignedBlindedBeaconBlock, parent_eth1_finalization_data: Eth1FinalizationData, mut consensus_context: ConsensusContext, - data_column_recv: Option>>, ) -> Result { // ----------------------------- BLOCK NOT YET ATTESTABLE ---------------------------------- // Everything in this initial section is on the hot path between processing the block and @@ -3892,7 +3899,7 @@ impl BeaconChain { if let Some(proto_block) = fork_choice.get_block(&block_root) { if let Err(e) = self.early_attester_cache.add_head_block( block_root, - signed_block.clone(), + &signed_block, proto_block, &state, &self.spec, @@ -3961,15 +3968,9 @@ impl BeaconChain { // If the write fails, revert fork choice to the version from disk, else we can // end up with blocks in fork choice that are missing from disk. // See https://github.com/sigp/lighthouse/issues/2028 - let (_, signed_block, blobs, data_columns) = signed_block.deconstruct(); + let (_, signed_block, block_data) = signed_block.deconstruct(); - match self.get_blobs_or_columns_store_op( - block_root, - signed_block.epoch(), - blobs, - data_columns, - data_column_recv, - ) { + match self.get_blobs_or_columns_store_op(block_root, block_data) { Ok(Some(blobs_or_columns_store_op)) => { ops.push(blobs_or_columns_store_op); } @@ -7218,29 +7219,34 @@ impl BeaconChain { } } - fn get_blobs_or_columns_store_op( + pub(crate) fn get_blobs_or_columns_store_op( &self, block_root: Hash256, - block_epoch: Epoch, - blobs: Option>, - data_columns: Option>, - data_column_recv: Option>>, + block_data: AvailableBlockData, ) -> Result>, String> { - if self.spec.is_peer_das_enabled_for_epoch(block_epoch) { - // TODO(das) we currently store all subnet sampled columns. Tracking issue to exclude non - // custody columns: https://github.com/sigp/lighthouse/issues/6465 - let custody_columns_count = self.data_availability_checker.get_sampling_column_count(); + // TODO(das) we currently store all subnet sampled columns. Tracking issue to exclude non + // custody columns: https://github.com/sigp/lighthouse/issues/6465 + let _custody_columns_count = self.data_availability_checker.get_sampling_column_count(); - let custody_columns_available = data_columns - .as_ref() - .as_ref() - .is_some_and(|columns| columns.len() == custody_columns_count); - - let data_columns_to_persist = if custody_columns_available { - // If the block was made available via custody columns received from gossip / rpc, use them - // since we already have them. - data_columns - } else if let Some(data_column_recv) = data_column_recv { + match block_data { + AvailableBlockData::NoData => Ok(None), + AvailableBlockData::Blobs(blobs) => { + debug!( + self.log, "Writing blobs to store"; + "block_root" => %block_root, + "count" => blobs.len(), + ); + Ok(Some(StoreOp::PutBlobs(block_root, blobs))) + } + AvailableBlockData::DataColumns(data_columns) => { + debug!( + self.log, "Writing data columns to store"; + "block_root" => %block_root, + "count" => data_columns.len(), + ); + Ok(Some(StoreOp::PutDataColumns(block_root, data_columns))) + } + AvailableBlockData::DataColumnsRecv(data_column_recv) => { // Blobs were available from the EL, in this case we wait for the data columns to be computed (blocking). let _column_recv_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DATA_COLUMNS_WAIT); @@ -7250,34 +7256,18 @@ impl BeaconChain { let computed_data_columns = data_column_recv .blocking_recv() .map_err(|e| format!("Did not receive data columns from sender: {e:?}"))?; - Some(computed_data_columns) - } else { - // No blobs in the block. - None - }; - - if let Some(data_columns) = data_columns_to_persist { - if !data_columns.is_empty() { - debug!( - self.log, "Writing data_columns to store"; - "block_root" => %block_root, - "count" => data_columns.len(), - ); - return Ok(Some(StoreOp::PutDataColumns(block_root, data_columns))); - } - } - } else if let Some(blobs) = blobs { - if !blobs.is_empty() { debug!( - self.log, "Writing blobs to store"; + self.log, "Writing data columns to store"; "block_root" => %block_root, - "count" => blobs.len(), + "count" => computed_data_columns.len(), ); - return Ok(Some(StoreOp::PutBlobs(block_root, blobs))); + // TODO(das): Store only this node's custody columns + Ok(Some(StoreOp::PutDataColumns( + block_root, + computed_data_columns, + ))) } } - - Ok(None) } } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 1265276376..9a8def585f 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1707,7 +1707,6 @@ impl ExecutionPendingBlock { parent_eth1_finalization_data, confirmed_state_roots, consensus_context, - data_column_recv: None, }, payload_verification_handle, }) diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index 38d0fc708c..07ffae7712 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -7,11 +7,10 @@ use derivative::Derivative; use state_processing::ConsensusContext; use std::fmt::{Debug, Formatter}; use std::sync::Arc; -use tokio::sync::oneshot; use types::blob_sidecar::BlobIdentifier; use types::{ - BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, DataColumnSidecarList, - Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, + BeaconBlockRef, BeaconState, BlindedPayload, BlobSidecarList, ChainSpec, Epoch, EthSpec, + Hash256, RuntimeVariableList, SignedBeaconBlock, SignedBeaconBlockHeader, Slot, }; /// A block that has been received over RPC. It has 2 internal variants: @@ -265,7 +264,6 @@ impl ExecutedBlock { /// A block that has completed all pre-deneb block processing checks including verification /// by an EL client **and** has all requisite blob data to be imported into fork choice. -#[derive(PartialEq)] pub struct AvailableExecutedBlock { pub block: AvailableBlock, pub import_data: BlockImportData, @@ -338,8 +336,7 @@ impl AvailabilityPendingExecutedBlock { } } -#[derive(Debug, Derivative)] -#[derivative(PartialEq)] +#[derive(Debug, PartialEq)] pub struct BlockImportData { pub block_root: Hash256, pub state: BeaconState, @@ -347,12 +344,6 @@ pub struct BlockImportData { pub parent_eth1_finalization_data: Eth1FinalizationData, pub confirmed_state_roots: Vec, pub consensus_context: ConsensusContext, - #[derivative(PartialEq = "ignore")] - /// An optional receiver for `DataColumnSidecarList`. - /// - /// This field is `Some` when data columns are being computed asynchronously. - /// The resulting `DataColumnSidecarList` will be sent through this receiver. - pub data_column_recv: Option>>, } impl BlockImportData { @@ -371,7 +362,6 @@ impl BlockImportData { }, confirmed_state_roots: vec![], consensus_context: ConsensusContext::new(Slot::new(0)), - data_column_recv: None, } } } diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index f10d59ca1a..875645ee9f 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -91,7 +91,6 @@ pub enum DataColumnReconstructionResult { /// /// Indicates if the block is fully `Available` or if we need blobs or blocks /// to "complete" the requirements for an `AvailableBlock`. -#[derive(PartialEq)] pub enum Availability { MissingComponents(Hash256), Available(Box>), @@ -219,7 +218,7 @@ impl DataAvailabilityChecker { .map_err(AvailabilityCheckError::InvalidBlobs)?; self.availability_cache - .put_kzg_verified_blobs(block_root, verified_blobs, None, &self.log) + .put_kzg_verified_blobs(block_root, verified_blobs, &self.log) } /// Put a list of custody columns received via RPC into the availability cache. This performs KZG @@ -253,23 +252,29 @@ impl DataAvailabilityChecker { pub fn put_engine_blobs( &self, block_root: Hash256, + block_epoch: Epoch, blobs: FixedBlobSidecarList, - data_column_recv: Option>>, + data_columns_recv: Option>>, ) -> Result, AvailabilityCheckError> { - let seen_timestamp = self - .slot_clock - .now_duration() - .ok_or(AvailabilityCheckError::SlotClockError)?; - - let verified_blobs = - KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp); - - self.availability_cache.put_kzg_verified_blobs( - block_root, - verified_blobs, - data_column_recv, - &self.log, - ) + // `data_columns_recv` is always Some if block_root is post-PeerDAS + if let Some(data_columns_recv) = data_columns_recv { + self.availability_cache.put_computed_data_columns_recv( + block_root, + block_epoch, + data_columns_recv, + &self.log, + ) + } else { + let seen_timestamp = self + .slot_clock + .now_duration() + .ok_or(AvailabilityCheckError::SlotClockError)?; + self.availability_cache.put_kzg_verified_blobs( + block_root, + KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp), + &self.log, + ) + } } /// Check if we've cached other blobs for this block. If it completes a set and we also @@ -284,7 +289,6 @@ impl DataAvailabilityChecker { self.availability_cache.put_kzg_verified_blobs( gossip_blob.block_root(), vec![gossip_blob.into_inner()], - None, &self.log, ) } @@ -338,15 +342,14 @@ impl DataAvailabilityChecker { ) -> Result, AvailabilityCheckError> { let (block_root, block, blobs, data_columns) = block.deconstruct(); if self.blobs_required_for_block(&block) { - return if let Some(blob_list) = blobs.as_ref() { + return if let Some(blob_list) = blobs { verify_kzg_for_blob_list(blob_list.iter(), &self.kzg) .map_err(AvailabilityCheckError::InvalidBlobs)?; Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs, + blob_data: AvailableBlockData::Blobs(blob_list), blobs_available_timestamp: None, - data_columns: None, spec: self.spec.clone(), })) } else { @@ -365,14 +368,13 @@ impl DataAvailabilityChecker { Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: None, - blobs_available_timestamp: None, - data_columns: Some( + blob_data: AvailableBlockData::DataColumns( data_column_list .into_iter() .map(|d| d.clone_arc()) .collect(), ), + blobs_available_timestamp: None, spec: self.spec.clone(), })) } else { @@ -383,9 +385,8 @@ impl DataAvailabilityChecker { Ok(MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: None, + blob_data: AvailableBlockData::NoData, blobs_available_timestamp: None, - data_columns: None, spec: self.spec.clone(), })) } @@ -437,27 +438,25 @@ impl DataAvailabilityChecker { let (block_root, block, blobs, data_columns) = block.deconstruct(); let maybe_available_block = if self.blobs_required_for_block(&block) { - if blobs.is_some() { + if let Some(blobs) = blobs { MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs, + blob_data: AvailableBlockData::Blobs(blobs), blobs_available_timestamp: None, - data_columns: None, spec: self.spec.clone(), }) } else { MaybeAvailableBlock::AvailabilityPending { block_root, block } } } else if self.data_columns_required_for_block(&block) { - if data_columns.is_some() { + if let Some(data_columns) = data_columns { MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: None, - data_columns: data_columns.map(|data_columns| { - data_columns.into_iter().map(|d| d.into_inner()).collect() - }), + blob_data: AvailableBlockData::DataColumns( + data_columns.into_iter().map(|d| d.into_inner()).collect(), + ), blobs_available_timestamp: None, spec: self.spec.clone(), }) @@ -468,8 +467,7 @@ impl DataAvailabilityChecker { MaybeAvailableBlock::Available(AvailableBlock { block_root, block, - blobs: None, - data_columns: None, + blob_data: AvailableBlockData::NoData, blobs_available_timestamp: None, spec: self.spec.clone(), }) @@ -545,11 +543,11 @@ impl DataAvailabilityChecker { &self, block_root: &Hash256, ) -> Result, AvailabilityCheckError> { - let pending_components = match self + let verified_data_columns = match self .availability_cache .check_and_set_reconstruction_started(block_root) { - ReconstructColumnsDecision::Yes(pending_components) => pending_components, + ReconstructColumnsDecision::Yes(verified_data_columns) => verified_data_columns, ReconstructColumnsDecision::No(reason) => { return Ok(DataColumnReconstructionResult::NotStarted(reason)); } @@ -560,7 +558,7 @@ impl DataAvailabilityChecker { let all_data_columns = KzgVerifiedCustodyDataColumn::reconstruct_columns( &self.kzg, - &pending_components.verified_data_columns, + &verified_data_columns, &self.spec, ) .map_err(|e| { @@ -713,13 +711,25 @@ async fn availability_cache_maintenance_service( } } +#[derive(Debug)] +pub enum AvailableBlockData { + /// Block is pre-Deneb or has zero blobs + NoData, + /// Block is post-Deneb, pre-PeerDAS and has more than zero blobs + Blobs(BlobSidecarList), + /// Block is post-PeerDAS and has more than zero blobs + DataColumns(DataColumnSidecarList), + /// Block is post-PeerDAS, has more than zero blobs and we recomputed the columns from the EL's + /// mempool blobs + DataColumnsRecv(oneshot::Receiver>), +} + /// A fully available block that is ready to be imported into fork choice. -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug)] pub struct AvailableBlock { block_root: Hash256, block: Arc>, - blobs: Option>, - data_columns: Option>, + blob_data: AvailableBlockData, /// Timestamp at which this block first became available (UNIX timestamp, time since 1970). blobs_available_timestamp: Option, pub spec: Arc, @@ -729,15 +739,13 @@ impl AvailableBlock { pub fn __new_for_testing( block_root: Hash256, block: Arc>, - blobs: Option>, - data_columns: Option>, + data: AvailableBlockData, spec: Arc, ) -> Self { Self { block_root, block, - blobs, - data_columns, + blob_data: data, blobs_available_timestamp: None, spec, } @@ -750,39 +758,56 @@ impl AvailableBlock { self.block.clone() } - pub fn blobs(&self) -> Option<&BlobSidecarList> { - self.blobs.as_ref() - } - pub fn blobs_available_timestamp(&self) -> Option { self.blobs_available_timestamp } - pub fn data_columns(&self) -> Option<&DataColumnSidecarList> { - self.data_columns.as_ref() + pub fn data(&self) -> &AvailableBlockData { + &self.blob_data + } + + pub fn has_blobs(&self) -> bool { + match self.blob_data { + AvailableBlockData::NoData => false, + AvailableBlockData::Blobs(..) => true, + AvailableBlockData::DataColumns(_) => false, + AvailableBlockData::DataColumnsRecv(_) => false, + } } #[allow(clippy::type_complexity)] - pub fn deconstruct( - self, - ) -> ( - Hash256, - Arc>, - Option>, - Option>, - ) { + pub fn deconstruct(self) -> (Hash256, Arc>, AvailableBlockData) { let AvailableBlock { block_root, block, - blobs, - data_columns, + blob_data, .. } = self; - (block_root, block, blobs, data_columns) + (block_root, block, blob_data) + } + + /// Only used for testing + pub fn __clone_without_recv(&self) -> Result { + Ok(Self { + block_root: self.block_root, + block: self.block.clone(), + blob_data: match &self.blob_data { + AvailableBlockData::NoData => AvailableBlockData::NoData, + AvailableBlockData::Blobs(blobs) => AvailableBlockData::Blobs(blobs.clone()), + AvailableBlockData::DataColumns(data_columns) => { + AvailableBlockData::DataColumns(data_columns.clone()) + } + AvailableBlockData::DataColumnsRecv(_) => { + return Err("Can't clone DataColumnsRecv".to_owned()) + } + }, + blobs_available_timestamp: self.blobs_available_timestamp, + spec: self.spec.clone(), + }) } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum MaybeAvailableBlock { /// This variant is fully available. /// i.e. for pre-deneb blocks, it contains a (`SignedBeaconBlock`, `Blobs::None`) and for diff --git a/beacon_node/beacon_chain/src/data_availability_checker/error.rs b/beacon_node/beacon_chain/src/data_availability_checker/error.rs index 1ab85ab105..4e75ed4945 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/error.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/error.rs @@ -10,7 +10,7 @@ pub enum Error { blob_commitment: KzgCommitment, block_commitment: KzgCommitment, }, - Unexpected, + Unexpected(&'static str), SszTypes(ssz_types::Error), MissingBlobs, MissingCustodyColumns, @@ -40,7 +40,7 @@ impl Error { | Error::MissingCustodyColumns | Error::StoreError(_) | Error::DecodeError(_) - | Error::Unexpected + | Error::Unexpected(_) | Error::ParentStateMissing(_) | Error::BlockReplayError(_) | Error::RebuildingStateCaches(_) diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 034a6582ad..78de538929 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -1,4 +1,5 @@ use super::state_lru_cache::{DietAvailabilityPendingExecutedBlock, StateLRUCache}; +use super::AvailableBlockData; use crate::beacon_chain::BeaconStore; use crate::blob_verification::KzgVerifiedBlob; use crate::block_verification_types::{ @@ -10,6 +11,7 @@ use crate::BeaconChainTypes; use lru::LruCache; use parking_lot::RwLock; use slog::{debug, Logger}; +use std::cmp::Ordering; use std::num::NonZeroUsize; use std::sync::Arc; use tokio::sync::oneshot; @@ -39,19 +41,6 @@ pub struct PendingComponents { } impl PendingComponents { - /// Clones the `PendingComponent` without cloning `data_column_recv`, as `Receiver` is not cloneable. - /// This should only be used when the receiver is no longer needed. - pub fn clone_without_column_recv(&self) -> Self { - PendingComponents { - block_root: self.block_root, - verified_blobs: self.verified_blobs.clone(), - verified_data_columns: self.verified_data_columns.clone(), - executed_block: self.executed_block.clone(), - reconstruction_started: self.reconstruction_started, - data_column_recv: None, - } - } - /// Returns an immutable reference to the cached block. pub fn get_cached_block(&self) -> &Option> { &self.executed_block @@ -95,26 +84,6 @@ impl PendingComponents { .unwrap_or(false) } - /// Returns the number of blobs that are expected to be present. Returns `None` if we don't have a - /// block. - /// - /// This corresponds to the number of commitments that are present in a block. - pub fn block_kzg_commitments_count(&self) -> Option { - self.get_cached_block() - .as_ref() - .map(|b| b.get_commitments().len()) - } - - /// Returns the number of blobs that have been received and are stored in the cache. - pub fn num_received_blobs(&self) -> usize { - self.get_cached_blobs().iter().flatten().count() - } - - /// Returns the number of data columns that have been received and are stored in the cache. - pub fn num_received_data_columns(&self) -> usize { - self.verified_data_columns.len() - } - /// Returns the indices of cached custody columns pub fn get_cached_data_columns_indices(&self) -> Vec { self.verified_data_columns @@ -189,51 +158,121 @@ impl PendingComponents { self.merge_blobs(reinsert); } - /// Checks if the block and all of its expected blobs or custody columns (post-PeerDAS) are - /// available in the cache. + /// Returns Some if the block has received all its required data for import. The return value + /// must be persisted in the DB along with the block. /// - /// Returns `true` if both the block exists and the number of received blobs / custody columns - /// matches the number of expected blobs / custody columns. - pub fn is_available(&self, custody_column_count: usize, log: &Logger) -> bool { - let block_kzg_commitments_count_opt = self.block_kzg_commitments_count(); - let expected_blobs_msg = block_kzg_commitments_count_opt - .as_ref() - .map(|num| num.to_string()) - .unwrap_or("unknown".to_string()); + /// WARNING: This function can potentially take a lot of time if the state needs to be + /// reconstructed from disk. Ensure you are not holding any write locks while calling this. + pub fn make_available( + &mut self, + custody_column_count: usize, + spec: &Arc, + recover: R, + ) -> Result>, AvailabilityCheckError> + where + R: FnOnce( + DietAvailabilityPendingExecutedBlock, + ) -> Result, AvailabilityCheckError>, + { + let Some(block) = &self.executed_block else { + // Block not available yet + return Ok(None); + }; - // No data columns when there are 0 blobs - let expected_columns_opt = block_kzg_commitments_count_opt.map(|blob_count| { - if blob_count > 0 { - custody_column_count - } else { - 0 + let num_expected_blobs = block.num_blobs_expected(); + + let blob_data = if num_expected_blobs == 0 { + Some(AvailableBlockData::NoData) + } else if spec.is_peer_das_enabled_for_epoch(block.epoch()) { + match self.verified_data_columns.len().cmp(&custody_column_count) { + Ordering::Greater => { + // Should never happen + return Err(AvailabilityCheckError::Unexpected("too many columns")); + } + Ordering::Equal => { + // Block is post-peerdas, and we got enough columns + let data_columns = self + .verified_data_columns + .iter() + .map(|d| d.clone().into_inner()) + .collect::>(); + Some(AvailableBlockData::DataColumns(data_columns)) + } + Ordering::Less => { + // The data_columns_recv is an infallible promise that we will receive all expected + // columns, so we consider the block available. + // We take the receiver as it can't be cloned, and make_available should never + // be called again once it returns `Some`. + self.data_column_recv + .take() + .map(AvailableBlockData::DataColumnsRecv) + } } - }); - let expected_columns_msg = expected_columns_opt - .as_ref() - .map(|num| num.to_string()) - .unwrap_or("unknown".to_string()); + } else { + // Before PeerDAS, blobs + let num_received_blobs = self.verified_blobs.iter().flatten().count(); + match num_received_blobs.cmp(&num_expected_blobs) { + Ordering::Greater => { + // Should never happen + return Err(AvailabilityCheckError::Unexpected("too many blobs")); + } + Ordering::Equal => { + let max_blobs = spec.max_blobs_per_block(block.epoch()) as usize; + let blobs_vec = self + .verified_blobs + .iter() + .flatten() + .map(|blob| blob.clone().to_blob()) + .collect::>(); + let blobs = RuntimeVariableList::new(blobs_vec, max_blobs) + .map_err(|_| AvailabilityCheckError::Unexpected("over max_blobs"))?; + Some(AvailableBlockData::Blobs(blobs)) + } + Ordering::Less => { + // Not enough blobs received yet + None + } + } + }; - let num_received_blobs = self.num_received_blobs(); - let num_received_columns = self.num_received_data_columns(); + // Block's data not available yet + let Some(blob_data) = blob_data else { + return Ok(None); + }; - debug!( - log, - "Component(s) added to data availability checker"; - "block_root" => ?self.block_root, - "received_blobs" => num_received_blobs, - "expected_blobs" => expected_blobs_msg, - "received_columns" => num_received_columns, - "expected_columns" => expected_columns_msg, - ); + // Block is available, construct `AvailableExecutedBlock` - let all_blobs_received = block_kzg_commitments_count_opt - .is_some_and(|num_expected_blobs| num_expected_blobs == num_received_blobs); + let blobs_available_timestamp = match blob_data { + AvailableBlockData::NoData => None, + AvailableBlockData::Blobs(_) => self + .verified_blobs + .iter() + .flatten() + .map(|blob| blob.seen_timestamp()) + .max(), + // TODO(das): To be fixed with https://github.com/sigp/lighthouse/pull/6850 + AvailableBlockData::DataColumns(_) => None, + AvailableBlockData::DataColumnsRecv(_) => None, + }; - let all_columns_received = expected_columns_opt - .is_some_and(|num_expected_columns| num_expected_columns == num_received_columns); + let AvailabilityPendingExecutedBlock { + block, + import_data, + payload_verification_outcome, + } = recover(block.clone())?; - all_blobs_received || all_columns_received + let available_block = AvailableBlock { + block_root: self.block_root, + block, + blob_data, + blobs_available_timestamp, + spec: spec.clone(), + }; + Ok(Some(AvailableExecutedBlock::new( + available_block, + import_data, + payload_verification_outcome, + ))) } /// Returns an empty `PendingComponents` object with the given block root. @@ -248,87 +287,6 @@ impl PendingComponents { } } - /// Verifies an `SignedBeaconBlock` against a set of KZG verified blobs. - /// This does not check whether a block *should* have blobs, these checks should have been - /// completed when producing the `AvailabilityPendingBlock`. - /// - /// WARNING: This function can potentially take a lot of time if the state needs to be - /// reconstructed from disk. Ensure you are not holding any write locks while calling this. - pub fn make_available( - self, - spec: &Arc, - recover: R, - ) -> Result, AvailabilityCheckError> - where - R: FnOnce( - DietAvailabilityPendingExecutedBlock, - ) -> Result, AvailabilityCheckError>, - { - let Self { - block_root, - verified_blobs, - verified_data_columns, - executed_block, - data_column_recv, - .. - } = self; - - let blobs_available_timestamp = verified_blobs - .iter() - .flatten() - .map(|blob| blob.seen_timestamp()) - .max(); - - let Some(diet_executed_block) = executed_block else { - return Err(AvailabilityCheckError::Unexpected); - }; - - let is_peer_das_enabled = spec.is_peer_das_enabled_for_epoch(diet_executed_block.epoch()); - let (blobs, data_columns) = if is_peer_das_enabled { - let data_columns = verified_data_columns - .into_iter() - .map(|d| d.into_inner()) - .collect::>(); - (None, Some(data_columns)) - } else { - let num_blobs_expected = diet_executed_block.num_blobs_expected(); - let Some(verified_blobs) = verified_blobs - .into_iter() - .map(|b| b.map(|b| b.to_blob())) - .take(num_blobs_expected) - .collect::>>() - else { - return Err(AvailabilityCheckError::Unexpected); - }; - let max_len = spec.max_blobs_per_block(diet_executed_block.as_block().epoch()) as usize; - ( - Some(RuntimeVariableList::new(verified_blobs, max_len)?), - None, - ) - }; - let executed_block = recover(diet_executed_block)?; - - let AvailabilityPendingExecutedBlock { - block, - mut import_data, - payload_verification_outcome, - } = executed_block; - - import_data.data_column_recv = data_column_recv; - - let available_block = AvailableBlock { - block_root, - block, - blobs, - data_columns, - blobs_available_timestamp, - spec: spec.clone(), - }; - Ok(Availability::Available(Box::new( - AvailableExecutedBlock::new(available_block, import_data, payload_verification_outcome), - ))) - } - /// Returns the epoch of the block if it is cached, otherwise returns the epoch of the first blob. pub fn epoch(&self) -> Option { self.executed_block @@ -354,6 +312,41 @@ impl PendingComponents { None }) } + + pub fn status_str( + &self, + block_epoch: Epoch, + sampling_column_count: usize, + spec: &ChainSpec, + ) -> String { + let block_count = if self.executed_block.is_some() { 1 } else { 0 }; + if spec.is_peer_das_enabled_for_epoch(block_epoch) { + let data_column_recv_count = if self.data_column_recv.is_some() { + 1 + } else { + 0 + }; + format!( + "block {} data_columns {}/{} data_columns_recv {}", + block_count, + self.verified_data_columns.len(), + sampling_column_count, + data_column_recv_count, + ) + } else { + let num_expected_blobs = if let Some(block) = self.get_cached_block() { + &block.num_blobs_expected().to_string() + } else { + "?" + }; + format!( + "block {} blobs {}/{}", + block_count, + self.verified_blobs.len(), + num_expected_blobs + ) + } + } } /// This is the main struct for this module. Outside methods should @@ -374,7 +367,7 @@ pub struct DataAvailabilityCheckerInner { // the current usage, as it's deconstructed immediately. #[allow(clippy::large_enum_variant)] pub(crate) enum ReconstructColumnsDecision { - Yes(PendingComponents), + Yes(Vec>), No(&'static str), } @@ -455,16 +448,10 @@ impl DataAvailabilityCheckerInner { } /// Puts the KZG verified blobs into the availability cache as pending components. - /// - /// The `data_column_recv` parameter is an optional `Receiver` for data columns that are - /// computed asynchronously. This method remains **used** after PeerDAS activation, because - /// blocks can be made available if the EL already has the blobs and returns them via the - /// `getBlobsV1` engine method. More details in [fetch_blobs.rs](https://github.com/sigp/lighthouse/blob/44f8add41ea2252769bb967864af95b3c13af8ca/beacon_node/beacon_chain/src/fetch_blobs.rs). pub fn put_kzg_verified_blobs>>( &self, block_root: Hash256, kzg_verified_blobs: I, - data_column_recv: Option>>, log: &Logger, ) -> Result, AvailabilityCheckError> { let mut kzg_verified_blobs = kzg_verified_blobs.into_iter().peekable(); @@ -474,7 +461,7 @@ impl DataAvailabilityCheckerInner { .map(|verified_blob| verified_blob.as_blob().epoch()) else { // Verified blobs list should be non-empty. - return Err(AvailabilityCheckError::Unexpected); + return Err(AvailabilityCheckError::Unexpected("empty blobs")); }; let mut fixed_blobs = @@ -499,21 +486,22 @@ impl DataAvailabilityCheckerInner { // Merge in the blobs. pending_components.merge_blobs(fixed_blobs); - if data_column_recv.is_some() { - // If `data_column_recv` is `Some`, it means we have all the blobs from engine, and have - // started computing data columns. We store the receiver in `PendingComponents` for - // later use when importing the block. - pending_components.data_column_recv = data_column_recv; - } + debug!(log, "Component added to data availability checker"; + "component" => "blobs", + "block_root" => ?block_root, + "status" => pending_components.status_str(epoch, self.sampling_column_count, &self.spec), + ); - if pending_components.is_available(self.sampling_column_count, log) { + if let Some(available_block) = + pending_components.make_available(self.sampling_column_count, &self.spec, |block| { + self.state_cache.recover_pending_executed_block(block) + })? + { // We keep the pending components in the availability cache during block import (#5845). // `data_column_recv` is returned as part of the available block and is no longer needed here. - write_lock.put(block_root, pending_components.clone_without_column_recv()); + write_lock.put(block_root, pending_components); drop(write_lock); - pending_components.make_available(&self.spec, |diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) + Ok(Availability::Available(Box::new(available_block))) } else { write_lock.put(block_root, pending_components); Ok(Availability::MissingComponents(block_root)) @@ -535,7 +523,7 @@ impl DataAvailabilityCheckerInner { .map(|verified_blob| verified_blob.as_data_column().epoch()) else { // Verified data_columns list should be non-empty. - return Err(AvailabilityCheckError::Unexpected); + return Err(AvailabilityCheckError::Unexpected("empty columns")); }; let mut write_lock = self.critical.write(); @@ -551,14 +539,72 @@ impl DataAvailabilityCheckerInner { // Merge in the data columns. pending_components.merge_data_columns(kzg_verified_data_columns)?; - if pending_components.is_available(self.sampling_column_count, log) { + debug!(log, "Component added to data availability checker"; + "component" => "data_columns", + "block_root" => ?block_root, + "status" => pending_components.status_str(epoch, self.sampling_column_count, &self.spec), + ); + + if let Some(available_block) = + pending_components.make_available(self.sampling_column_count, &self.spec, |block| { + self.state_cache.recover_pending_executed_block(block) + })? + { // We keep the pending components in the availability cache during block import (#5845). // `data_column_recv` is returned as part of the available block and is no longer needed here. - write_lock.put(block_root, pending_components.clone_without_column_recv()); + write_lock.put(block_root, pending_components); drop(write_lock); - pending_components.make_available(&self.spec, |diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) + Ok(Availability::Available(Box::new(available_block))) + } else { + write_lock.put(block_root, pending_components); + Ok(Availability::MissingComponents(block_root)) + } + } + + /// The `data_column_recv` parameter is a `Receiver` for data columns that are computed + /// asynchronously. This method is used if the EL already has the blobs and returns them via the + /// `getBlobsV1` engine method. More details in [fetch_blobs.rs](https://github.com/sigp/lighthouse/blob/44f8add41ea2252769bb967864af95b3c13af8ca/beacon_node/beacon_chain/src/fetch_blobs.rs). + pub fn put_computed_data_columns_recv( + &self, + block_root: Hash256, + block_epoch: Epoch, + data_column_recv: oneshot::Receiver>, + log: &Logger, + ) -> Result, AvailabilityCheckError> { + let mut write_lock = self.critical.write(); + + // Grab existing entry or create a new entry. + let mut pending_components = write_lock + .pop_entry(&block_root) + .map(|(_, v)| v) + .unwrap_or_else(|| { + PendingComponents::empty( + block_root, + self.spec.max_blobs_per_block(block_epoch) as usize, + ) + }); + + // We have all the blobs from engine, and have started computing data columns. We store the + // receiver in `PendingComponents` for later use when importing the block. + // TODO(das): Error or log if we overwrite a prior receiver https://github.com/sigp/lighthouse/issues/6764 + pending_components.data_column_recv = Some(data_column_recv); + + debug!(log, "Component added to data availability checker"; + "component" => "data_columns_recv", + "block_root" => ?block_root, + "status" => pending_components.status_str(block_epoch, self.sampling_column_count, &self.spec), + ); + + if let Some(available_block) = + pending_components.make_available(self.sampling_column_count, &self.spec, |block| { + self.state_cache.recover_pending_executed_block(block) + })? + { + // We keep the pending components in the availability cache during block import (#5845). + // `data_column_recv` is returned as part of the available block and is no longer needed here. + write_lock.put(block_root, pending_components); + drop(write_lock); + Ok(Availability::Available(Box::new(available_block))) } else { write_lock.put(block_root, pending_components); Ok(Availability::MissingComponents(block_root)) @@ -603,7 +649,7 @@ impl DataAvailabilityCheckerInner { } pending_components.reconstruction_started = true; - ReconstructColumnsDecision::Yes(pending_components.clone_without_column_recv()) + ReconstructColumnsDecision::Yes(pending_components.verified_data_columns.clone()) } /// This could mean some invalid data columns made it through to the `DataAvailabilityChecker`. @@ -643,15 +689,23 @@ impl DataAvailabilityCheckerInner { // Merge in the block. pending_components.merge_block(diet_executed_block); + debug!(log, "Component added to data availability checker"; + "component" => "block", + "block_root" => ?block_root, + "status" => pending_components.status_str(epoch, self.sampling_column_count, &self.spec), + ); + // Check if we have all components and entire set is consistent. - if pending_components.is_available(self.sampling_column_count, log) { + if let Some(available_block) = + pending_components.make_available(self.sampling_column_count, &self.spec, |block| { + self.state_cache.recover_pending_executed_block(block) + })? + { // We keep the pending components in the availability cache during block import (#5845). // `data_column_recv` is returned as part of the available block and is no longer needed here. - write_lock.put(block_root, pending_components.clone_without_column_recv()); + write_lock.put(block_root, pending_components); drop(write_lock); - pending_components.make_available(&self.spec, |diet_block| { - self.state_cache.recover_pending_executed_block(diet_block) - }) + Ok(Availability::Available(Box::new(available_block))) } else { write_lock.put(block_root, pending_components); Ok(Availability::MissingComponents(block_root)) @@ -882,7 +936,6 @@ mod test { parent_eth1_finalization_data, confirmed_state_roots: vec![], consensus_context, - data_column_recv: None, }; let payload_verification_outcome = PayloadVerificationOutcome { @@ -989,7 +1042,7 @@ mod test { for (blob_index, gossip_blob) in blobs.into_iter().enumerate() { kzg_verified_blobs.push(gossip_blob.into_inner()); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), None, harness.logger()) + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), harness.logger()) .expect("should put blob"); if blob_index == blobs_expected - 1 { assert!(matches!(availability, Availability::Available(_))); @@ -1017,11 +1070,10 @@ mod test { for gossip_blob in blobs { kzg_verified_blobs.push(gossip_blob.into_inner()); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), None, harness.logger()) + .put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), harness.logger()) .expect("should put blob"); - assert_eq!( - availability, - Availability::MissingComponents(root), + assert!( + matches!(availability, Availability::MissingComponents(_)), "should be pending block" ); assert_eq!(cache.critical.read().len(), 1); @@ -1273,7 +1325,6 @@ mod pending_components_tests { }, confirmed_state_roots: vec![], consensus_context: ConsensusContext::new(Slot::new(0)), - data_column_recv: None, }, payload_verification_outcome: PayloadVerificationOutcome { payload_verification_status: PayloadVerificationStatus::Verified, diff --git a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs index 2a2a0431cc..5b9b7c7023 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/state_lru_cache.rs @@ -136,7 +136,6 @@ impl StateLRUCache { consensus_context: diet_executed_block .consensus_context .into_consensus_context(), - data_column_recv: None, }, payload_verification_outcome: diet_executed_block.payload_verification_outcome, }) diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index c94ea0e941..a90911026c 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -1,4 +1,4 @@ -use crate::data_availability_checker::AvailableBlock; +use crate::data_availability_checker::{AvailableBlock, AvailableBlockData}; use crate::{ attester_cache::{CommitteeLengths, Error}, metrics, @@ -52,7 +52,7 @@ impl EarlyAttesterCache { pub fn add_head_block( &self, beacon_block_root: Hash256, - block: AvailableBlock, + block: &AvailableBlock, proto_block: ProtoBlock, state: &BeaconState, spec: &ChainSpec, @@ -70,14 +70,23 @@ impl EarlyAttesterCache { }, }; - let (_, block, blobs, data_columns) = block.deconstruct(); + let (blobs, data_columns) = match block.data() { + AvailableBlockData::NoData => (None, None), + AvailableBlockData::Blobs(blobs) => (Some(blobs.clone()), None), + AvailableBlockData::DataColumns(data_columns) => (None, Some(data_columns.clone())), + // TODO(das): Once the columns are received, they will not be available in + // the early attester cache. If someone does a query to us via RPC we + // will get downscored. + AvailableBlockData::DataColumnsRecv(_) => (None, None), + }; + let item = CacheItem { epoch, committee_lengths, beacon_block_root, source, target, - block, + block: block.block_cloned(), blobs, data_columns, proto_block, diff --git a/beacon_node/beacon_chain/src/historical_blocks.rs b/beacon_node/beacon_chain/src/historical_blocks.rs index a48f32e7b4..a9caeb18bb 100644 --- a/beacon_node/beacon_chain/src/historical_blocks.rs +++ b/beacon_node/beacon_chain/src/historical_blocks.rs @@ -1,4 +1,4 @@ -use crate::data_availability_checker::AvailableBlock; +use crate::data_availability_checker::{AvailableBlock, AvailableBlockData}; use crate::{metrics, BeaconChain, BeaconChainTypes}; use itertools::Itertools; use slog::debug; @@ -105,7 +105,7 @@ impl BeaconChain { let blob_batch_size = blocks_to_import .iter() - .filter(|available_block| available_block.blobs().is_some()) + .filter(|available_block| available_block.has_blobs()) .count() .saturating_mul(n_blob_ops_per_block); @@ -114,14 +114,13 @@ impl BeaconChain { let mut new_oldest_blob_slot = blob_info.oldest_blob_slot; let mut new_oldest_data_column_slot = data_column_info.oldest_data_column_slot; - let mut blob_batch = Vec::with_capacity(blob_batch_size); + let mut blob_batch = Vec::::with_capacity(blob_batch_size); let mut cold_batch = Vec::with_capacity(blocks_to_import.len()); let mut hot_batch = Vec::with_capacity(blocks_to_import.len()); let mut signed_blocks = Vec::with_capacity(blocks_to_import.len()); for available_block in blocks_to_import.into_iter().rev() { - let (block_root, block, maybe_blobs, maybe_data_columns) = - available_block.deconstruct(); + let (block_root, block, block_data) = available_block.deconstruct(); if block_root != expected_block_root { return Err(HistoricalBlockError::MismatchedBlockRoot { @@ -144,17 +143,26 @@ impl BeaconChain { ); } - // Store the blobs too - if let Some(blobs) = maybe_blobs { - new_oldest_blob_slot = Some(block.slot()); - self.store - .blobs_as_kv_store_ops(&block_root, blobs, &mut blob_batch); + match &block_data { + AvailableBlockData::NoData => {} + AvailableBlockData::Blobs(..) => { + new_oldest_blob_slot = Some(block.slot()); + } + AvailableBlockData::DataColumns(_) | AvailableBlockData::DataColumnsRecv(_) => { + new_oldest_data_column_slot = Some(block.slot()); + } } - // Store the data columns too - if let Some(data_columns) = maybe_data_columns { - new_oldest_data_column_slot = Some(block.slot()); - self.store - .data_columns_as_kv_store_ops(&block_root, data_columns, &mut blob_batch); + + // Store the blobs or data columns too + if let Some(op) = self + .get_blobs_or_columns_store_op(block_root, block_data) + .map_err(|e| { + HistoricalBlockError::StoreError(StoreError::DBError { + message: format!("get_blobs_or_columns_store_op error {e:?}"), + }) + })? + { + blob_batch.extend(self.store.convert_to_kv_batch(vec![op])?); } // Store block roots, including at all skip slots in the freezer DB. diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index 621475a3ec..d89a8530e1 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -242,7 +242,7 @@ async fn produces_attestations() { .early_attester_cache .add_head_block( block_root, - available_block, + &available_block, proto_block, &state, &chain.spec, @@ -310,7 +310,7 @@ async fn early_attester_cache_old_request() { .early_attester_cache .add_head_block( head.beacon_block_root, - available_block, + &available_block, head_proto_block, &head.beacon_state, &harness.chain.spec, diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 7a2df76970..997a2859b7 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -2517,18 +2517,13 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { // Corrupt the signature on the 1st block to ensure that the backfill processor is checking // signatures correctly. Regression test for https://github.com/sigp/lighthouse/pull/5120. - let mut batch_with_invalid_first_block = available_blocks.clone(); + let mut batch_with_invalid_first_block = + available_blocks.iter().map(clone_block).collect::>(); batch_with_invalid_first_block[0] = { - let (block_root, block, blobs, data_columns) = available_blocks[0].clone().deconstruct(); + let (block_root, block, data) = clone_block(&available_blocks[0]).deconstruct(); let mut corrupt_block = (*block).clone(); *corrupt_block.signature_mut() = Signature::empty(); - AvailableBlock::__new_for_testing( - block_root, - Arc::new(corrupt_block), - blobs, - data_columns, - Arc::new(spec), - ) + AvailableBlock::__new_for_testing(block_root, Arc::new(corrupt_block), data, Arc::new(spec)) }; // Importing the invalid batch should error. @@ -2540,8 +2535,9 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { )); // Importing the batch with valid signatures should succeed. + let available_blocks_dup = available_blocks.iter().map(clone_block).collect::>(); beacon_chain - .import_historical_block_batch(available_blocks.clone()) + .import_historical_block_batch(available_blocks_dup) .unwrap(); assert_eq!(beacon_chain.store.get_oldest_block_slot(), 0); @@ -3690,3 +3686,7 @@ fn get_blocks( .map(|checkpoint| checkpoint.beacon_block_root.into()) .collect() } + +fn clone_block(block: &AvailableBlock) -> AvailableBlock { + block.__clone_without_recv().unwrap() +} From 6e11bddd4bd0de87a93e7880a9818bd80c95b75b Mon Sep 17 00:00:00 2001 From: Krishang Shah <93703995+kamuik16@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:33:17 +0530 Subject: [PATCH 08/13] feat: adds CLI flags to delay publishing for edge case testing on PeerDAS devnets (#6947) Closes #6919 --- beacon_node/beacon_chain/src/chain_config.rs | 6 +++++ beacon_node/http_api/src/publish_blocks.rs | 27 ++++++++++++++++++++ beacon_node/src/cli.rs | 25 ++++++++++++++++++ beacon_node/src/config.rs | 8 ++++++ lighthouse/tests/beacon_node.rs | 26 +++++++++++++++++++ 5 files changed, 92 insertions(+) diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index fcdd57abbc..d3852183b9 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -94,6 +94,10 @@ pub struct ChainConfig { /// The delay in milliseconds applied by the node between sending each blob or data column batch. /// This doesn't apply if the node is the block proposer. pub blob_publication_batch_interval: Duration, + /// Artificial delay for block publishing. For PeerDAS testing only. + pub block_publishing_delay: Option, + /// Artificial delay for data column publishing. For PeerDAS testing only. + pub data_column_publishing_delay: Option, } impl Default for ChainConfig { @@ -129,6 +133,8 @@ impl Default for ChainConfig { enable_sampling: false, blob_publication_batches: 4, blob_publication_batch_interval: Duration::from_millis(300), + block_publishing_delay: None, + data_column_publishing_delay: None, } } } diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 60d4b2f16e..072ae5dc03 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -86,6 +86,8 @@ pub async fn publish_block>( network_globals: Arc>, ) -> Result { let seen_timestamp = timestamp_now(); + let block_publishing_delay_for_testing = chain.config.block_publishing_delay; + let data_column_publishing_delay_for_testing = chain.config.data_column_publishing_delay; let (unverified_block, unverified_blobs, is_locally_built_block) = match provenanced_block { ProvenancedBlock::Local(block, blobs, _) => (block, blobs, true), @@ -147,6 +149,14 @@ pub async fn publish_block>( let should_publish_block = gossip_verified_block_result.is_ok(); if BroadcastValidation::Gossip == validation_level && should_publish_block { + if let Some(block_publishing_delay) = block_publishing_delay_for_testing { + debug!( + log, + "Publishing block with artificial delay"; + "block_publishing_delay" => ?block_publishing_delay + ); + tokio::time::sleep(block_publishing_delay).await; + } publish_block_p2p( block.clone(), sender_clone.clone(), @@ -207,6 +217,23 @@ pub async fn publish_block>( } if gossip_verified_columns.iter().map(Option::is_some).count() > 0 { + if let Some(data_column_publishing_delay) = data_column_publishing_delay_for_testing { + // Subtract block publishing delay if it is also used. + // Note: if `data_column_publishing_delay` is less than `block_publishing_delay`, it + // will still be delayed by `block_publishing_delay`. This could be solved with spawning + // async tasks but the limitation is minor and I believe it's probably not worth + // affecting the mainnet code path. + let block_publishing_delay = block_publishing_delay_for_testing.unwrap_or_default(); + let delay = data_column_publishing_delay.saturating_sub(block_publishing_delay); + if !delay.is_zero() { + debug!( + log, + "Publishing data columns with artificial delay"; + "data_column_publishing_delay" => ?data_column_publishing_delay + ); + tokio::time::sleep(delay).await; + } + } publish_column_sidecars(network_tx, &gossip_verified_columns, &chain).map_err(|_| { warp_utils::reject::custom_server_error("unable to publish data column sidecars".into()) })?; diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 4c2daecdd3..200de0b14a 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1599,5 +1599,30 @@ pub fn cli_app() -> Command { .action(ArgAction::Set) .display_order(0) ) + .arg( + Arg::new("delay-block-publishing") + .long("delay-block-publishing") + .value_name("SECONDS") + .action(ArgAction::Set) + .help_heading(FLAG_HEADER) + .help("TESTING ONLY: Artificially delay block publishing by the specified number of seconds. \ + This only works for if `BroadcastValidation::Gossip` is used (default). \ + DO NOT USE IN PRODUCTION.") + .hide(true) + .display_order(0) + ) + .arg( + Arg::new("delay-data-column-publishing") + .long("delay-data-column-publishing") + .value_name("SECONDS") + .action(ArgAction::Set) + .help_heading(FLAG_HEADER) + .help("TESTING ONLY: Artificially delay data column publishing by the specified number of seconds. \ + Limitation: If `delay-block-publishing` is also used, data columns will be delayed for a \ + minimum of `delay-block-publishing` seconds. + DO NOT USE IN PRODUCTION.") + .hide(true) + .display_order(0) + ) .group(ArgGroup::new("enable_http").args(["http", "gui", "staking"]).multiple(true)) } diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 24d569bea2..0b9e90717d 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -895,6 +895,14 @@ pub fn get_config( .max_gossip_aggregate_batch_size = clap_utils::parse_required(cli_args, "beacon-processor-aggregate-batch-size")?; + if let Some(delay) = clap_utils::parse_optional(cli_args, "delay-block-publishing")? { + client_config.chain.block_publishing_delay = Some(Duration::from_secs_f64(delay)); + } + + if let Some(delay) = clap_utils::parse_optional(cli_args, "delay-data-column-publishing")? { + client_config.chain.data_column_publishing_delay = Some(Duration::from_secs_f64(delay)); + } + Ok(client_config) } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 03314930b9..283c20cc6e 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2715,3 +2715,29 @@ fn beacon_node_backend_override() { assert_eq!(config.store.backend, BeaconNodeBackend::LevelDb); }); } + +#[test] +fn block_publishing_delay_for_testing() { + CommandLineTest::new() + .flag("delay-block-publishing", Some("2.5")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.block_publishing_delay, + Some(Duration::from_secs_f64(2.5f64)) + ); + }); +} + +#[test] +fn data_column_publishing_delay_for_testing() { + CommandLineTest::new() + .flag("delay-data-column-publishing", Some("3.5")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!( + config.chain.data_column_publishing_delay, + Some(Duration::from_secs_f64(3.5f64)) + ); + }); +} From 454c7d05c40bdf82c457ed88b47dc6875f0f29ac Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Mon, 24 Feb 2025 18:15:32 +1100 Subject: [PATCH 09/13] Remove LC server config from HTTP API (#7017) Partly addresses - https://github.com/sigp/lighthouse/issues/6959 Use the `enable_light_client_server` field from the beacon chain config in the HTTP API. I think we can make this the single source of truth, as I think the network crate also has access to the beacon chain config. --- beacon_node/http_api/src/lib.rs | 63 ++++++++++++-------------- beacon_node/http_api/src/test_utils.rs | 1 - beacon_node/src/config.rs | 3 -- lighthouse/tests/beacon_node.rs | 2 - testing/simulator/src/local_network.rs | 1 - 5 files changed, 29 insertions(+), 41 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 5d75dc8c9a..b3fd864f6d 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -155,7 +155,6 @@ pub struct Config { pub enable_beacon_processor: bool, #[serde(with = "eth2::types::serde_status_code")] pub duplicate_block_status_code: StatusCode, - pub enable_light_client_server: bool, pub target_peers: usize, } @@ -171,7 +170,6 @@ impl Default for Config { sse_capacity_multiplier: 1, enable_beacon_processor: true, duplicate_block_status_code: StatusCode::ACCEPTED, - enable_light_client_server: true, target_peers: 100, } } @@ -296,18 +294,6 @@ pub fn prometheus_metrics() -> warp::filters::log::Log impl Filter + Clone { - warp::any() - .and_then(move || async move { - if is_enabled { - Ok(()) - } else { - Err(warp::reject::not_found()) - } - }) - .untuple_one() -} - /// Creates a server that will serve requests using information from `ctx`. /// /// The server will shut down gracefully when the `shutdown` future resolves. @@ -493,6 +479,18 @@ pub fn serve( }, ); + // Create a `warp` filter that returns 404s if the light client server is disabled. + let light_client_server_filter = + warp::any() + .and(chain_filter.clone()) + .then(|chain: Arc>| async move { + if chain.config.enable_light_client_server { + Ok(()) + } else { + Err(warp::reject::not_found()) + } + }); + // Create a `warp` filter that provides access to the logger. let inner_ctx = ctx.clone(); let log_filter = warp::any().map(move || inner_ctx.log.clone()); @@ -2452,6 +2450,7 @@ pub fn serve( let beacon_light_client_path = eth_v1 .and(warp::path("beacon")) .and(warp::path("light_client")) + .and(light_client_server_filter) .and(chain_filter.clone()); // GET beacon/light_client/bootstrap/{block_root} @@ -2467,11 +2466,13 @@ pub fn serve( .and(warp::path::end()) .and(warp::header::optional::("accept")) .then( - |chain: Arc>, + |light_client_server_enabled: Result<(), Rejection>, + chain: Arc>, task_spawner: TaskSpawner, block_root: Hash256, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { + light_client_server_enabled?; get_light_client_bootstrap::(chain, &block_root, accept_header) }) }, @@ -2485,10 +2486,12 @@ pub fn serve( .and(warp::path::end()) .and(warp::header::optional::("accept")) .then( - |chain: Arc>, + |light_client_server_enabled: Result<(), Rejection>, + chain: Arc>, task_spawner: TaskSpawner, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { + light_client_server_enabled?; let update = chain .light_client_server_cache .get_latest_optimistic_update() @@ -2532,10 +2535,12 @@ pub fn serve( .and(warp::path::end()) .and(warp::header::optional::("accept")) .then( - |chain: Arc>, + |light_client_server_enabled: Result<(), Rejection>, + chain: Arc>, task_spawner: TaskSpawner, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { + light_client_server_enabled?; let update = chain .light_client_server_cache .get_latest_finality_update() @@ -2580,11 +2585,13 @@ pub fn serve( .and(warp::query::()) .and(warp::header::optional::("accept")) .then( - |chain: Arc>, + |light_client_server_enabled: Result<(), Rejection>, + chain: Arc>, task_spawner: TaskSpawner, query: LightClientUpdatesQuery, accept_header: Option| { task_spawner.blocking_response_task(Priority::P1, move || { + light_client_server_enabled?; get_light_client_updates::(chain, query, accept_header) }) }, @@ -4723,22 +4730,10 @@ pub fn serve( .uor(get_lighthouse_database_info) .uor(get_lighthouse_block_rewards) .uor(get_lighthouse_attestation_performance) - .uor( - enable(ctx.config.enable_light_client_server) - .and(get_beacon_light_client_optimistic_update), - ) - .uor( - enable(ctx.config.enable_light_client_server) - .and(get_beacon_light_client_finality_update), - ) - .uor( - enable(ctx.config.enable_light_client_server) - .and(get_beacon_light_client_bootstrap), - ) - .uor( - enable(ctx.config.enable_light_client_server) - .and(get_beacon_light_client_updates), - ) + .uor(get_beacon_light_client_optimistic_update) + .uor(get_beacon_light_client_finality_update) + .uor(get_beacon_light_client_bootstrap) + .uor(get_beacon_light_client_updates) .uor(get_lighthouse_block_packing_efficiency) .uor(get_lighthouse_merge_readiness) .uor(get_events) diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index fbc92a45cc..c692ec999e 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -249,7 +249,6 @@ pub async fn create_api_server_with_config( enabled: true, listen_port: port, data_dir: std::path::PathBuf::from(DEFAULT_ROOT_DIR), - enable_light_client_server: true, ..http_config }, chain: Some(chain), diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 0b9e90717d..ec217ed71e 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -174,9 +174,6 @@ pub fn get_config( client_config.http_api.duplicate_block_status_code = parse_required(cli_args, "http-duplicate-block-status")?; - - client_config.http_api.enable_light_client_server = - !cli_args.get_flag("disable-light-client-server"); } if cli_args.get_flag("light-client-server") { diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 283c20cc6e..f64f553290 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2506,7 +2506,6 @@ fn light_client_server_default() { .with_config(|config| { assert!(config.network.enable_light_client_server); assert!(config.chain.enable_light_client_server); - assert!(config.http_api.enable_light_client_server); }); } @@ -2539,7 +2538,6 @@ fn light_client_http_server_disabled() { .flag("disable-light-client-server", None) .run_with_zero_port() .with_config(|config| { - assert!(!config.http_api.enable_light_client_server); assert!(!config.network.enable_light_client_server); assert!(!config.chain.enable_light_client_server); }); diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index a95c15c231..3914d33f93 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -44,7 +44,6 @@ fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) beacon_config.network.enable_light_client_server = true; beacon_config.network.discv5_config.enable_packet_filter = false; beacon_config.chain.enable_light_client_server = true; - beacon_config.http_api.enable_light_client_server = true; beacon_config.chain.optimistic_finalized_sync = false; beacon_config.trusted_setup = serde_json::from_reader(get_trusted_setup().as_slice()) .expect("Trusted setup bytes should be valid"); From 54b4150a6220b0bc61e8e069c6efdeba6153135d Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Mon, 24 Feb 2025 19:30:11 +1100 Subject: [PATCH 10/13] Add test flag to override `SYNC_TOLERANCE_EPOCHS` for range sync testing (#7030) Related to #6880, an issue that's usually observed on local devnets with small number of nodes. When testing range sync, I usually shutdown a node for some period of time and restart it again. However, if it's within `SYNC_TOLERANCE_EPOCHS` (8), Lighthouse would consider the node as synced, and if it may attempt to produce a block if requested by a validator - on a local devnet, nodes frequently produce blocks - when this happens, the node ends up producing a block that would revert finality and would get disconnected from peers immediately. ### Usage Run Lighthouse BN with this flag to override: ``` --sync-tolerance--epoch 0 ``` --- beacon_node/http_api/src/lib.rs | 9 +++++++-- beacon_node/src/cli.rs | 13 +++++++++++++ beacon_node/src/config.rs | 5 ++++- lighthouse/tests/beacon_node.rs | 11 +++++++++++ scripts/local_testnet/network_params_das.yaml | 4 ++++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index b3fd864f6d..01dd6bea5f 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -112,7 +112,7 @@ const API_PREFIX: &str = "eth"; /// /// This helps prevent attacks where nodes can convince us that we're syncing some non-existent /// finalized head. -const SYNC_TOLERANCE_EPOCHS: u64 = 8; +const DEFAULT_SYNC_TOLERANCE_EPOCHS: u64 = 8; /// A custom type which allows for both unsecured and TLS-enabled HTTP servers. type HttpServer = (SocketAddr, Pin + Send>>); @@ -156,6 +156,7 @@ pub struct Config { #[serde(with = "eth2::types::serde_status_code")] pub duplicate_block_status_code: StatusCode, pub target_peers: usize, + pub sync_tolerance_epochs: Option, } impl Default for Config { @@ -171,6 +172,7 @@ impl Default for Config { enable_beacon_processor: true, duplicate_block_status_code: StatusCode::ACCEPTED, target_peers: 100, + sync_tolerance_epochs: None, } } } @@ -459,7 +461,10 @@ pub fn serve( ) })?; - let tolerance = SYNC_TOLERANCE_EPOCHS * T::EthSpec::slots_per_epoch(); + let sync_tolerance_epochs = config + .sync_tolerance_epochs + .unwrap_or(DEFAULT_SYNC_TOLERANCE_EPOCHS); + let tolerance = sync_tolerance_epochs * T::EthSpec::slots_per_epoch(); if head_slot + tolerance >= current_slot { Ok(()) diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 200de0b14a..92e46e5bb6 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -1509,6 +1509,19 @@ pub fn cli_app() -> Command { .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("sync-tolerance-epochs") + .long("sync-tolerance-epochs") + .help("Overrides the default SYNC_TOLERANCE_EPOCHS. This flag is not intended \ + for production and MUST only be used in TESTING only. This is primarily used \ + for testing range sync, to prevent the node from producing a block before the \ + node is synced with the network which may result in the node getting \ + disconnected from peers immediately.") + .hide(true) + .requires("enable_http") + .action(ArgAction::Set) + .display_order(0) + ) .arg( Arg::new("gui") .long("gui") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index ec217ed71e..292385c27d 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -8,7 +8,7 @@ use beacon_chain::graffiti_calculator::GraffitiOrigin; use beacon_chain::TrustedSetup; use clap::{parser::ValueSource, ArgMatches, Id}; use clap_utils::flags::DISABLE_MALLOC_TUNING_FLAG; -use clap_utils::{parse_flag, parse_required}; +use clap_utils::{parse_flag, parse_optional, parse_required}; use client::{ClientConfig, ClientGenesis}; use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR}; use environment::RuntimeContext; @@ -174,6 +174,9 @@ pub fn get_config( client_config.http_api.duplicate_block_status_code = parse_required(cli_args, "http-duplicate-block-status")?; + + client_config.http_api.sync_tolerance_epochs = + parse_optional(cli_args, "sync-tolerance-epochs")?; } if cli_args.get_flag("light-client-server") { diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index f64f553290..415629024a 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -2543,6 +2543,17 @@ fn light_client_http_server_disabled() { }); } +#[test] +fn sync_tolerance_epochs() { + CommandLineTest::new() + .flag("http", None) + .flag("sync-tolerance-epochs", Some("0")) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.http_api.sync_tolerance_epochs, Some(0)); + }); +} + #[test] fn gui_flag() { CommandLineTest::new() diff --git a/scripts/local_testnet/network_params_das.yaml b/scripts/local_testnet/network_params_das.yaml index 030aa2b820..80b4bc95c6 100644 --- a/scripts/local_testnet/network_params_das.yaml +++ b/scripts/local_testnet/network_params_das.yaml @@ -4,11 +4,15 @@ participants: cl_extra_params: - --subscribe-all-data-column-subnets - --subscribe-all-subnets + # Note: useful for testing range sync (only produce block if node is in sync to prevent forking) + - --sync-tolerance-epochs=0 - --target-peers=3 count: 2 - cl_type: lighthouse cl_image: lighthouse:local cl_extra_params: + # Note: useful for testing range sync (only produce block if node is in sync to prevent forking) + - --sync-tolerance-epochs=0 - --target-peers=3 count: 2 network_params: From 8a772520a50abf10c901ab9d5ad81b9d58bcf0ae Mon Sep 17 00:00:00 2001 From: realbigsean Date: Mon, 24 Feb 2025 21:00:51 -0800 Subject: [PATCH 11/13] Cache validator registration only after successful publish (#7034) --- .../src/preparation_service.rs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/validator_client/validator_services/src/preparation_service.rs b/validator_client/validator_services/src/preparation_service.rs index fe6eab3a8a..1f63f932a8 100644 --- a/validator_client/validator_services/src/preparation_service.rs +++ b/validator_client/validator_services/src/preparation_service.rs @@ -428,7 +428,7 @@ impl PreparationService { pubkey, } = key.clone(); - let signed_data = match self + match self .validator_store .sign_validator_registration_data(ValidatorRegistrationData { fee_recipient, @@ -458,13 +458,7 @@ impl PreparationService { ); continue; } - }; - - self.validator_registration_cache - .write() - .insert(key, signed_data.clone()); - - signed_data + } }; signed.push(signed_data); } @@ -478,11 +472,20 @@ impl PreparationService { }) .await { - Ok(()) => info!( - log, - "Published validator registrations to the builder network"; - "count" => batch.len(), - ), + Ok(()) => { + info!( + log, + "Published validator registrations to the builder network"; + "count" => batch.len(), + ); + let mut guard = self.validator_registration_cache.write(); + for signed_data in batch { + guard.insert( + ValidatorRegistrationKey::from(signed_data.message.clone()), + signed_data.clone(), + ); + } + } Err(e) => warn!( log, "Unable to publish validator registrations to the builder network"; From 1235d44802257ea6749ad6dc4c554f3a43b60e0b Mon Sep 17 00:00:00 2001 From: Mac L Date: Wed, 5 Mar 2025 09:57:09 +0400 Subject: [PATCH 12/13] Remove `watch` (#7048) Removes the WIP chain indexer known as beacon watch. --- .github/workflows/test-suite.yml | 2 - Cargo.lock | 433 +----- Cargo.toml | 2 - book/src/setup.md | 3 - watch/.gitignore | 1 - watch/Cargo.toml | 45 - watch/README.md | 458 ------ watch/config.yaml.default | 49 - watch/diesel.toml | 5 - watch/migrations/.gitkeep | 0 .../down.sql | 6 - .../up.sql | 36 - .../down.sql | 1 - .../2022-01-01-000000_canonical_slots/up.sql | 6 - .../2022-01-01-000001_beacon_blocks/down.sql | 1 - .../2022-01-01-000001_beacon_blocks/up.sql | 7 - .../2022-01-01-000002_validators/down.sql | 1 - .../2022-01-01-000002_validators/up.sql | 7 - .../2022-01-01-000003_proposer_info/down.sql | 1 - .../2022-01-01-000003_proposer_info/up.sql | 5 - .../2022-01-01-000004_active_config/down.sql | 1 - .../2022-01-01-000004_active_config/up.sql | 5 - .../2022-01-01-000010_blockprint/down.sql | 1 - .../2022-01-01-000010_blockprint/up.sql | 4 - .../2022-01-01-000011_block_rewards/down.sql | 1 - .../2022-01-01-000011_block_rewards/up.sql | 6 - .../2022-01-01-000012_block_packing/down.sql | 1 - .../2022-01-01-000012_block_packing/up.sql | 6 - .../down.sql | 1 - .../up.sql | 8 - .../2022-01-01-000020_capella/down.sql | 2 - .../2022-01-01-000020_capella/up.sql | 3 - watch/postgres_docker_compose/compose.yml | 16 - watch/src/block_packing/database.rs | 140 -- watch/src/block_packing/mod.rs | 38 - watch/src/block_packing/server.rs | 31 - watch/src/block_packing/updater.rs | 211 --- watch/src/block_rewards/database.rs | 137 -- watch/src/block_rewards/mod.rs | 38 - watch/src/block_rewards/server.rs | 31 - watch/src/block_rewards/updater.rs | 157 -- watch/src/blockprint/config.rs | 40 - watch/src/blockprint/database.rs | 225 --- watch/src/blockprint/mod.rs | 150 -- watch/src/blockprint/server.rs | 31 - watch/src/blockprint/updater.rs | 172 --- watch/src/cli.rs | 52 - watch/src/client.rs | 178 --- watch/src/config.rs | 50 - watch/src/database/compat.rs | 47 - watch/src/database/config.rs | 74 - watch/src/database/error.rs | 55 - watch/src/database/mod.rs | 786 ---------- watch/src/database/models.rs | 67 - watch/src/database/schema.rs | 102 -- watch/src/database/utils.rs | 28 - watch/src/database/watch_types.rs | 119 -- watch/src/lib.rs | 12 - watch/src/logger.rs | 24 - watch/src/main.rs | 41 - watch/src/server/config.rs | 28 - watch/src/server/error.rs | 59 - watch/src/server/handler.rs | 266 ---- watch/src/server/mod.rs | 136 -- watch/src/suboptimal_attestations/database.rs | 224 --- watch/src/suboptimal_attestations/mod.rs | 56 - watch/src/suboptimal_attestations/server.rs | 299 ---- watch/src/suboptimal_attestations/updater.rs | 236 --- watch/src/updater/config.rs | 65 - watch/src/updater/error.rs | 57 - watch/src/updater/handler.rs | 471 ------ watch/src/updater/mod.rs | 234 --- watch/tests/tests.rs | 1294 ----------------- 73 files changed, 2 insertions(+), 7583 deletions(-) delete mode 100644 watch/.gitignore delete mode 100644 watch/Cargo.toml delete mode 100644 watch/README.md delete mode 100644 watch/config.yaml.default delete mode 100644 watch/diesel.toml delete mode 100644 watch/migrations/.gitkeep delete mode 100644 watch/migrations/00000000000000_diesel_initial_setup/down.sql delete mode 100644 watch/migrations/00000000000000_diesel_initial_setup/up.sql delete mode 100644 watch/migrations/2022-01-01-000000_canonical_slots/down.sql delete mode 100644 watch/migrations/2022-01-01-000000_canonical_slots/up.sql delete mode 100644 watch/migrations/2022-01-01-000001_beacon_blocks/down.sql delete mode 100644 watch/migrations/2022-01-01-000001_beacon_blocks/up.sql delete mode 100644 watch/migrations/2022-01-01-000002_validators/down.sql delete mode 100644 watch/migrations/2022-01-01-000002_validators/up.sql delete mode 100644 watch/migrations/2022-01-01-000003_proposer_info/down.sql delete mode 100644 watch/migrations/2022-01-01-000003_proposer_info/up.sql delete mode 100644 watch/migrations/2022-01-01-000004_active_config/down.sql delete mode 100644 watch/migrations/2022-01-01-000004_active_config/up.sql delete mode 100644 watch/migrations/2022-01-01-000010_blockprint/down.sql delete mode 100644 watch/migrations/2022-01-01-000010_blockprint/up.sql delete mode 100644 watch/migrations/2022-01-01-000011_block_rewards/down.sql delete mode 100644 watch/migrations/2022-01-01-000011_block_rewards/up.sql delete mode 100644 watch/migrations/2022-01-01-000012_block_packing/down.sql delete mode 100644 watch/migrations/2022-01-01-000012_block_packing/up.sql delete mode 100644 watch/migrations/2022-01-01-000013_suboptimal_attestations/down.sql delete mode 100644 watch/migrations/2022-01-01-000013_suboptimal_attestations/up.sql delete mode 100644 watch/migrations/2022-01-01-000020_capella/down.sql delete mode 100644 watch/migrations/2022-01-01-000020_capella/up.sql delete mode 100644 watch/postgres_docker_compose/compose.yml delete mode 100644 watch/src/block_packing/database.rs delete mode 100644 watch/src/block_packing/mod.rs delete mode 100644 watch/src/block_packing/server.rs delete mode 100644 watch/src/block_packing/updater.rs delete mode 100644 watch/src/block_rewards/database.rs delete mode 100644 watch/src/block_rewards/mod.rs delete mode 100644 watch/src/block_rewards/server.rs delete mode 100644 watch/src/block_rewards/updater.rs delete mode 100644 watch/src/blockprint/config.rs delete mode 100644 watch/src/blockprint/database.rs delete mode 100644 watch/src/blockprint/mod.rs delete mode 100644 watch/src/blockprint/server.rs delete mode 100644 watch/src/blockprint/updater.rs delete mode 100644 watch/src/cli.rs delete mode 100644 watch/src/client.rs delete mode 100644 watch/src/config.rs delete mode 100644 watch/src/database/compat.rs delete mode 100644 watch/src/database/config.rs delete mode 100644 watch/src/database/error.rs delete mode 100644 watch/src/database/mod.rs delete mode 100644 watch/src/database/models.rs delete mode 100644 watch/src/database/schema.rs delete mode 100644 watch/src/database/utils.rs delete mode 100644 watch/src/database/watch_types.rs delete mode 100644 watch/src/lib.rs delete mode 100644 watch/src/logger.rs delete mode 100644 watch/src/main.rs delete mode 100644 watch/src/server/config.rs delete mode 100644 watch/src/server/error.rs delete mode 100644 watch/src/server/handler.rs delete mode 100644 watch/src/server/mod.rs delete mode 100644 watch/src/suboptimal_attestations/database.rs delete mode 100644 watch/src/suboptimal_attestations/mod.rs delete mode 100644 watch/src/suboptimal_attestations/server.rs delete mode 100644 watch/src/suboptimal_attestations/updater.rs delete mode 100644 watch/src/updater/config.rs delete mode 100644 watch/src/updater/error.rs delete mode 100644 watch/src/updater/handler.rs delete mode 100644 watch/src/updater/mod.rs delete mode 100644 watch/tests/tests.rs diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 574f1eda79..817fd9524d 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -24,8 +24,6 @@ env: LIGHTHOUSE_GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }} # Enable self-hosted runners for the sigp repo only. SELF_HOSTED_RUNNERS: ${{ github.repository == 'sigp/lighthouse' }} - # Self-hosted runners need to reference a different host for `./watch` tests. - WATCH_HOST: ${{ github.repository == 'sigp/lighthouse' && 'host.docker.internal' || 'localhost' }} # Disable incremental compilation CARGO_INCREMENTAL: 0 # Enable portable to prevent issues with caching `blst` for the wrong CPU type diff --git a/Cargo.lock b/Cargo.lock index c51e3583d0..3a63b6cdf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -685,61 +685,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.6.0", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper 1.0.2", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.2", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "backtrace" version = "0.3.74" @@ -1096,16 +1041,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "bollard-stubs" -version = "1.42.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" -dependencies = [ - "serde", - "serde_with", -] - [[package]] name = "boot_node" version = "7.0.0-beta.0" @@ -2114,53 +2049,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "diesel" -version = "2.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04001f23ba8843dc315804fa324000376084dfb1c30794ff68dd279e6e5696d5" -dependencies = [ - "bitflags 2.8.0", - "byteorder", - "diesel_derives", - "itoa", - "pq-sys", - "r2d2", -] - -[[package]] -name = "diesel_derives" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" -dependencies = [ - "diesel_table_macro_syntax", - "dsl_auto_type", - "proc-macro2", - "quote", - "syn 2.0.98", -] - -[[package]] -name = "diesel_migrations" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" -dependencies = [ - "diesel", - "migrations_internals", - "migrations_macros", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" -dependencies = [ - "syn 2.0.98", -] - [[package]] name = "digest" version = "0.9.0" @@ -2293,20 +2181,6 @@ dependencies = [ "types", ] -[[package]] -name = "dsl_auto_type" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" -dependencies = [ - "darling 0.20.10", - "either", - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "dtoa" version = "1.0.9" @@ -2907,7 +2781,7 @@ dependencies = [ "serde", "serde_json", "syn 1.0.109", - "toml 0.5.11", + "toml", "url", "walkdir", ] @@ -5647,22 +5521,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest 0.10.7", -] - [[package]] name = "mdbx-sys" version = "0.11.6-4" @@ -5737,27 +5595,6 @@ dependencies = [ "prometheus", ] -[[package]] -name = "migrations_internals" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd01039851e82f8799046eabbb354056283fb265c8ec0996af940f4e85a380ff" -dependencies = [ - "serde", - "toml 0.8.19", -] - -[[package]] -name = "migrations_macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" -dependencies = [ - "migrations_internals", - "proc-macro2", - "quote", -] - [[package]] name = "milhouse" version = "0.3.0" @@ -6629,24 +6466,6 @@ dependencies = [ "rustc_version 0.4.1", ] -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "pin-project" version = "1.1.8" @@ -6783,35 +6602,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" -[[package]] -name = "postgres-protocol" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator", - "hmac 0.12.1", - "md-5", - "memchr", - "rand 0.9.0", - "sha2 0.10.8", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" -dependencies = [ - "bytes", - "fallible-iterator", - "postgres-protocol", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -6827,16 +6617,6 @@ dependencies = [ "zerocopy 0.7.35", ] -[[package]] -name = "pq-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b51d65ebe1cb1f40641b15abae017fed35ccdda46e3dab1ff8768f625a3222" -dependencies = [ - "libc", - "vcpkg", -] - [[package]] name = "pretty_reqwest_error" version = "0.1.0" @@ -7400,7 +7180,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper", "system-configuration 0.5.1", "tokio", "tokio-native-tls", @@ -8057,16 +7837,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_repr" version = "0.1.19" @@ -8078,15 +7848,6 @@ dependencies = [ "syn 2.0.98", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -8099,28 +7860,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling 0.13.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -8298,12 +8037,6 @@ dependencies = [ "types", ] -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "slab" version = "0.4.9" @@ -8667,17 +8400,6 @@ dependencies = [ "zstd 0.13.2", ] -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.10.0" @@ -8770,12 +8492,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" - [[package]] name = "synstructure" version = "0.13.1" @@ -8960,23 +8676,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "testcontainers" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" -dependencies = [ - "bollard-stubs", - "futures", - "hex", - "hmac 0.12.1", - "log", - "rand 0.8.5", - "serde", - "serde_json", - "sha2 0.10.8", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -9221,32 +8920,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-postgres" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot 0.12.3", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.9.0", - "socket2", - "tokio", - "tokio-util", - "whoami", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -9304,26 +8977,11 @@ dependencies = [ "serde", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.22.23", -] - [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] [[package]] name = "toml_edit" @@ -9343,34 +9001,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap 2.7.1", - "serde", - "serde_spanned", "toml_datetime", "winnow 0.7.0", ] -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 1.0.2", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - [[package]] name = "tower-service" version = "0.3.3" @@ -9636,12 +9270,6 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" version = "1.0.16" @@ -9657,12 +9285,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -10094,12 +9716,6 @@ dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -10199,40 +9815,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "watch" -version = "0.1.0" -dependencies = [ - "axum", - "beacon_chain", - "beacon_node", - "bls", - "clap", - "clap_utils", - "diesel", - "diesel_migrations", - "env_logger 0.9.3", - "eth2", - "http_api", - "hyper 1.6.0", - "log", - "logging", - "network", - "r2d2", - "rand 0.8.5", - "reqwest", - "serde", - "serde_json", - "serde_yaml", - "task_executor", - "testcontainers", - "tokio", - "tokio-postgres", - "types", - "unused_port", - "url", -] - [[package]] name = "web-sys" version = "0.3.77" @@ -10299,17 +9881,6 @@ dependencies = [ "rustix 0.38.44", ] -[[package]] -name = "whoami" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" -dependencies = [ - "redox_syscall 0.5.8", - "wasite", - "web-sys", -] - [[package]] name = "widestring" version = "0.4.3" diff --git a/Cargo.toml b/Cargo.toml index 73912f6082..54729a60a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,8 +104,6 @@ members = [ "validator_client/validator_store", "validator_manager", - - "watch", ] resolver = "2" diff --git a/book/src/setup.md b/book/src/setup.md index d3da68f97c..7143c8f0fb 100644 --- a/book/src/setup.md +++ b/book/src/setup.md @@ -17,9 +17,6 @@ The additional requirements for developers are: some dependencies. See [`Installation Guide`](./installation.md) for more info. - [`java 17 runtime`](https://openjdk.java.net/projects/jdk/). 17 is the minimum, used by web3signer_tests. -- [`libpq-dev`](https://www.postgresql.org/docs/devel/libpq.html). Also known as - `libpq-devel` on some systems. -- [`docker`](https://www.docker.com/). Some tests need docker installed and **running**. ## Using `make` diff --git a/watch/.gitignore b/watch/.gitignore deleted file mode 100644 index 5b6b0720c9..0000000000 --- a/watch/.gitignore +++ /dev/null @@ -1 +0,0 @@ -config.yaml diff --git a/watch/Cargo.toml b/watch/Cargo.toml deleted file mode 100644 index 41cfb58e28..0000000000 --- a/watch/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "watch" -version = "0.1.0" -edition = { workspace = true } - -[lib] -name = "watch" -path = "src/lib.rs" - -[[bin]] -name = "watch" -path = "src/main.rs" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -axum = "0.7" -beacon_node = { workspace = true } -bls = { workspace = true } -clap = { workspace = true } -clap_utils = { workspace = true } -diesel = { version = "2.0.2", features = ["postgres", "r2d2"] } -diesel_migrations = { version = "2.0.0", features = ["postgres"] } -env_logger = { workspace = true } -eth2 = { workspace = true } -hyper = { workspace = true } -log = { workspace = true } -r2d2 = { workspace = true } -rand = { workspace = true } -reqwest = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -tokio = { workspace = true } -types = { workspace = true } -url = { workspace = true } - -[dev-dependencies] -beacon_chain = { workspace = true } -http_api = { workspace = true } -logging = { workspace = true } -network = { workspace = true } -task_executor = { workspace = true } -testcontainers = "0.15" -tokio-postgres = "0.7.5" -unused_port = { workspace = true } diff --git a/watch/README.md b/watch/README.md deleted file mode 100644 index 877cddf234..0000000000 --- a/watch/README.md +++ /dev/null @@ -1,458 +0,0 @@ -## beacon.watch - ->beacon.watch is pre-MVP and still under active development and subject to change. - -beacon.watch is an Ethereum Beacon Chain monitoring platform whose goal is to provide fast access to -data which is: -1. Not already stored natively in the Beacon Chain -2. Too specialized for Block Explorers -3. Too sensitive for public Block Explorers - - -### Requirements -- `git` -- `rust` : https://rustup.rs/ -- `libpq` : https://www.postgresql.org/download/ -- `diesel_cli` : -``` -cargo install diesel_cli --no-default-features --features postgres -``` -- `docker` : https://docs.docker.com/engine/install/ -- `docker-compose` : https://docs.docker.com/compose/install/ - -### Setup -1. Setup the database: -``` -cd postgres_docker_compose -docker-compose up -``` - -1. Ensure the tests pass: -``` -cargo test --release -``` - -1. Drop the database (if it already exists) and run the required migrations: -``` -diesel database reset --database-url postgres://postgres:postgres@localhost/dev -``` - -1. Ensure a synced Lighthouse beacon node with historical states is available -at `localhost:5052`. - -1. Run the updater daemon: -``` -cargo run --release -- run-updater -``` - -1. Start the HTTP API server: -``` -cargo run --release -- serve -``` - -1. Ensure connectivity: -``` -curl "http://localhost:5059/v1/slots/highest" -``` - -> Functionality on MacOS has not been tested. Windows is not supported. - - -### Configuration -beacon.watch can be configured through the use of a config file. -Available options can be seen in `config.yaml.default`. - -You can specify a config file during runtime: -``` -cargo run -- run-updater --config path/to/config.yaml -cargo run -- serve --config path/to/config.yaml -``` - -You can specify only the parts of the config file which you need changed. -Missing values will remain as their defaults. - -For example, if you wish to run with default settings but only wish to alter `log_level` -your config file would be: -```yaml -# config.yaml -log_level = "info" -``` - -### Available Endpoints -As beacon.watch continues to develop, more endpoints will be added. - -> In these examples any data containing information from blockprint has either been redacted or fabricated. - -#### `/v1/slots/{slot}` -```bash -curl "http://localhost:5059/v1/slots/4635296" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "skipped": false, - "beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" -} -``` - -#### `/v1/slots?start_slot={}&end_slot={}` -```bash -curl "http://localhost:5059/v1/slots?start_slot=4635296&end_slot=4635297" -``` -```json -[ - { - "slot": "4635297", - "root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182", - "skipped": false, - "beacon_block": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182" - }, - { - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "skipped": false, - "beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" - } -] -``` - -#### `/v1/slots/lowest` -```bash -curl "http://localhost:5059/v1/slots/lowest" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "skipped": false, - "beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" -} -``` - -#### `/v1/slots/highest` -```bash -curl "http://localhost:5059/v1/slots/highest" -``` -```json -{ - "slot": "4635358", - "root": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b", - "skipped": false, - "beacon_block": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b" -} -``` - -#### `v1/slots/{slot}/block` -```bash -curl "http://localhost:5059/v1/slots/4635296/block" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" -} -``` - -#### `/v1/blocks/{block_id}` -```bash -curl "http://localhost:5059/v1/blocks/4635296" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" -} -``` - -#### `/v1/blocks?start_slot={}&end_slot={}` -```bash -curl "http://localhost:5059/v1/blocks?start_slot=4635296&end_slot=4635297" -``` -```json -[ - { - "slot": "4635297", - "root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182", - "parent_root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" - }, - { - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" - } -] -``` - -#### `/v1/blocks/{block_id}/previous` -```bash -curl "http://localhost:5059/v1/blocks/4635297/previous" -# OR -curl "http://localhost:5059/v1/blocks/0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182/previous" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" -} -``` - -#### `/v1/blocks/{block_id}/next` -```bash -curl "http://localhost:5059/v1/blocks/4635296/next" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/next" -``` -```json -{ - "slot": "4635297", - "root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182", - "parent_root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62" -} -``` - -#### `/v1/blocks/lowest` -```bash -curl "http://localhost:5059/v1/blocks/lowest" -``` -```json -{ - "slot": "4635296", - "root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62", - "parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b" -} -``` - -#### `/v1/blocks/highest` -```bash -curl "http://localhost:5059/v1/blocks/highest" -``` -```json -{ - "slot": "4635358", - "root": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b", - "parent_root": "0xb66e05418bb5b1d4a965c994e1f0e5b5f0d7b780e0df12f3f6321510654fa1d2" -} -``` - -#### `/v1/blocks/{block_id}/proposer` -```bash -curl "http://localhost:5059/v1/blocks/4635296/proposer" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/proposer" - -``` -```json -{ - "slot": "4635296", - "proposer_index": 223126, - "graffiti": "" -} -``` - -#### `/v1/blocks/{block_id}/rewards` -```bash -curl "http://localhost:5059/v1/blocks/4635296/reward" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/reward" - -``` -```json -{ - "slot": "4635296", - "total": 25380059, - "attestation_reward": 24351867, - "sync_committee_reward": 1028192 -} -``` - -#### `/v1/blocks/{block_id}/packing` -```bash -curl "http://localhost:5059/v1/blocks/4635296/packing" -# OR -curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/packing" - -``` -```json -{ - "slot": "4635296", - "available": 16152, - "included": 13101, - "prior_skip_slots": 0 -} -``` - -#### `/v1/validators/{validator}` -```bash -curl "http://localhost:5059/v1/validators/1" -# OR -curl "http://localhost:5059/v1/validators/0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c" -``` -```json -{ - "index": 1, - "public_key": "0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c", - "status": "active_ongoing", - "client": null, - "activation_epoch": 0, - "exit_epoch": null -} -``` - -#### `/v1/validators/{validator}/attestation/{epoch}` -```bash -curl "http://localhost:5059/v1/validators/1/attestation/144853" -# OR -curl "http://localhost:5059/v1/validators/0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c/attestation/144853" -``` -```json -{ - "index": 1, - "epoch": "144853", - "source": true, - "head": true, - "target": true -} -``` - -#### `/v1/validators/missed/{vote}/{epoch}` -```bash -curl "http://localhost:5059/v1/validators/missed/head/144853" -``` -```json -[ - 63, - 67, - 98, - ... -] -``` - -#### `/v1/validators/missed/{vote}/{epoch}/graffiti` -```bash -curl "http://localhost:5059/v1/validators/missed/head/144853/graffiti" -``` -```json -{ - "Mr F was here": 3, - "Lighthouse/v3.1.0-aa022f4": 5, - ... -} -``` - -#### `/v1/clients/missed/{vote}/{epoch}` -```bash -curl "http://localhost:5059/v1/clients/missed/source/144853" -``` -```json -{ - "Lighthouse": 100, - "Lodestar": 100, - "Nimbus": 100, - "Prysm": 100, - "Teku": 100, - "Unknown": 100 -} -``` - -#### `/v1/clients/missed/{vote}/{epoch}/percentages` -Note that this endpoint expresses the following: -``` -What percentage of each client implementation missed this vote? -``` - -```bash -curl "http://localhost:5059/v1/clients/missed/target/144853/percentages" -``` -```json -{ - "Lighthouse": 0.51234567890, - "Lodestar": 0.51234567890, - "Nimbus": 0.51234567890, - "Prysm": 0.09876543210, - "Teku": 0.09876543210, - "Unknown": 0.05647382910 -} -``` - -#### `/v1/clients/missed/{vote}/{epoch}/percentages/relative` -Note that this endpoint expresses the following: -``` -For the validators which did miss this vote, what percentage of them were from each client implementation? -``` -You can check these values against the output of `/v1/clients/percentages` to see any discrepancies. - -```bash -curl "http://localhost:5059/v1/clients/missed/target/144853/percentages/relative" -``` -```json -{ - "Lighthouse": 11.11111111111111, - "Lodestar": 11.11111111111111, - "Nimbus": 11.11111111111111, - "Prysm": 16.66666666666667, - "Teku": 16.66666666666667, - "Unknown": 33.33333333333333 -} - -``` - -#### `/v1/clients` -```bash -curl "http://localhost:5059/v1/clients" -``` -```json -{ - "Lighthouse": 5000, - "Lodestar": 5000, - "Nimbus": 5000, - "Prysm": 5000, - "Teku": 5000, - "Unknown": 5000 -} -``` - -#### `/v1/clients/percentages` -```bash -curl "http://localhost:5059/v1/clients/percentages" -``` -```json -{ - "Lighthouse": 16.66666666666667, - "Lodestar": 16.66666666666667, - "Nimbus": 16.66666666666667, - "Prysm": 16.66666666666667, - "Teku": 16.66666666666667, - "Unknown": 16.66666666666667 -} -``` - -### Future work -- New tables - - `skip_slots`? - - -- More API endpoints - - `/v1/proposers?start_epoch={}&end_epoch={}` and similar - - `/v1/validators/{status}/count` - - -- Concurrently backfill and forwards fill, so forwards fill is not bottlenecked by large backfills. - - -- Better/prettier (async?) logging. - - -- Connect to a range of beacon_nodes to sync different components concurrently. -Generally, processing certain api queries such as `block_packing` and `attestation_performance` take the longest to sync. - - -### Architecture -Connection Pooling: -- 1 Pool for Updater (read and write) -- 1 Pool for HTTP Server (should be read only, although not sure if we can enforce this) diff --git a/watch/config.yaml.default b/watch/config.yaml.default deleted file mode 100644 index 131609237c..0000000000 --- a/watch/config.yaml.default +++ /dev/null @@ -1,49 +0,0 @@ ---- -database: - user: "postgres" - password: "postgres" - dbname: "dev" - default_dbname: "postgres" - host: "localhost" - port: 5432 - connect_timeout_millis: 2000 - -server: - listen_addr: "127.0.0.1" - listen_port: 5059 - -updater: - # The URL of the Beacon Node to perform sync tasks with. - # Cannot yet accept multiple beacon nodes. - beacon_node_url: "http://localhost:5052" - # The number of epochs to backfill. Must be below 100. - max_backfill_size_epochs: 2 - # The epoch at which to stop backfilling. - backfill_stop_epoch: 0 - # Whether to sync the attestations table. - attestations: true - # Whether to sync the proposer_info table. - proposer_info: true - # Whether to sync the block_rewards table. - block_rewards: true - # Whether to sync the block_packing table. - block_packing: true - -blockprint: - # Whether to sync client information from blockprint. - enabled: false - # The URL of the blockprint server. - url: "" - # The username used to authenticate to the blockprint server. - username: "" - # The password used to authenticate to the blockprint server. - password: "" - -# Log level. -# Valid options are: -# - "trace" -# - "debug" -# - "info" -# - "warn" -# - "error" -log_level: "debug" diff --git a/watch/diesel.toml b/watch/diesel.toml deleted file mode 100644 index bfb01bccf0..0000000000 --- a/watch/diesel.toml +++ /dev/null @@ -1,5 +0,0 @@ -# For documentation on how to configure this file, -# see diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/database/schema.rs" diff --git a/watch/migrations/.gitkeep b/watch/migrations/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/watch/migrations/00000000000000_diesel_initial_setup/down.sql b/watch/migrations/00000000000000_diesel_initial_setup/down.sql deleted file mode 100644 index a9f5260911..0000000000 --- a/watch/migrations/00000000000000_diesel_initial_setup/down.sql +++ /dev/null @@ -1,6 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - -DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); -DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/watch/migrations/00000000000000_diesel_initial_setup/up.sql b/watch/migrations/00000000000000_diesel_initial_setup/up.sql deleted file mode 100644 index d68895b1a7..0000000000 --- a/watch/migrations/00000000000000_diesel_initial_setup/up.sql +++ /dev/null @@ -1,36 +0,0 @@ --- This file was automatically created by Diesel to setup helper functions --- and other internal bookkeeping. This file is safe to edit, any future --- changes will be added to existing projects as new migrations. - - - - --- Sets up a trigger for the given table to automatically set a column called --- `updated_at` whenever the row is modified (unless `updated_at` was included --- in the modified columns) --- --- # Example --- --- ```sql --- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); --- --- SELECT diesel_manage_updated_at('users'); --- ``` -CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ -BEGIN - EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s - FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); -END; -$$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ -BEGIN - IF ( - NEW IS DISTINCT FROM OLD AND - NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at - ) THEN - NEW.updated_at := current_timestamp; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/watch/migrations/2022-01-01-000000_canonical_slots/down.sql b/watch/migrations/2022-01-01-000000_canonical_slots/down.sql deleted file mode 100644 index 551ed6605c..0000000000 --- a/watch/migrations/2022-01-01-000000_canonical_slots/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE canonical_slots diff --git a/watch/migrations/2022-01-01-000000_canonical_slots/up.sql b/watch/migrations/2022-01-01-000000_canonical_slots/up.sql deleted file mode 100644 index 2629f11a4c..0000000000 --- a/watch/migrations/2022-01-01-000000_canonical_slots/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE canonical_slots ( - slot integer PRIMARY KEY, - root bytea NOT NULL, - skipped boolean NOT NULL, - beacon_block bytea UNIQUE -) diff --git a/watch/migrations/2022-01-01-000001_beacon_blocks/down.sql b/watch/migrations/2022-01-01-000001_beacon_blocks/down.sql deleted file mode 100644 index 8901956f47..0000000000 --- a/watch/migrations/2022-01-01-000001_beacon_blocks/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE beacon_blocks diff --git a/watch/migrations/2022-01-01-000001_beacon_blocks/up.sql b/watch/migrations/2022-01-01-000001_beacon_blocks/up.sql deleted file mode 100644 index 250c667b23..0000000000 --- a/watch/migrations/2022-01-01-000001_beacon_blocks/up.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE beacon_blocks ( - slot integer PRIMARY KEY REFERENCES canonical_slots(slot) ON DELETE CASCADE, - root bytea REFERENCES canonical_slots(beacon_block) NOT NULL, - parent_root bytea NOT NULL, - attestation_count integer NOT NULL, - transaction_count integer -) diff --git a/watch/migrations/2022-01-01-000002_validators/down.sql b/watch/migrations/2022-01-01-000002_validators/down.sql deleted file mode 100644 index 17819fc349..0000000000 --- a/watch/migrations/2022-01-01-000002_validators/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE validators diff --git a/watch/migrations/2022-01-01-000002_validators/up.sql b/watch/migrations/2022-01-01-000002_validators/up.sql deleted file mode 100644 index 69cfef6772..0000000000 --- a/watch/migrations/2022-01-01-000002_validators/up.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE validators ( - index integer PRIMARY KEY, - public_key bytea NOT NULL, - status text NOT NULL, - activation_epoch integer, - exit_epoch integer -) diff --git a/watch/migrations/2022-01-01-000003_proposer_info/down.sql b/watch/migrations/2022-01-01-000003_proposer_info/down.sql deleted file mode 100644 index d61330be5b..0000000000 --- a/watch/migrations/2022-01-01-000003_proposer_info/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE proposer_info diff --git a/watch/migrations/2022-01-01-000003_proposer_info/up.sql b/watch/migrations/2022-01-01-000003_proposer_info/up.sql deleted file mode 100644 index 488aedb273..0000000000 --- a/watch/migrations/2022-01-01-000003_proposer_info/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE proposer_info ( - slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE, - proposer_index integer REFERENCES validators(index) ON DELETE CASCADE NOT NULL, - graffiti text NOT NULL -) diff --git a/watch/migrations/2022-01-01-000004_active_config/down.sql b/watch/migrations/2022-01-01-000004_active_config/down.sql deleted file mode 100644 index b4304eb7b7..0000000000 --- a/watch/migrations/2022-01-01-000004_active_config/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE active_config diff --git a/watch/migrations/2022-01-01-000004_active_config/up.sql b/watch/migrations/2022-01-01-000004_active_config/up.sql deleted file mode 100644 index 476a091160..0000000000 --- a/watch/migrations/2022-01-01-000004_active_config/up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE active_config ( - id integer PRIMARY KEY CHECK (id=1), - config_name text NOT NULL, - slots_per_epoch integer NOT NULL -) diff --git a/watch/migrations/2022-01-01-000010_blockprint/down.sql b/watch/migrations/2022-01-01-000010_blockprint/down.sql deleted file mode 100644 index fa53325dad..0000000000 --- a/watch/migrations/2022-01-01-000010_blockprint/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE blockprint diff --git a/watch/migrations/2022-01-01-000010_blockprint/up.sql b/watch/migrations/2022-01-01-000010_blockprint/up.sql deleted file mode 100644 index 2d5741f50b..0000000000 --- a/watch/migrations/2022-01-01-000010_blockprint/up.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE blockprint ( - slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE, - best_guess text NOT NULL -) diff --git a/watch/migrations/2022-01-01-000011_block_rewards/down.sql b/watch/migrations/2022-01-01-000011_block_rewards/down.sql deleted file mode 100644 index 2dc87995c7..0000000000 --- a/watch/migrations/2022-01-01-000011_block_rewards/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE block_rewards diff --git a/watch/migrations/2022-01-01-000011_block_rewards/up.sql b/watch/migrations/2022-01-01-000011_block_rewards/up.sql deleted file mode 100644 index 47cb4304f0..0000000000 --- a/watch/migrations/2022-01-01-000011_block_rewards/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE block_rewards ( - slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE, - total integer NOT NULL, - attestation_reward integer NOT NULL, - sync_committee_reward integer NOT NULL -) diff --git a/watch/migrations/2022-01-01-000012_block_packing/down.sql b/watch/migrations/2022-01-01-000012_block_packing/down.sql deleted file mode 100644 index e9e7755e3e..0000000000 --- a/watch/migrations/2022-01-01-000012_block_packing/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE block_packing diff --git a/watch/migrations/2022-01-01-000012_block_packing/up.sql b/watch/migrations/2022-01-01-000012_block_packing/up.sql deleted file mode 100644 index 63a9925f92..0000000000 --- a/watch/migrations/2022-01-01-000012_block_packing/up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE block_packing ( - slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE, - available integer NOT NULL, - included integer NOT NULL, - prior_skip_slots integer NOT NULL -) diff --git a/watch/migrations/2022-01-01-000013_suboptimal_attestations/down.sql b/watch/migrations/2022-01-01-000013_suboptimal_attestations/down.sql deleted file mode 100644 index 0f32b6b4f3..0000000000 --- a/watch/migrations/2022-01-01-000013_suboptimal_attestations/down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE suboptimal_attestations diff --git a/watch/migrations/2022-01-01-000013_suboptimal_attestations/up.sql b/watch/migrations/2022-01-01-000013_suboptimal_attestations/up.sql deleted file mode 100644 index 5352afefc8..0000000000 --- a/watch/migrations/2022-01-01-000013_suboptimal_attestations/up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE suboptimal_attestations ( - epoch_start_slot integer CHECK (epoch_start_slot % 32 = 0) REFERENCES canonical_slots(slot) ON DELETE CASCADE, - index integer NOT NULL REFERENCES validators(index) ON DELETE CASCADE, - source boolean NOT NULL, - head boolean NOT NULL, - target boolean NOT NULL, - PRIMARY KEY(epoch_start_slot, index) -) diff --git a/watch/migrations/2022-01-01-000020_capella/down.sql b/watch/migrations/2022-01-01-000020_capella/down.sql deleted file mode 100644 index 5903b351db..0000000000 --- a/watch/migrations/2022-01-01-000020_capella/down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE beacon_blocks -DROP COLUMN withdrawal_count; diff --git a/watch/migrations/2022-01-01-000020_capella/up.sql b/watch/migrations/2022-01-01-000020_capella/up.sql deleted file mode 100644 index b52b4b0099..0000000000 --- a/watch/migrations/2022-01-01-000020_capella/up.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE beacon_blocks -ADD COLUMN withdrawal_count integer; - diff --git a/watch/postgres_docker_compose/compose.yml b/watch/postgres_docker_compose/compose.yml deleted file mode 100644 index eae4de4a2b..0000000000 --- a/watch/postgres_docker_compose/compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: "3" - -services: - postgres: - image: postgres:12.3-alpine - restart: always - environment: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - volumes: - - postgres:/var/lib/postgresql/data - ports: - - 127.0.0.1:5432:5432 - -volumes: - postgres: diff --git a/watch/src/block_packing/database.rs b/watch/src/block_packing/database.rs deleted file mode 100644 index f7375431cb..0000000000 --- a/watch/src/block_packing/database.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::database::{ - schema::{beacon_blocks, block_packing}, - watch_types::{WatchHash, WatchSlot}, - Error, PgConn, MAX_SIZE_BATCH_INSERT, -}; - -use diesel::prelude::*; -use diesel::{Insertable, Queryable}; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::time::Instant; - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = block_packing)] -pub struct WatchBlockPacking { - pub slot: WatchSlot, - pub available: i32, - pub included: i32, - pub prior_skip_slots: i32, -} - -/// Insert a batch of values into the `block_packing` table. -/// -/// On a conflict, it will do nothing, leaving the old value. -pub fn insert_batch_block_packing( - conn: &mut PgConn, - packing: Vec, -) -> Result<(), Error> { - use self::block_packing::dsl::*; - - let mut count = 0; - let timer = Instant::now(); - - for chunk in packing.chunks(MAX_SIZE_BATCH_INSERT) { - count += diesel::insert_into(block_packing) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - let time_taken = timer.elapsed(); - debug!("Block packing inserted, count: {count}, time taken: {time_taken:?}"); - Ok(()) -} - -/// Selects the row from the `block_packing` table where `slot` is minimum. -pub fn get_lowest_block_packing(conn: &mut PgConn) -> Result, Error> { - use self::block_packing::dsl::*; - let timer = Instant::now(); - - let result = block_packing - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block packing requested: lowest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `block_packing` table where `slot` is maximum. -pub fn get_highest_block_packing(conn: &mut PgConn) -> Result, Error> { - use self::block_packing::dsl::*; - let timer = Instant::now(); - - let result = block_packing - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block packing requested: highest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `block_packing` table corresponding to a given `root_query`. -pub fn get_block_packing_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root}; - use self::block_packing::dsl::*; - let timer = Instant::now(); - - let join = beacon_blocks.inner_join(block_packing); - - let result = join - .select((slot, available, included, prior_skip_slots)) - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block packing requested: {root_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `block_packing` table corresponding to a given `slot_query`. -pub fn get_block_packing_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::block_packing::dsl::*; - let timer = Instant::now(); - - let result = block_packing - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block packing requested: {slot_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding -/// row in `block_packing`. -#[allow(dead_code)] -pub fn get_unknown_block_packing( - conn: &mut PgConn, - slots_per_epoch: u64, -) -> Result>, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root, slot}; - use self::block_packing::dsl::block_packing; - - let join = beacon_blocks.left_join(block_packing); - - let result = join - .select(slot) - .filter(root.is_null()) - // Block packing cannot be retrieved for epoch 0 so we need to exclude them. - .filter(slot.ge(slots_per_epoch as i32)) - .order_by(slot.desc()) - .nullable() - .load::>(conn)?; - - Ok(result) -} diff --git a/watch/src/block_packing/mod.rs b/watch/src/block_packing/mod.rs deleted file mode 100644 index 5d74fc5979..0000000000 --- a/watch/src/block_packing/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -pub mod database; -pub mod server; -pub mod updater; - -use crate::database::watch_types::WatchSlot; -use crate::updater::error::Error; - -pub use database::{ - get_block_packing_by_root, get_block_packing_by_slot, get_highest_block_packing, - get_lowest_block_packing, get_unknown_block_packing, insert_batch_block_packing, - WatchBlockPacking, -}; -pub use server::block_packing_routes; - -use eth2::BeaconNodeHttpClient; -use types::Epoch; - -/// Sends a request to `lighthouse/analysis/block_packing`. -/// Formats the response into a vector of `WatchBlockPacking`. -/// -/// Will fail if `start_epoch == 0`. -pub async fn get_block_packing( - bn: &BeaconNodeHttpClient, - start_epoch: Epoch, - end_epoch: Epoch, -) -> Result, Error> { - Ok(bn - .get_lighthouse_analysis_block_packing(start_epoch, end_epoch) - .await? - .into_iter() - .map(|data| WatchBlockPacking { - slot: WatchSlot::from_slot(data.slot), - available: data.available_attestations as i32, - included: data.included_attestations as i32, - prior_skip_slots: data.prior_skip_slots as i32, - }) - .collect()) -} diff --git a/watch/src/block_packing/server.rs b/watch/src/block_packing/server.rs deleted file mode 100644 index 819144562a..0000000000 --- a/watch/src/block_packing/server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::block_packing::database::{ - get_block_packing_by_root, get_block_packing_by_slot, WatchBlockPacking, -}; -use crate::database::{get_connection, PgPool, WatchHash, WatchSlot}; -use crate::server::Error; - -use axum::{extract::Path, routing::get, Extension, Json, Router}; -use eth2::types::BlockId; -use std::str::FromStr; - -pub async fn get_block_packing( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(get_block_packing_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(get_block_packing_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub fn block_packing_routes() -> Router { - Router::new().route("/v1/blocks/:block/packing", get(get_block_packing)) -} diff --git a/watch/src/block_packing/updater.rs b/watch/src/block_packing/updater.rs deleted file mode 100644 index 34847f6264..0000000000 --- a/watch/src/block_packing/updater.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::database::{self, Error as DbError}; -use crate::updater::{Error, UpdateHandler}; - -use crate::block_packing::get_block_packing; - -use eth2::types::{Epoch, EthSpec}; -use log::{debug, error, warn}; - -const MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING: u64 = 50; - -impl UpdateHandler { - /// Forward fills the `block_packing` table starting from the entry with the - /// highest slot. - /// - /// It constructs a request to the `get_block_packing` API with: - /// `start_epoch` -> highest completely filled epoch + 1 (or epoch of lowest beacon block) - /// `end_epoch` -> epoch of highest beacon block - /// - /// It will resync the latest epoch if it is not fully filled. - /// That is, `if highest_filled_slot % slots_per_epoch != 31` - /// This means that if the last slot of an epoch is a skip slot, the whole epoch will be - //// resynced during the next head update. - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`. - pub async fn fill_block_packing(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - // Get the slot of the highest entry in the `block_packing` table. - let highest_filled_slot_opt = if self.config.block_packing { - database::get_highest_block_packing(&mut conn)?.map(|packing| packing.slot) - } else { - return Err(Error::NotEnabled("block_packing".to_string())); - }; - - let mut start_epoch = if let Some(highest_filled_slot) = highest_filled_slot_opt { - if highest_filled_slot.as_slot() % self.slots_per_epoch - == self.slots_per_epoch.saturating_sub(1) - { - // The whole epoch is filled so we can begin syncing the next one. - highest_filled_slot.as_slot().epoch(self.slots_per_epoch) + 1 - } else { - // The epoch is only partially synced. Try to sync it fully. - highest_filled_slot.as_slot().epoch(self.slots_per_epoch) - } - } else { - // No entries in the `block_packing` table. Use `beacon_blocks` instead. - if let Some(lowest_beacon_block) = database::get_lowest_beacon_block(&mut conn)? { - lowest_beacon_block - .slot - .as_slot() - .epoch(self.slots_per_epoch) - } else { - // There are no blocks in the database, do not fill the `block_packing` table. - warn!("Refusing to fill block packing as there are no blocks in the database"); - return Ok(()); - } - }; - - // The `get_block_packing` API endpoint cannot accept `start_epoch == 0`. - if start_epoch == 0 { - start_epoch += 1 - } - - if let Some(highest_block_slot) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot()) - { - let mut end_epoch = highest_block_slot.epoch(self.slots_per_epoch); - - if start_epoch > end_epoch { - debug!("Block packing is up to date with the head of the database"); - return Ok(()); - } - - // Ensure the size of the request does not exceed the maximum allowed value. - if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING) { - end_epoch = start_epoch + MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING - } - - if let Some(lowest_block_slot) = - database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot()) - { - let mut packing = get_block_packing(&self.bn, start_epoch, end_epoch).await?; - - // Since we pull a full epoch of data but are not guaranteed to have all blocks of - // that epoch available, only insert blocks with corresponding `beacon_block`s. - packing.retain(|packing| { - packing.slot.as_slot() >= lowest_block_slot - && packing.slot.as_slot() <= highest_block_slot - }); - database::insert_batch_block_packing(&mut conn, packing)?; - } else { - return Err(Error::Database(DbError::Other( - "Database did not return a lowest block when one exists".to_string(), - ))); - } - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the - // `block_packing` table. This is a critical failure. It usually means someone has - // manually tampered with the database tables and should not occur during normal - // operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } - - /// Backfill the `block_packing` table starting from the entry with the lowest slot. - /// - /// It constructs a request to the `get_block_packing` function with: - /// `start_epoch` -> epoch of lowest_beacon_block - /// `end_epoch` -> epoch of lowest filled `block_packing` - 1 (or epoch of highest beacon block) - /// - /// It will resync the lowest epoch if it is not fully filled. - /// That is, `if lowest_filled_slot % slots_per_epoch != 0` - /// This means that if the last slot of an epoch is a skip slot, the whole epoch will be - //// resynced during the next head update. - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`. - pub async fn backfill_block_packing(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let max_block_packing_backfill = self.config.max_backfill_size_epochs; - - // Get the slot of the lowest entry in the `block_packing` table. - let lowest_filled_slot_opt = if self.config.block_packing { - database::get_lowest_block_packing(&mut conn)?.map(|packing| packing.slot) - } else { - return Err(Error::NotEnabled("block_packing".to_string())); - }; - - let end_epoch = if let Some(lowest_filled_slot) = lowest_filled_slot_opt { - if lowest_filled_slot.as_slot() % self.slots_per_epoch == 0 { - lowest_filled_slot - .as_slot() - .epoch(self.slots_per_epoch) - .saturating_sub(Epoch::new(1)) - } else { - // The epoch is only partially synced. Try to sync it fully. - lowest_filled_slot.as_slot().epoch(self.slots_per_epoch) - } - } else { - // No entries in the `block_packing` table. Use `beacon_blocks` instead. - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - highest_beacon_block.as_slot().epoch(self.slots_per_epoch) - } else { - // There are no blocks in the database, do not backfill the `block_packing` table. - warn!("Refusing to backfill block packing as there are no blocks in the database"); - return Ok(()); - } - }; - - if end_epoch <= 1 { - debug!("Block packing backfill is complete"); - return Ok(()); - } - - if let Some(lowest_block_slot) = - database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot()) - { - let mut start_epoch = lowest_block_slot.epoch(self.slots_per_epoch); - - if start_epoch >= end_epoch { - debug!("Block packing is up to date with the base of the database"); - return Ok(()); - } - - // Ensure that the request range does not exceed `max_block_packing_backfill` or - // `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`. - if start_epoch < end_epoch.saturating_sub(max_block_packing_backfill) { - start_epoch = end_epoch.saturating_sub(max_block_packing_backfill) - } - if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING) { - start_epoch = end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING) - } - - // The `block_packing` API cannot accept `start_epoch == 0`. - if start_epoch == 0 { - start_epoch += 1 - } - - if let Some(highest_block_slot) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot()) - { - let mut packing = get_block_packing(&self.bn, start_epoch, end_epoch).await?; - - // Only insert blocks with corresponding `beacon_block`s. - packing.retain(|packing| { - packing.slot.as_slot() >= lowest_block_slot - && packing.slot.as_slot() <= highest_block_slot - }); - - database::insert_batch_block_packing(&mut conn, packing)?; - } else { - return Err(Error::Database(DbError::Other( - "Database did not return a lowest block when one exists".to_string(), - ))); - } - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the - // `block_packing` table. This is a critical failure. It usually means someone has - // manually tampered with the database tables and should not occur during normal - // operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } -} diff --git a/watch/src/block_rewards/database.rs b/watch/src/block_rewards/database.rs deleted file mode 100644 index a2bf49f3e4..0000000000 --- a/watch/src/block_rewards/database.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::database::{ - schema::{beacon_blocks, block_rewards}, - watch_types::{WatchHash, WatchSlot}, - Error, PgConn, MAX_SIZE_BATCH_INSERT, -}; - -use diesel::prelude::*; -use diesel::{Insertable, Queryable}; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::time::Instant; - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = block_rewards)] -pub struct WatchBlockRewards { - pub slot: WatchSlot, - pub total: i32, - pub attestation_reward: i32, - pub sync_committee_reward: i32, -} - -/// Insert a batch of values into the `block_rewards` table. -/// -/// On a conflict, it will do nothing, leaving the old value. -pub fn insert_batch_block_rewards( - conn: &mut PgConn, - rewards: Vec, -) -> Result<(), Error> { - use self::block_rewards::dsl::*; - - let mut count = 0; - let timer = Instant::now(); - - for chunk in rewards.chunks(MAX_SIZE_BATCH_INSERT) { - count += diesel::insert_into(block_rewards) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - let time_taken = timer.elapsed(); - debug!("Block rewards inserted, count: {count}, time_taken: {time_taken:?}"); - Ok(()) -} - -/// Selects the row from the `block_rewards` table where `slot` is minimum. -pub fn get_lowest_block_rewards(conn: &mut PgConn) -> Result, Error> { - use self::block_rewards::dsl::*; - let timer = Instant::now(); - - let result = block_rewards - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block rewards requested: lowest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `block_rewards` table where `slot` is maximum. -pub fn get_highest_block_rewards(conn: &mut PgConn) -> Result, Error> { - use self::block_rewards::dsl::*; - let timer = Instant::now(); - - let result = block_rewards - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block rewards requested: highest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `block_rewards` table corresponding to a given `root_query`. -pub fn get_block_rewards_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root}; - use self::block_rewards::dsl::*; - let timer = Instant::now(); - - let join = beacon_blocks.inner_join(block_rewards); - - let result = join - .select((slot, total, attestation_reward, sync_committee_reward)) - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block rewards requested: {root_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `block_rewards` table corresponding to a given `slot_query`. -pub fn get_block_rewards_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::block_rewards::dsl::*; - let timer = Instant::now(); - - let result = block_rewards - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Block rewards requested: {slot_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding -/// row in `block_rewards`. -#[allow(dead_code)] -pub fn get_unknown_block_rewards(conn: &mut PgConn) -> Result>, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root, slot}; - use self::block_rewards::dsl::block_rewards; - - let join = beacon_blocks.left_join(block_rewards); - - let result = join - .select(slot) - .filter(root.is_null()) - // Block rewards cannot be retrieved for `slot == 0` so we need to exclude it. - .filter(slot.ne(0)) - .order_by(slot.desc()) - .nullable() - .load::>(conn)?; - - Ok(result) -} diff --git a/watch/src/block_rewards/mod.rs b/watch/src/block_rewards/mod.rs deleted file mode 100644 index 0dac88ea58..0000000000 --- a/watch/src/block_rewards/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -pub mod database; -mod server; -mod updater; - -use crate::database::watch_types::WatchSlot; -use crate::updater::error::Error; - -pub use database::{ - get_block_rewards_by_root, get_block_rewards_by_slot, get_highest_block_rewards, - get_lowest_block_rewards, get_unknown_block_rewards, insert_batch_block_rewards, - WatchBlockRewards, -}; -pub use server::block_rewards_routes; - -use eth2::BeaconNodeHttpClient; -use types::Slot; - -/// Sends a request to `lighthouse/analysis/block_rewards`. -/// Formats the response into a vector of `WatchBlockRewards`. -/// -/// Will fail if `start_slot == 0`. -pub async fn get_block_rewards( - bn: &BeaconNodeHttpClient, - start_slot: Slot, - end_slot: Slot, -) -> Result, Error> { - Ok(bn - .get_lighthouse_analysis_block_rewards(start_slot, end_slot) - .await? - .into_iter() - .map(|data| WatchBlockRewards { - slot: WatchSlot::from_slot(data.meta.slot), - total: data.total as i32, - attestation_reward: data.attestation_rewards.total as i32, - sync_committee_reward: data.sync_committee_rewards as i32, - }) - .collect()) -} diff --git a/watch/src/block_rewards/server.rs b/watch/src/block_rewards/server.rs deleted file mode 100644 index 480346e25b..0000000000 --- a/watch/src/block_rewards/server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::block_rewards::database::{ - get_block_rewards_by_root, get_block_rewards_by_slot, WatchBlockRewards, -}; -use crate::database::{get_connection, PgPool, WatchHash, WatchSlot}; -use crate::server::Error; - -use axum::{extract::Path, routing::get, Extension, Json, Router}; -use eth2::types::BlockId; -use std::str::FromStr; - -pub async fn get_block_rewards( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(get_block_rewards_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(get_block_rewards_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub fn block_rewards_routes() -> Router { - Router::new().route("/v1/blocks/:block/rewards", get(get_block_rewards)) -} diff --git a/watch/src/block_rewards/updater.rs b/watch/src/block_rewards/updater.rs deleted file mode 100644 index e2893ad0fe..0000000000 --- a/watch/src/block_rewards/updater.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::database::{self, Error as DbError}; -use crate::updater::{Error, UpdateHandler}; - -use crate::block_rewards::get_block_rewards; - -use eth2::types::EthSpec; -use log::{debug, error, warn}; - -const MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS: u64 = 1600; - -impl UpdateHandler { - /// Forward fills the `block_rewards` table starting from the entry with the - /// highest slot. - /// - /// It constructs a request to the `get_block_rewards` API with: - /// `start_slot` -> highest filled `block_rewards` + 1 (or lowest beacon block) - /// `end_slot` -> highest beacon block - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`. - pub async fn fill_block_rewards(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - // Get the slot of the highest entry in the `block_rewards` table. - let highest_filled_slot_opt = if self.config.block_rewards { - database::get_highest_block_rewards(&mut conn)?.map(|reward| reward.slot) - } else { - return Err(Error::NotEnabled("block_rewards".to_string())); - }; - - let mut start_slot = if let Some(highest_filled_slot) = highest_filled_slot_opt { - highest_filled_slot.as_slot() + 1 - } else { - // No entries in the `block_rewards` table. Use `beacon_blocks` instead. - if let Some(lowest_beacon_block) = - database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot) - { - lowest_beacon_block.as_slot() - } else { - // There are no blocks in the database, do not fill the `block_rewards` table. - warn!("Refusing to fill block rewards as there are no blocks in the database"); - return Ok(()); - } - }; - - // The `block_rewards` API cannot accept `start_slot == 0`. - if start_slot == 0 { - start_slot += 1; - } - - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - let mut end_slot = highest_beacon_block.as_slot(); - - if start_slot > end_slot { - debug!("Block rewards are up to date with the head of the database"); - return Ok(()); - } - - // Ensure the size of the request does not exceed the maximum allowed value. - if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS) { - end_slot = start_slot + MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS - } - - let rewards = get_block_rewards(&self.bn, start_slot, end_slot).await?; - database::insert_batch_block_rewards(&mut conn, rewards)?; - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the - // `block_rewards` table. This is a critical failure. It usually means someone has - // manually tampered with the database tables and should not occur during normal - // operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } - - /// Backfill the `block_rewards` tables starting from the entry with the - /// lowest slot. - /// - /// It constructs a request to the `get_block_rewards` API with: - /// `start_slot` -> lowest_beacon_block - /// `end_slot` -> lowest filled `block_rewards` - 1 (or highest beacon block) - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`. - pub async fn backfill_block_rewards(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let max_block_reward_backfill = self.config.max_backfill_size_epochs * self.slots_per_epoch; - - // Get the slot of the lowest entry in the `block_rewards` table. - let lowest_filled_slot_opt = if self.config.block_rewards { - database::get_lowest_block_rewards(&mut conn)?.map(|reward| reward.slot) - } else { - return Err(Error::NotEnabled("block_rewards".to_string())); - }; - - let end_slot = if let Some(lowest_filled_slot) = lowest_filled_slot_opt { - lowest_filled_slot.as_slot().saturating_sub(1_u64) - } else { - // No entries in the `block_rewards` table. Use `beacon_blocks` instead. - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - highest_beacon_block.as_slot() - } else { - // There are no blocks in the database, do not backfill the `block_rewards` table. - warn!("Refusing to backfill block rewards as there are no blocks in the database"); - return Ok(()); - } - }; - - if end_slot <= 1 { - debug!("Block rewards backfill is complete"); - return Ok(()); - } - - if let Some(lowest_block_slot) = database::get_lowest_beacon_block(&mut conn)? { - let mut start_slot = lowest_block_slot.slot.as_slot(); - - if start_slot >= end_slot { - debug!("Block rewards are up to date with the base of the database"); - return Ok(()); - } - - // Ensure that the request range does not exceed `max_block_reward_backfill` or - // `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`. - if start_slot < end_slot.saturating_sub(max_block_reward_backfill) { - start_slot = end_slot.saturating_sub(max_block_reward_backfill) - } - - if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS) { - start_slot = end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS) - } - - // The `block_rewards` API cannot accept `start_slot == 0`. - if start_slot == 0 { - start_slot += 1 - } - - let rewards = get_block_rewards(&self.bn, start_slot, end_slot).await?; - - if self.config.block_rewards { - database::insert_batch_block_rewards(&mut conn, rewards)?; - } - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the - // `block_rewards` table. This is a critical failure. It usually means someone has - // manually tampered with the database tables and should not occur during normal - // operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } -} diff --git a/watch/src/blockprint/config.rs b/watch/src/blockprint/config.rs deleted file mode 100644 index 721fa7cb19..0000000000 --- a/watch/src/blockprint/config.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub const fn enabled() -> bool { - false -} - -pub const fn url() -> Option { - None -} - -pub const fn username() -> Option { - None -} - -pub const fn password() -> Option { - None -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(default = "enabled")] - pub enabled: bool, - #[serde(default = "url")] - pub url: Option, - #[serde(default = "username")] - pub username: Option, - #[serde(default = "password")] - pub password: Option, -} - -impl Default for Config { - fn default() -> Self { - Config { - enabled: enabled(), - url: url(), - username: username(), - password: password(), - } - } -} diff --git a/watch/src/blockprint/database.rs b/watch/src/blockprint/database.rs deleted file mode 100644 index f0bc3f8ac8..0000000000 --- a/watch/src/blockprint/database.rs +++ /dev/null @@ -1,225 +0,0 @@ -use crate::database::{ - self, - schema::{beacon_blocks, blockprint}, - watch_types::{WatchHash, WatchSlot}, - Error, PgConn, MAX_SIZE_BATCH_INSERT, -}; - -use diesel::prelude::*; -use diesel::sql_types::{Integer, Text}; -use diesel::{Insertable, Queryable}; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::Instant; - -type WatchConsensusClient = String; -pub fn list_consensus_clients() -> Vec { - vec![ - "Lighthouse".to_string(), - "Lodestar".to_string(), - "Nimbus".to_string(), - "Prysm".to_string(), - "Teku".to_string(), - "Unknown".to_string(), - ] -} - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = blockprint)] -pub struct WatchBlockprint { - pub slot: WatchSlot, - pub best_guess: WatchConsensusClient, -} - -#[derive(Debug, QueryableByName, diesel::FromSqlRow)] -#[allow(dead_code)] -pub struct WatchValidatorBlockprint { - #[diesel(sql_type = Integer)] - pub proposer_index: i32, - #[diesel(sql_type = Text)] - pub best_guess: WatchConsensusClient, - #[diesel(sql_type = Integer)] - pub slot: WatchSlot, -} - -/// Insert a batch of values into the `blockprint` table. -/// -/// On a conflict, it will do nothing, leaving the old value. -pub fn insert_batch_blockprint( - conn: &mut PgConn, - prints: Vec, -) -> Result<(), Error> { - use self::blockprint::dsl::*; - - let mut count = 0; - let timer = Instant::now(); - - for chunk in prints.chunks(MAX_SIZE_BATCH_INSERT) { - count += diesel::insert_into(blockprint) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - let time_taken = timer.elapsed(); - debug!("Blockprint inserted, count: {count}, time_taken: {time_taken:?}"); - Ok(()) -} - -/// Selects the row from the `blockprint` table where `slot` is minimum. -pub fn get_lowest_blockprint(conn: &mut PgConn) -> Result, Error> { - use self::blockprint::dsl::*; - let timer = Instant::now(); - - let result = blockprint - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Blockprint requested: lowest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `blockprint` table where `slot` is maximum. -pub fn get_highest_blockprint(conn: &mut PgConn) -> Result, Error> { - use self::blockprint::dsl::*; - let timer = Instant::now(); - - let result = blockprint - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Blockprint requested: highest, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `blockprint` table corresponding to a given `root_query`. -pub fn get_blockprint_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root}; - use self::blockprint::dsl::*; - let timer = Instant::now(); - - let join = beacon_blocks.inner_join(blockprint); - - let result = join - .select((slot, best_guess)) - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Blockprint requested: {root_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `blockprint` table corresponding to a given `slot_query`. -pub fn get_blockprint_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::blockprint::dsl::*; - let timer = Instant::now(); - - let result = blockprint - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Blockprint requested: {slot_query}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding -/// row in `blockprint`. -#[allow(dead_code)] -pub fn get_unknown_blockprint(conn: &mut PgConn) -> Result>, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root, slot}; - use self::blockprint::dsl::blockprint; - - let join = beacon_blocks.left_join(blockprint); - - let result = join - .select(slot) - .filter(root.is_null()) - .order_by(slot.desc()) - .nullable() - .load::>(conn)?; - - Ok(result) -} - -/// Constructs a HashMap of `index` -> `best_guess` for each validator's latest proposal at or before -/// `target_slot`. -/// Inserts `"Unknown" if no prior proposals exist. -pub fn construct_validator_blockprints_at_slot( - conn: &mut PgConn, - target_slot: WatchSlot, - slots_per_epoch: u64, -) -> Result, Error> { - use self::blockprint::dsl::{blockprint, slot}; - - let total_validators = - database::count_validators_activated_before_slot(conn, target_slot, slots_per_epoch)? - as usize; - - let mut blockprint_map = HashMap::with_capacity(total_validators); - - let latest_proposals = - database::get_all_validators_latest_proposer_info_at_slot(conn, target_slot)?; - - let latest_proposal_slots: Vec = latest_proposals.clone().into_keys().collect(); - - let result = blockprint - .filter(slot.eq_any(latest_proposal_slots)) - .load::(conn)?; - - // Insert the validators which have available blockprints. - for print in result { - if let Some(proposer) = latest_proposals.get(&print.slot) { - blockprint_map.insert(*proposer, print.best_guess); - } - } - - // Insert the rest of the unknown validators. - for validator_index in 0..total_validators { - blockprint_map - .entry(validator_index as i32) - .or_insert_with(|| "Unknown".to_string()); - } - - Ok(blockprint_map) -} - -/// Counts the number of occurances of each `client` present in the `validators` table at or before some -/// `target_slot`. -pub fn get_validators_clients_at_slot( - conn: &mut PgConn, - target_slot: WatchSlot, - slots_per_epoch: u64, -) -> Result, Error> { - let mut client_map: HashMap = HashMap::new(); - - // This includes all validators which were activated at or before `target_slot`. - let validator_blockprints = - construct_validator_blockprints_at_slot(conn, target_slot, slots_per_epoch)?; - - for client in list_consensus_clients() { - let count = validator_blockprints - .iter() - .filter(|(_, v)| (*v).clone() == client) - .count(); - client_map.insert(client, count); - } - - Ok(client_map) -} diff --git a/watch/src/blockprint/mod.rs b/watch/src/blockprint/mod.rs deleted file mode 100644 index 319090c656..0000000000 --- a/watch/src/blockprint/mod.rs +++ /dev/null @@ -1,150 +0,0 @@ -pub mod database; -pub mod server; -pub mod updater; - -mod config; - -use crate::database::WatchSlot; - -use eth2::SensitiveUrl; -use reqwest::{Client, Response, Url}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::Duration; -use types::Slot; - -pub use config::Config; -pub use database::{ - get_blockprint_by_root, get_blockprint_by_slot, get_highest_blockprint, get_lowest_blockprint, - get_unknown_blockprint, get_validators_clients_at_slot, insert_batch_blockprint, - WatchBlockprint, -}; -pub use server::blockprint_routes; - -const TIMEOUT: Duration = Duration::from_secs(50); - -#[derive(Debug)] -#[allow(dead_code)] -pub enum Error { - Reqwest(reqwest::Error), - Url(url::ParseError), - BlockprintNotSynced, - Other(String), -} - -impl From for Error { - fn from(e: reqwest::Error) -> Self { - Error::Reqwest(e) - } -} - -impl From for Error { - fn from(e: url::ParseError) -> Self { - Error::Url(e) - } -} - -pub struct WatchBlockprintClient { - pub client: Client, - pub server: SensitiveUrl, - pub username: Option, - pub password: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct BlockprintSyncingResponse { - pub greatest_block_slot: Slot, - pub synced: bool, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct BlockprintResponse { - pub proposer_index: i32, - pub slot: Slot, - pub best_guess_single: String, -} - -impl WatchBlockprintClient { - async fn get(&self, url: Url) -> Result { - let mut builder = self.client.get(url).timeout(TIMEOUT); - if let Some(username) = &self.username { - builder = builder.basic_auth(username, self.password.as_ref()); - } - let response = builder.send().await.map_err(Error::Reqwest)?; - - if !response.status().is_success() { - return Err(Error::Other(response.text().await?)); - } - - Ok(response) - } - - // Returns the `greatest_block_slot` as reported by the Blockprint server. - // Will error if the Blockprint server is not synced. - #[allow(dead_code)] - pub async fn ensure_synced(&self) -> Result { - let url = self.server.full.join("sync/")?.join("status")?; - - let response = self.get(url).await?; - - let result = response.json::().await?; - if !result.synced { - return Err(Error::BlockprintNotSynced); - } - - Ok(result.greatest_block_slot) - } - - // Pulls the latest blockprint for all validators. - #[allow(dead_code)] - pub async fn blockprint_all_validators( - &self, - highest_validator: i32, - ) -> Result, Error> { - let url = self - .server - .full - .join("validator/")? - .join("blocks/")? - .join("latest")?; - - let response = self.get(url).await?; - - let mut result = response.json::>().await?; - result.retain(|print| print.proposer_index <= highest_validator); - - let mut map: HashMap = HashMap::with_capacity(result.len()); - for print in result { - map.insert(print.proposer_index, print.best_guess_single); - } - - Ok(map) - } - - // Construct a request to the Blockprint server for a range of slots between `start_slot` and - // `end_slot`. - pub async fn get_blockprint( - &self, - start_slot: Slot, - end_slot: Slot, - ) -> Result, Error> { - let url = self - .server - .full - .join("blocks/")? - .join(&format!("{start_slot}/{end_slot}"))?; - - let response = self.get(url).await?; - - let result = response - .json::>() - .await? - .iter() - .map(|response| WatchBlockprint { - slot: WatchSlot::from_slot(response.slot), - best_guess: response.best_guess_single.clone(), - }) - .collect(); - Ok(result) - } -} diff --git a/watch/src/blockprint/server.rs b/watch/src/blockprint/server.rs deleted file mode 100644 index 488af15717..0000000000 --- a/watch/src/blockprint/server.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::blockprint::database::{ - get_blockprint_by_root, get_blockprint_by_slot, WatchBlockprint, -}; -use crate::database::{get_connection, PgPool, WatchHash, WatchSlot}; -use crate::server::Error; - -use axum::{extract::Path, routing::get, Extension, Json, Router}; -use eth2::types::BlockId; -use std::str::FromStr; - -pub async fn get_blockprint( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(get_blockprint_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(get_blockprint_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub fn blockprint_routes() -> Router { - Router::new().route("/v1/blocks/:block/blockprint", get(get_blockprint)) -} diff --git a/watch/src/blockprint/updater.rs b/watch/src/blockprint/updater.rs deleted file mode 100644 index 7ec56dd9c8..0000000000 --- a/watch/src/blockprint/updater.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::database::{self, Error as DbError}; -use crate::updater::{Error, UpdateHandler}; - -use eth2::types::EthSpec; -use log::{debug, error, warn}; - -const MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT: u64 = 1600; - -impl UpdateHandler { - /// Forward fills the `blockprint` table starting from the entry with the - /// highest slot. - /// - /// It constructs a request to the `get_blockprint` API with: - /// `start_slot` -> highest filled `blockprint` + 1 (or lowest beacon block) - /// `end_slot` -> highest beacon block - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`. - pub async fn fill_blockprint(&mut self) -> Result<(), Error> { - // Ensure blockprint in enabled. - if let Some(blockprint_client) = &self.blockprint { - let mut conn = database::get_connection(&self.pool)?; - - // Get the slot of the highest entry in the `blockprint` table. - let mut start_slot = if let Some(highest_filled_slot) = - database::get_highest_blockprint(&mut conn)?.map(|print| print.slot) - { - highest_filled_slot.as_slot() + 1 - } else { - // No entries in the `blockprint` table. Use `beacon_blocks` instead. - if let Some(lowest_beacon_block) = - database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot) - { - lowest_beacon_block.as_slot() - } else { - // There are no blocks in the database, do not fill the `blockprint` table. - warn!("Refusing to fill blockprint as there are no blocks in the database"); - return Ok(()); - } - }; - - // The `blockprint` API cannot accept `start_slot == 0`. - if start_slot == 0 { - start_slot += 1; - } - - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - let mut end_slot = highest_beacon_block.as_slot(); - - if start_slot > end_slot { - debug!("Blockprint is up to date with the head of the database"); - return Ok(()); - } - - // Ensure the size of the request does not exceed the maximum allowed value. - if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT) { - end_slot = start_slot + MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT - } - - let mut prints = blockprint_client - .get_blockprint(start_slot, end_slot) - .await?; - - // Ensure the prints returned from blockprint are for slots which exist in the - // `beacon_blocks` table. - prints.retain(|print| { - database::get_beacon_block_by_slot(&mut conn, print.slot) - .ok() - .flatten() - .is_some() - }); - - database::insert_batch_blockprint(&mut conn, prints)?; - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in either - // `blockprint` table. This is a critical failure. It usually means - // someone has manually tampered with the database tables and should not occur during - // normal operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - } - - Ok(()) - } - - /// Backfill the `blockprint` table starting from the entry with the lowest slot. - /// - /// It constructs a request to the `get_blockprint` API with: - /// `start_slot` -> lowest_beacon_block - /// `end_slot` -> lowest filled `blockprint` - 1 (or highest beacon block) - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`. - pub async fn backfill_blockprint(&mut self) -> Result<(), Error> { - // Ensure blockprint in enabled. - if let Some(blockprint_client) = &self.blockprint { - let mut conn = database::get_connection(&self.pool)?; - let max_blockprint_backfill = - self.config.max_backfill_size_epochs * self.slots_per_epoch; - - // Get the slot of the lowest entry in the `blockprint` table. - let end_slot = if let Some(lowest_filled_slot) = - database::get_lowest_blockprint(&mut conn)?.map(|print| print.slot) - { - lowest_filled_slot.as_slot().saturating_sub(1_u64) - } else { - // No entries in the `blockprint` table. Use `beacon_blocks` instead. - if let Some(highest_beacon_block) = - database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot) - { - highest_beacon_block.as_slot() - } else { - // There are no blocks in the database, do not backfill the `blockprint` table. - warn!("Refusing to backfill blockprint as there are no blocks in the database"); - return Ok(()); - } - }; - - if end_slot <= 1 { - debug!("Blockprint backfill is complete"); - return Ok(()); - } - - if let Some(lowest_block_slot) = database::get_lowest_beacon_block(&mut conn)? { - let mut start_slot = lowest_block_slot.slot.as_slot(); - - if start_slot >= end_slot { - debug!("Blockprint are up to date with the base of the database"); - return Ok(()); - } - - // Ensure that the request range does not exceed `max_blockprint_backfill` or - // `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`. - if start_slot < end_slot.saturating_sub(max_blockprint_backfill) { - start_slot = end_slot.saturating_sub(max_blockprint_backfill) - } - - if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT) { - start_slot = end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT) - } - - // The `blockprint` API cannot accept `start_slot == 0`. - if start_slot == 0 { - start_slot += 1 - } - - let mut prints = blockprint_client - .get_blockprint(start_slot, end_slot) - .await?; - - // Ensure the prints returned from blockprint are for slots which exist in the - // `beacon_blocks` table. - prints.retain(|print| { - database::get_beacon_block_by_slot(&mut conn, print.slot) - .ok() - .flatten() - .is_some() - }); - - database::insert_batch_blockprint(&mut conn, prints)?; - } else { - // There are no blocks in the `beacon_blocks` database, but there are entries in the `blockprint` - // table. This is a critical failure. It usually means someone has manually tampered with the - // database tables and should not occur during normal operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - } - Ok(()) - } -} diff --git a/watch/src/cli.rs b/watch/src/cli.rs deleted file mode 100644 index b7179efe5d..0000000000 --- a/watch/src/cli.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::{config::Config, logger, server, updater}; -use clap::{Arg, ArgAction, Command}; -use clap_utils::get_color_style; - -pub const SERVE: &str = "serve"; -pub const RUN_UPDATER: &str = "run-updater"; -pub const CONFIG: &str = "config"; - -fn run_updater() -> Command { - Command::new(RUN_UPDATER).styles(get_color_style()) -} - -fn serve() -> Command { - Command::new(SERVE).styles(get_color_style()) -} - -pub fn app() -> Command { - Command::new("beacon_watch_daemon") - .author("Sigma Prime ") - .styles(get_color_style()) - .arg( - Arg::new(CONFIG) - .long(CONFIG) - .value_name("PATH_TO_CONFIG") - .help("Path to configuration file") - .action(ArgAction::Set) - .global(true), - ) - .subcommand(run_updater()) - .subcommand(serve()) -} - -pub async fn run() -> Result<(), String> { - let matches = app().get_matches(); - - let config = match matches.get_one::(CONFIG) { - Some(path) => Config::load_from_file(path.to_string())?, - None => Config::default(), - }; - - logger::init_logger(&config.log_level); - - match matches.subcommand() { - Some((RUN_UPDATER, _)) => updater::run_updater(config) - .await - .map_err(|e| format!("Failure: {:?}", e)), - Some((SERVE, _)) => server::serve(config) - .await - .map_err(|e| format!("Failure: {:?}", e)), - _ => Err("Unsupported subcommand. See --help".into()), - } -} diff --git a/watch/src/client.rs b/watch/src/client.rs deleted file mode 100644 index 43aaccde34..0000000000 --- a/watch/src/client.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::block_packing::WatchBlockPacking; -use crate::block_rewards::WatchBlockRewards; -use crate::database::models::{ - WatchBeaconBlock, WatchCanonicalSlot, WatchProposerInfo, WatchValidator, -}; -use crate::suboptimal_attestations::WatchAttestation; - -use eth2::types::BlockId; -use reqwest::Client; -use serde::de::DeserializeOwned; -use types::Hash256; -use url::Url; - -#[derive(Debug)] -pub enum Error { - Reqwest(reqwest::Error), - Url(url::ParseError), -} - -impl From for Error { - fn from(e: reqwest::Error) -> Self { - Error::Reqwest(e) - } -} - -impl From for Error { - fn from(e: url::ParseError) -> Self { - Error::Url(e) - } -} - -pub struct WatchHttpClient { - pub client: Client, - pub server: Url, -} - -impl WatchHttpClient { - async fn get_opt(&self, url: Url) -> Result, Error> { - let response = self.client.get(url).send().await?; - - if response.status() == 404 { - Ok(None) - } else { - response - .error_for_status()? - .json() - .await - .map_err(Into::into) - } - } - - pub async fn get_beacon_blocks( - &self, - block_id: BlockId, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&block_id.to_string())?; - - self.get_opt(url).await - } - - pub async fn get_lowest_canonical_slot(&self) -> Result, Error> { - let url = self.server.join("v1/")?.join("slots/")?.join("lowest")?; - - self.get_opt(url).await - } - - pub async fn get_highest_canonical_slot(&self) -> Result, Error> { - let url = self.server.join("v1/")?.join("slots/")?.join("highest")?; - - self.get_opt(url).await - } - - pub async fn get_lowest_beacon_block(&self) -> Result, Error> { - let url = self.server.join("v1/")?.join("blocks/")?.join("lowest")?; - - self.get_opt(url).await - } - - pub async fn get_highest_beacon_block(&self) -> Result, Error> { - let url = self.server.join("v1/")?.join("blocks/")?.join("highest")?; - - self.get_opt(url).await - } - - pub async fn get_next_beacon_block( - &self, - parent: Hash256, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&format!("{parent:?}/"))? - .join("next")?; - - self.get_opt(url).await - } - - pub async fn get_validator_by_index( - &self, - index: i32, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("validators/")? - .join(&format!("{index}"))?; - - self.get_opt(url).await - } - - pub async fn get_proposer_info( - &self, - block_id: BlockId, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&format!("{block_id}/"))? - .join("proposer")?; - - self.get_opt(url).await - } - - pub async fn get_block_reward( - &self, - block_id: BlockId, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&format!("{block_id}/"))? - .join("rewards")?; - - self.get_opt(url).await - } - - pub async fn get_block_packing( - &self, - block_id: BlockId, - ) -> Result, Error> { - let url = self - .server - .join("v1/")? - .join("blocks/")? - .join(&format!("{block_id}/"))? - .join("packing")?; - - self.get_opt(url).await - } - - pub async fn get_all_validators(&self) -> Result>, Error> { - let url = self.server.join("v1/")?.join("validators/")?.join("all")?; - - self.get_opt(url).await - } - - pub async fn get_attestations( - &self, - epoch: i32, - ) -> Result>, Error> { - let url = self - .server - .join("v1/")? - .join("validators/")? - .join("all/")? - .join("attestation/")? - .join(&format!("{epoch}"))?; - - self.get_opt(url).await - } -} diff --git a/watch/src/config.rs b/watch/src/config.rs deleted file mode 100644 index 4e61f9df9c..0000000000 --- a/watch/src/config.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::blockprint::Config as BlockprintConfig; -use crate::database::Config as DatabaseConfig; -use crate::server::Config as ServerConfig; -use crate::updater::Config as UpdaterConfig; - -use serde::{Deserialize, Serialize}; -use std::fs::File; - -pub const LOG_LEVEL: &str = "debug"; - -fn log_level() -> String { - LOG_LEVEL.to_string() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(default)] - pub blockprint: BlockprintConfig, - #[serde(default)] - pub database: DatabaseConfig, - #[serde(default)] - pub server: ServerConfig, - #[serde(default)] - pub updater: UpdaterConfig, - /// The minimum severity for log messages. - #[serde(default = "log_level")] - pub log_level: String, -} - -impl Default for Config { - fn default() -> Self { - Self { - blockprint: BlockprintConfig::default(), - database: DatabaseConfig::default(), - server: ServerConfig::default(), - updater: UpdaterConfig::default(), - log_level: log_level(), - } - } -} - -impl Config { - pub fn load_from_file(path_to_file: String) -> Result { - let file = - File::open(path_to_file).map_err(|e| format!("Error reading config file: {:?}", e))?; - let config: Config = serde_yaml::from_reader(file) - .map_err(|e| format!("Error parsing config file: {:?}", e))?; - Ok(config) - } -} diff --git a/watch/src/database/compat.rs b/watch/src/database/compat.rs deleted file mode 100644 index e3e9e0df6f..0000000000 --- a/watch/src/database/compat.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Implementations of PostgreSQL compatibility traits. -use crate::database::watch_types::{WatchHash, WatchPK, WatchSlot}; -use diesel::deserialize::{self, FromSql}; -use diesel::pg::{Pg, PgValue}; -use diesel::serialize::{self, Output, ToSql}; -use diesel::sql_types::{Binary, Integer}; - -macro_rules! impl_to_from_sql_int { - ($type:ty) => { - impl ToSql for $type - where - i32: ToSql, - { - fn to_sql<'a>(&'a self, out: &mut Output<'a, '_, Pg>) -> serialize::Result { - let v = i32::try_from(self.as_u64()).map_err(|e| Box::new(e))?; - >::to_sql(&v, &mut out.reborrow()) - } - } - - impl FromSql for $type { - fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { - Ok(Self::new(i32::from_sql(bytes)? as u64)) - } - } - }; -} - -macro_rules! impl_to_from_sql_binary { - ($type:ty) => { - impl ToSql for $type { - fn to_sql<'a>(&'a self, out: &mut Output<'a, '_, Pg>) -> serialize::Result { - let b = self.as_bytes(); - <&[u8] as ToSql>::to_sql(&b, &mut out.reborrow()) - } - } - - impl FromSql for $type { - fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { - Self::from_bytes(bytes.as_bytes()).map_err(|e| e.to_string().into()) - } - } - }; -} - -impl_to_from_sql_int!(WatchSlot); -impl_to_from_sql_binary!(WatchHash); -impl_to_from_sql_binary!(WatchPK); diff --git a/watch/src/database/config.rs b/watch/src/database/config.rs deleted file mode 100644 index dc0c70832f..0000000000 --- a/watch/src/database/config.rs +++ /dev/null @@ -1,74 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub const USER: &str = "postgres"; -pub const PASSWORD: &str = "postgres"; -pub const DBNAME: &str = "dev"; -pub const DEFAULT_DBNAME: &str = "postgres"; -pub const HOST: &str = "localhost"; -pub const fn port() -> u16 { - 5432 -} -pub const fn connect_timeout_millis() -> u64 { - 2_000 // 2s -} - -fn user() -> String { - USER.to_string() -} - -fn password() -> String { - PASSWORD.to_string() -} - -fn dbname() -> String { - DBNAME.to_string() -} - -fn default_dbname() -> String { - DEFAULT_DBNAME.to_string() -} - -fn host() -> String { - HOST.to_string() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(default = "user")] - pub user: String, - #[serde(default = "password")] - pub password: String, - #[serde(default = "dbname")] - pub dbname: String, - #[serde(default = "default_dbname")] - pub default_dbname: String, - #[serde(default = "host")] - pub host: String, - #[serde(default = "port")] - pub port: u16, - #[serde(default = "connect_timeout_millis")] - pub connect_timeout_millis: u64, -} - -impl Default for Config { - fn default() -> Self { - Self { - user: user(), - password: password(), - dbname: dbname(), - default_dbname: default_dbname(), - host: host(), - port: port(), - connect_timeout_millis: connect_timeout_millis(), - } - } -} - -impl Config { - pub fn build_database_url(&self) -> String { - format!( - "postgres://{}:{}@{}:{}/{}", - self.user, self.password, self.host, self.port, self.dbname - ) - } -} diff --git a/watch/src/database/error.rs b/watch/src/database/error.rs deleted file mode 100644 index 8c5088fa13..0000000000 --- a/watch/src/database/error.rs +++ /dev/null @@ -1,55 +0,0 @@ -use bls::Error as BlsError; -use diesel::result::{ConnectionError, Error as PgError}; -use eth2::SensitiveError; -use r2d2::Error as PoolError; -use std::fmt; -use types::BeaconStateError; - -#[derive(Debug)] -pub enum Error { - BeaconState(BeaconStateError), - Database(PgError), - DatabaseCorrupted, - InvalidSig(BlsError), - PostgresConnection(ConnectionError), - Pool(PoolError), - SensitiveUrl(SensitiveError), - InvalidRoot, - Other(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From for Error { - fn from(e: BeaconStateError) -> Self { - Error::BeaconState(e) - } -} - -impl From for Error { - fn from(e: ConnectionError) -> Self { - Error::PostgresConnection(e) - } -} - -impl From for Error { - fn from(e: PgError) -> Self { - Error::Database(e) - } -} - -impl From for Error { - fn from(e: PoolError) -> Self { - Error::Pool(e) - } -} - -impl From for Error { - fn from(e: BlsError) -> Self { - Error::InvalidSig(e) - } -} diff --git a/watch/src/database/mod.rs b/watch/src/database/mod.rs deleted file mode 100644 index 7193b0744a..0000000000 --- a/watch/src/database/mod.rs +++ /dev/null @@ -1,786 +0,0 @@ -mod config; -mod error; - -pub mod compat; -pub mod models; -pub mod schema; -pub mod utils; -pub mod watch_types; - -use self::schema::{ - active_config, beacon_blocks, canonical_slots, proposer_info, suboptimal_attestations, - validators, -}; - -use diesel::dsl::max; -use diesel::prelude::*; -use diesel::r2d2::{Builder, ConnectionManager, Pool, PooledConnection}; -use diesel::upsert::excluded; -use log::{debug, info}; -use std::collections::HashMap; -use std::time::Instant; -use types::{EthSpec, SignedBeaconBlock}; - -pub use self::error::Error; -pub use self::models::{WatchBeaconBlock, WatchCanonicalSlot, WatchProposerInfo, WatchValidator}; -pub use self::watch_types::{WatchHash, WatchPK, WatchSlot}; - -// Clippy has false positives on these re-exports from Rust 1.75.0-beta.1. -#[allow(unused_imports)] -pub use crate::block_rewards::{ - get_block_rewards_by_root, get_block_rewards_by_slot, get_highest_block_rewards, - get_lowest_block_rewards, get_unknown_block_rewards, insert_batch_block_rewards, - WatchBlockRewards, -}; - -#[allow(unused_imports)] -pub use crate::block_packing::{ - get_block_packing_by_root, get_block_packing_by_slot, get_highest_block_packing, - get_lowest_block_packing, get_unknown_block_packing, insert_batch_block_packing, - WatchBlockPacking, -}; - -#[allow(unused_imports)] -pub use crate::suboptimal_attestations::{ - get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, get_attestation_by_pubkey, - get_highest_attestation, get_lowest_attestation, insert_batch_suboptimal_attestations, - WatchAttestation, WatchSuboptimalAttestation, -}; - -#[allow(unused_imports)] -pub use crate::blockprint::{ - get_blockprint_by_root, get_blockprint_by_slot, get_highest_blockprint, get_lowest_blockprint, - get_unknown_blockprint, get_validators_clients_at_slot, insert_batch_blockprint, - WatchBlockprint, -}; - -pub use config::Config; - -/// Batch inserts cannot exceed a certain size. -/// See https://github.com/diesel-rs/diesel/issues/2414. -/// For some reason, this seems to translate to 65535 / 5 (13107) records. -pub const MAX_SIZE_BATCH_INSERT: usize = 13107; - -pub type PgPool = Pool>; -pub type PgConn = PooledConnection>; - -/// Connect to a Postgresql database and build a connection pool. -pub fn build_connection_pool(config: &Config) -> Result { - let database_url = config.clone().build_database_url(); - info!("Building connection pool at: {database_url}"); - let pg = ConnectionManager::::new(&database_url); - Builder::new().build(pg).map_err(Error::Pool) -} - -/// Retrieve an idle connection from the pool. -pub fn get_connection(pool: &PgPool) -> Result { - pool.get().map_err(Error::Pool) -} - -/// Insert the active config into the database. This is used to check if the connected beacon node -/// is compatible with the database. These values will not change (except -/// `current_blockprint_checkpoint`). -pub fn insert_active_config( - conn: &mut PgConn, - new_config_name: String, - new_slots_per_epoch: u64, -) -> Result<(), Error> { - use self::active_config::dsl::*; - - diesel::insert_into(active_config) - .values(&vec![( - id.eq(1), - config_name.eq(new_config_name), - slots_per_epoch.eq(new_slots_per_epoch as i32), - )]) - .on_conflict_do_nothing() - .execute(conn)?; - - Ok(()) -} - -/// Get the active config from the database. -pub fn get_active_config(conn: &mut PgConn) -> Result, Error> { - use self::active_config::dsl::*; - Ok(active_config - .select((config_name, slots_per_epoch)) - .filter(id.eq(1)) - .first::<(String, i32)>(conn) - .optional()?) -} - -/* - * INSERT statements - */ - -/// Inserts a single row into the `canonical_slots` table. -/// If `new_slot.beacon_block` is `None`, the value in the row will be `null`. -/// -/// On a conflict, it will do nothing, leaving the old value. -pub fn insert_canonical_slot(conn: &mut PgConn, new_slot: WatchCanonicalSlot) -> Result<(), Error> { - diesel::insert_into(canonical_slots::table) - .values(&new_slot) - .on_conflict_do_nothing() - .execute(conn)?; - - debug!("Canonical slot inserted: {}", new_slot.slot); - Ok(()) -} - -pub fn insert_beacon_block( - conn: &mut PgConn, - block: SignedBeaconBlock, - root: WatchHash, -) -> Result<(), Error> { - use self::canonical_slots::dsl::{beacon_block, slot as canonical_slot}; - - let block_message = block.message(); - - // Pull out relevant values from the block. - let slot = WatchSlot::from_slot(block.slot()); - let parent_root = WatchHash::from_hash(block.parent_root()); - let proposer_index = block_message.proposer_index() as i32; - let graffiti = block_message.body().graffiti().as_utf8_lossy(); - let attestation_count = block_message.body().attestations_len() as i32; - - let full_payload = block_message.execution_payload().ok(); - - let transaction_count: Option = if let Some(bellatrix_payload) = - full_payload.and_then(|payload| payload.execution_payload_bellatrix().ok()) - { - Some(bellatrix_payload.transactions.len() as i32) - } else { - full_payload - .and_then(|payload| payload.execution_payload_capella().ok()) - .map(|payload| payload.transactions.len() as i32) - }; - - let withdrawal_count: Option = full_payload - .and_then(|payload| payload.execution_payload_capella().ok()) - .map(|payload| payload.withdrawals.len() as i32); - - let block_to_add = WatchBeaconBlock { - slot, - root, - parent_root, - attestation_count, - transaction_count, - withdrawal_count, - }; - - let proposer_info_to_add = WatchProposerInfo { - slot, - proposer_index, - graffiti, - }; - - // Update the canonical slots table. - diesel::update(canonical_slots::table) - .set(beacon_block.eq(root)) - .filter(canonical_slot.eq(slot)) - // Do not overwrite the value if it already exists. - .filter(beacon_block.is_null()) - .execute(conn)?; - - diesel::insert_into(beacon_blocks::table) - .values(block_to_add) - .on_conflict_do_nothing() - .execute(conn)?; - - diesel::insert_into(proposer_info::table) - .values(proposer_info_to_add) - .on_conflict_do_nothing() - .execute(conn)?; - - debug!("Beacon block inserted at slot: {slot}, root: {root}, parent: {parent_root}"); - Ok(()) -} - -/// Insert a validator into the `validators` table -/// -/// On a conflict, it will only overwrite `status`, `activation_epoch` and `exit_epoch`. -pub fn insert_validator(conn: &mut PgConn, validator: WatchValidator) -> Result<(), Error> { - use self::validators::dsl::*; - let new_index = validator.index; - let new_public_key = validator.public_key; - - diesel::insert_into(validators) - .values(validator) - .on_conflict(index) - .do_update() - .set(( - status.eq(excluded(status)), - activation_epoch.eq(excluded(activation_epoch)), - exit_epoch.eq(excluded(exit_epoch)), - )) - .execute(conn)?; - - debug!("Validator inserted, index: {new_index}, public_key: {new_public_key}"); - Ok(()) -} - -/// Insert a batch of values into the `validators` table. -/// -/// On a conflict, it will do nothing. -/// -/// Should not be used when updating validators. -/// Validators should be updated through the `insert_validator` function which contains the correct -/// `on_conflict` clauses. -pub fn insert_batch_validators( - conn: &mut PgConn, - all_validators: Vec, -) -> Result<(), Error> { - use self::validators::dsl::*; - - let mut count = 0; - - for chunk in all_validators.chunks(1000) { - count += diesel::insert_into(validators) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - debug!("Validators inserted, count: {count}"); - Ok(()) -} - -/* - * SELECT statements - */ - -/// Selects a single row of the `canonical_slots` table corresponding to a given `slot_query`. -pub fn get_canonical_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: {slot_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `canonical_slots` table corresponding to a given `root_query`. -/// Only returns the non-skipped slot which matches `root`. -pub fn get_canonical_slot_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(root.eq(root_query)) - .filter(skipped.eq(false)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical root requested: {root_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `root` from a single row of the `canonical_slots` table corresponding to a given -/// `slot_query`. -#[allow(dead_code)] -pub fn get_root_at_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .select(root) - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: {slot_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from the row of the `canonical_slots` table corresponding to the minimum value -/// of `slot`. -pub fn get_lowest_canonical_slot(conn: &mut PgConn) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: lowest, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `slot` from the row of the `canonical_slots` table corresponding to the minimum value -/// of `slot` and where `skipped == false`. -pub fn get_lowest_non_skipped_canonical_slot( - conn: &mut PgConn, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(skipped.eq(false)) - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: lowest_non_skipped, time taken: {time_taken:?})"); - Ok(result) -} - -/// Select 'slot' from the row of the `canonical_slots` table corresponding to the maximum value -/// of `slot`. -pub fn get_highest_canonical_slot(conn: &mut PgConn) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: highest, time taken: {time_taken:?}"); - Ok(result) -} - -/// Select 'slot' from the row of the `canonical_slots` table corresponding to the maximum value -/// of `slot` and where `skipped == false`. -pub fn get_highest_non_skipped_canonical_slot( - conn: &mut PgConn, -) -> Result, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(skipped.eq(false)) - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Canonical slot requested: highest_non_skipped, time taken: {time_taken:?}"); - Ok(result) -} - -/// Select all rows of the `canonical_slots` table where `slot >= `start_slot && slot <= -/// `end_slot`. -pub fn get_canonical_slots_by_range( - conn: &mut PgConn, - start_slot: WatchSlot, - end_slot: WatchSlot, -) -> Result>, Error> { - use self::canonical_slots::dsl::*; - let timer = Instant::now(); - - let result = canonical_slots - .filter(slot.ge(start_slot)) - .filter(slot.le(end_slot)) - .load::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!( - "Canonical slots by range requested, start_slot: {}, end_slot: {}, time_taken: {:?}", - start_slot.as_u64(), - end_slot.as_u64(), - time_taken - ); - Ok(result) -} - -/// Selects `root` from all rows of the `canonical_slots` table which have `beacon_block == null` -/// and `skipped == false` -pub fn get_unknown_canonical_blocks(conn: &mut PgConn) -> Result, Error> { - use self::canonical_slots::dsl::*; - - let result = canonical_slots - .select(root) - .filter(beacon_block.is_null()) - .filter(skipped.eq(false)) - .order_by(slot.desc()) - .load::(conn)?; - - Ok(result) -} - -/// Selects the row from the `beacon_blocks` table where `slot` is minimum. -pub fn get_lowest_beacon_block(conn: &mut PgConn) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .order_by(slot.asc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Beacon block requested: lowest, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `beacon_blocks` table where `slot` is maximum. -pub fn get_highest_beacon_block(conn: &mut PgConn) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .order_by(slot.desc()) - .limit(1) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Beacon block requested: highest, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row from the `beacon_blocks` table corresponding to a given `root_query`. -pub fn get_beacon_block_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - let time_taken = timer.elapsed(); - debug!("Beacon block requested: {root_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row from the `beacon_blocks` table corresponding to a given `slot_query`. -pub fn get_beacon_block_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - let time_taken = timer.elapsed(); - debug!("Beacon block requested: {slot_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects the row from the `beacon_blocks` table where `parent_root` equals the given `parent`. -/// This fetches the next block in the database. -/// -/// Will return `Ok(None)` if there are no matching blocks (e.g. the tip of the chain). -pub fn get_beacon_block_with_parent( - conn: &mut PgConn, - parent: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .filter(parent_root.eq(parent)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Next beacon block requested: {parent}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Select all rows of the `beacon_blocks` table where `slot >= `start_slot && slot <= -/// `end_slot`. -pub fn get_beacon_blocks_by_range( - conn: &mut PgConn, - start_slot: WatchSlot, - end_slot: WatchSlot, -) -> Result>, Error> { - use self::beacon_blocks::dsl::*; - let timer = Instant::now(); - - let result = beacon_blocks - .filter(slot.ge(start_slot)) - .filter(slot.le(end_slot)) - .load::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Beacon blocks by range requested, start_slot: {start_slot}, end_slot: {end_slot}, time_taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `proposer_info` table corresponding to a given `root_query`. -pub fn get_proposer_info_by_root( - conn: &mut PgConn, - root_query: WatchHash, -) -> Result, Error> { - use self::beacon_blocks::dsl::{beacon_blocks, root}; - use self::proposer_info::dsl::*; - let timer = Instant::now(); - - let join = beacon_blocks.inner_join(proposer_info); - - let result = join - .select((slot, proposer_index, graffiti)) - .filter(root.eq(root_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Proposer info requested for block: {root_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row of the `proposer_info` table corresponding to a given `slot_query`. -pub fn get_proposer_info_by_slot( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result, Error> { - use self::proposer_info::dsl::*; - let timer = Instant::now(); - - let result = proposer_info - .filter(slot.eq(slot_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Proposer info requested for slot: {slot_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects multiple rows of the `proposer_info` table between `start_slot` and `end_slot`. -/// Selects a single row of the `proposer_info` table corresponding to a given `slot_query`. -#[allow(dead_code)] -pub fn get_proposer_info_by_range( - conn: &mut PgConn, - start_slot: WatchSlot, - end_slot: WatchSlot, -) -> Result>, Error> { - use self::proposer_info::dsl::*; - let timer = Instant::now(); - - let result = proposer_info - .filter(slot.ge(start_slot)) - .filter(slot.le(end_slot)) - .load::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!( - "Proposer info requested for range: {start_slot} to {end_slot}, time taken: {time_taken:?}" - ); - Ok(result) -} - -pub fn get_validators_latest_proposer_info( - conn: &mut PgConn, - indices_query: Vec, -) -> Result, Error> { - use self::proposer_info::dsl::*; - - let proposers = proposer_info - .filter(proposer_index.eq_any(indices_query)) - .load::(conn)?; - - let mut result = HashMap::new(); - for proposer in proposers { - result - .entry(proposer.proposer_index) - .or_insert_with(|| proposer.clone()); - let entry = result - .get_mut(&proposer.proposer_index) - .ok_or_else(|| Error::Other("An internal error occured".to_string()))?; - if proposer.slot > entry.slot { - entry.slot = proposer.slot - } - } - - Ok(result) -} - -/// Selects the max(`slot`) and `proposer_index` of each unique index in the -/// `proposer_info` table and returns them formatted as a `HashMap`. -/// Only returns rows which have `slot <= target_slot`. -/// -/// Ideally, this would return the full row, but I have not found a way to do that without using -/// a much more expensive SQL query. -pub fn get_all_validators_latest_proposer_info_at_slot( - conn: &mut PgConn, - target_slot: WatchSlot, -) -> Result, Error> { - use self::proposer_info::dsl::*; - - let latest_proposals: Vec<(i32, Option)> = proposer_info - .group_by(proposer_index) - .select((proposer_index, max(slot))) - .filter(slot.le(target_slot)) - .load::<(i32, Option)>(conn)?; - - let mut result = HashMap::new(); - - for proposal in latest_proposals { - if let Some(latest_slot) = proposal.1 { - result.insert(latest_slot, proposal.0); - } - } - - Ok(result) -} - -/// Selects a single row from the `validators` table corresponding to a given -/// `validator_index_query`. -pub fn get_validator_by_index( - conn: &mut PgConn, - validator_index_query: i32, -) -> Result, Error> { - use self::validators::dsl::*; - let timer = Instant::now(); - - let result = validators - .filter(index.eq(validator_index_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Validator requested: {validator_index_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row from the `validators` table corresponding to a given -/// `public_key_query`. -pub fn get_validator_by_public_key( - conn: &mut PgConn, - public_key_query: WatchPK, -) -> Result, Error> { - use self::validators::dsl::*; - let timer = Instant::now(); - - let result = validators - .filter(public_key.eq(public_key_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Validator requested: {public_key_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects all rows from the `validators` table which have an `index` contained in -/// the `indices_query`. -#[allow(dead_code)] -pub fn get_validators_by_indices( - conn: &mut PgConn, - indices_query: Vec, -) -> Result, Error> { - use self::validators::dsl::*; - let timer = Instant::now(); - - let query_len = indices_query.len(); - let result = validators - .filter(index.eq_any(indices_query)) - .load::(conn)?; - - let time_taken = timer.elapsed(); - debug!("{query_len} validators requested, time taken: {time_taken:?}"); - Ok(result) -} - -// Selects all rows from the `validators` table. -pub fn get_all_validators(conn: &mut PgConn) -> Result, Error> { - use self::validators::dsl::*; - let timer = Instant::now(); - - let result = validators.load::(conn)?; - - let time_taken = timer.elapsed(); - debug!("All validators requested, time taken: {time_taken:?}"); - Ok(result) -} - -/// Counts the number of rows in the `validators` table. -#[allow(dead_code)] -pub fn count_validators(conn: &mut PgConn) -> Result { - use self::validators::dsl::*; - - validators.count().get_result(conn).map_err(Error::Database) -} - -/// Counts the number of rows in the `validators` table where -/// `activation_epoch <= target_slot.epoch()`. -pub fn count_validators_activated_before_slot( - conn: &mut PgConn, - target_slot: WatchSlot, - slots_per_epoch: u64, -) -> Result { - use self::validators::dsl::*; - - let target_epoch = target_slot.epoch(slots_per_epoch); - - validators - .count() - .filter(activation_epoch.le(target_epoch.as_u64() as i32)) - .get_result(conn) - .map_err(Error::Database) -} - -/* - * DELETE statements. - */ - -/// Deletes all rows of the `canonical_slots` table which have `slot` greater than `slot_query`. -/// -/// Due to the ON DELETE CASCADE clause present in the database migration SQL, deleting rows from -/// `canonical_slots` will delete all corresponding rows in `beacon_blocks, `block_rewards`, -/// `block_packing` and `proposer_info`. -pub fn delete_canonical_slots_above( - conn: &mut PgConn, - slot_query: WatchSlot, -) -> Result { - use self::canonical_slots::dsl::*; - - let result = diesel::delete(canonical_slots) - .filter(slot.gt(slot_query)) - .execute(conn)?; - - debug!("Deleted canonical slots above {slot_query}: {result} rows deleted"); - Ok(result) -} - -/// Deletes all rows of the `suboptimal_attestations` table which have `epoch_start_slot` greater -/// than `epoch_start_slot_query`. -pub fn delete_suboptimal_attestations_above( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result { - use self::suboptimal_attestations::dsl::*; - - let result = diesel::delete(suboptimal_attestations) - .filter(epoch_start_slot.gt(epoch_start_slot_query)) - .execute(conn)?; - - debug!("Deleted attestations above: {epoch_start_slot_query}, rows deleted: {result}"); - Ok(result) -} diff --git a/watch/src/database/models.rs b/watch/src/database/models.rs deleted file mode 100644 index f42444d661..0000000000 --- a/watch/src/database/models.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::database::{ - schema::{beacon_blocks, canonical_slots, proposer_info, validators}, - watch_types::{WatchHash, WatchPK, WatchSlot}, -}; -use diesel::{Insertable, Queryable}; -use serde::{Deserialize, Serialize}; -use std::hash::{Hash, Hasher}; - -pub type WatchEpoch = i32; - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = canonical_slots)] -pub struct WatchCanonicalSlot { - pub slot: WatchSlot, - pub root: WatchHash, - pub skipped: bool, - pub beacon_block: Option, -} - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = beacon_blocks)] -pub struct WatchBeaconBlock { - pub slot: WatchSlot, - pub root: WatchHash, - pub parent_root: WatchHash, - pub attestation_count: i32, - pub transaction_count: Option, - pub withdrawal_count: Option, -} - -#[derive(Clone, Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = validators)] -pub struct WatchValidator { - pub index: i32, - pub public_key: WatchPK, - pub status: String, - pub activation_epoch: Option, - pub exit_epoch: Option, -} - -// Implement a minimal version of `Hash` and `Eq` so that we know if a validator status has changed. -impl Hash for WatchValidator { - fn hash(&self, state: &mut H) { - self.index.hash(state); - self.status.hash(state); - self.activation_epoch.hash(state); - self.exit_epoch.hash(state); - } -} - -impl PartialEq for WatchValidator { - fn eq(&self, other: &Self) -> bool { - self.index == other.index - && self.status == other.status - && self.activation_epoch == other.activation_epoch - && self.exit_epoch == other.exit_epoch - } -} -impl Eq for WatchValidator {} - -#[derive(Clone, Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = proposer_info)] -pub struct WatchProposerInfo { - pub slot: WatchSlot, - pub proposer_index: i32, - pub graffiti: String, -} diff --git a/watch/src/database/schema.rs b/watch/src/database/schema.rs deleted file mode 100644 index 32f22d506d..0000000000 --- a/watch/src/database/schema.rs +++ /dev/null @@ -1,102 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - active_config (id) { - id -> Int4, - config_name -> Text, - slots_per_epoch -> Int4, - } -} - -diesel::table! { - beacon_blocks (slot) { - slot -> Int4, - root -> Bytea, - parent_root -> Bytea, - attestation_count -> Int4, - transaction_count -> Nullable, - withdrawal_count -> Nullable, - } -} - -diesel::table! { - block_packing (slot) { - slot -> Int4, - available -> Int4, - included -> Int4, - prior_skip_slots -> Int4, - } -} - -diesel::table! { - block_rewards (slot) { - slot -> Int4, - total -> Int4, - attestation_reward -> Int4, - sync_committee_reward -> Int4, - } -} - -diesel::table! { - blockprint (slot) { - slot -> Int4, - best_guess -> Text, - } -} - -diesel::table! { - canonical_slots (slot) { - slot -> Int4, - root -> Bytea, - skipped -> Bool, - beacon_block -> Nullable, - } -} - -diesel::table! { - proposer_info (slot) { - slot -> Int4, - proposer_index -> Int4, - graffiti -> Text, - } -} - -diesel::table! { - suboptimal_attestations (epoch_start_slot, index) { - epoch_start_slot -> Int4, - index -> Int4, - source -> Bool, - head -> Bool, - target -> Bool, - } -} - -diesel::table! { - validators (index) { - index -> Int4, - public_key -> Bytea, - status -> Text, - activation_epoch -> Nullable, - exit_epoch -> Nullable, - } -} - -diesel::joinable!(block_packing -> beacon_blocks (slot)); -diesel::joinable!(block_rewards -> beacon_blocks (slot)); -diesel::joinable!(blockprint -> beacon_blocks (slot)); -diesel::joinable!(proposer_info -> beacon_blocks (slot)); -diesel::joinable!(proposer_info -> validators (proposer_index)); -diesel::joinable!(suboptimal_attestations -> canonical_slots (epoch_start_slot)); -diesel::joinable!(suboptimal_attestations -> validators (index)); - -diesel::allow_tables_to_appear_in_same_query!( - active_config, - beacon_blocks, - block_packing, - block_rewards, - blockprint, - canonical_slots, - proposer_info, - suboptimal_attestations, - validators, -); diff --git a/watch/src/database/utils.rs b/watch/src/database/utils.rs deleted file mode 100644 index 9134c3698f..0000000000 --- a/watch/src/database/utils.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![allow(dead_code)] -use crate::database::config::Config; -use diesel::prelude::*; -use diesel_migrations::{FileBasedMigrations, MigrationHarness}; - -/// Sets `config.dbname` to `config.default_dbname` and returns `(new_config, old_dbname)`. -/// -/// This is useful for creating or dropping databases, since these actions must be done by -/// logging into another database. -pub fn get_config_using_default_db(config: &Config) -> (Config, String) { - let mut config = config.clone(); - let new_dbname = std::mem::replace(&mut config.dbname, config.default_dbname.clone()); - (config, new_dbname) -} - -/// Runs the set of migrations as detected in the local directory. -/// Equivalent to `diesel migration run`. -/// -/// Contains `unwrap`s so is only suitable for test code. -/// TODO(mac) refactor to return Result -pub fn run_migrations(config: &Config) -> PgConnection { - let database_url = config.clone().build_database_url(); - let mut conn = PgConnection::establish(&database_url).unwrap(); - let migrations = FileBasedMigrations::find_migrations_directory().unwrap(); - conn.run_pending_migrations(migrations).unwrap(); - conn.begin_test_transaction().unwrap(); - conn -} diff --git a/watch/src/database/watch_types.rs b/watch/src/database/watch_types.rs deleted file mode 100644 index c2b67084c9..0000000000 --- a/watch/src/database/watch_types.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::database::error::Error; -use diesel::{ - sql_types::{Binary, Integer}, - AsExpression, FromSqlRow, -}; -use serde::{Deserialize, Serialize}; -use std::fmt; -use std::str::FromStr; -use types::{Epoch, Hash256, PublicKeyBytes, Slot}; -#[derive( - Clone, - Copy, - Debug, - AsExpression, - FromSqlRow, - Deserialize, - Serialize, - Hash, - PartialEq, - Eq, - PartialOrd, - Ord, -)] -#[diesel(sql_type = Integer)] -pub struct WatchSlot(Slot); - -impl fmt::Display for WatchSlot { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl WatchSlot { - pub fn new(slot: u64) -> Self { - Self(Slot::new(slot)) - } - - pub fn from_slot(slot: Slot) -> Self { - Self(slot) - } - - pub fn as_slot(self) -> Slot { - self.0 - } - - pub fn as_u64(self) -> u64 { - self.0.as_u64() - } - - pub fn epoch(self, slots_per_epoch: u64) -> Epoch { - self.as_slot().epoch(slots_per_epoch) - } -} - -#[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Deserialize, Serialize)] -#[diesel(sql_type = Binary)] -pub struct WatchHash(Hash256); - -impl fmt::Display for WatchHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -impl WatchHash { - pub fn as_hash(&self) -> Hash256 { - self.0 - } - - pub fn from_hash(hash: Hash256) -> Self { - WatchHash(hash) - } - - pub fn as_bytes(&self) -> &[u8] { - self.0.as_slice() - } - - pub fn from_bytes(src: &[u8]) -> Result { - if src.len() == 32 { - Ok(WatchHash(Hash256::from_slice(src))) - } else { - Err(Error::InvalidRoot) - } - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, AsExpression, FromSqlRow, Serialize, Deserialize)] -#[diesel(sql_type = Binary)] -pub struct WatchPK(PublicKeyBytes); - -impl fmt::Display for WatchPK { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.0) - } -} - -impl WatchPK { - pub fn as_bytes(&self) -> &[u8] { - self.0.as_serialized() - } - - pub fn from_bytes(src: &[u8]) -> Result { - Ok(WatchPK(PublicKeyBytes::deserialize(src)?)) - } - - pub fn from_pubkey(key: PublicKeyBytes) -> Self { - WatchPK(key) - } -} - -impl FromStr for WatchPK { - type Err = String; - - fn from_str(s: &str) -> Result { - Ok(WatchPK( - PublicKeyBytes::from_str(s).map_err(|e| format!("Cannot be parsed: {}", e))?, - )) - } -} diff --git a/watch/src/lib.rs b/watch/src/lib.rs deleted file mode 100644 index 664c945165..0000000000 --- a/watch/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![cfg(unix)] -pub mod block_packing; -pub mod block_rewards; -pub mod blockprint; -pub mod cli; -pub mod client; -pub mod config; -pub mod database; -pub mod logger; -pub mod server; -pub mod suboptimal_attestations; -pub mod updater; diff --git a/watch/src/logger.rs b/watch/src/logger.rs deleted file mode 100644 index 49310b42aa..0000000000 --- a/watch/src/logger.rs +++ /dev/null @@ -1,24 +0,0 @@ -use env_logger::Builder; -use log::{info, LevelFilter}; -use std::process; - -pub fn init_logger(log_level: &str) { - let log_level = match log_level.to_lowercase().as_str() { - "trace" => LevelFilter::Trace, - "debug" => LevelFilter::Debug, - "info" => LevelFilter::Info, - "warn" => LevelFilter::Warn, - "error" => LevelFilter::Error, - _ => { - eprintln!("Unsupported log level"); - process::exit(1) - } - }; - - let mut builder = Builder::new(); - builder.filter(Some("watch"), log_level); - - builder.init(); - - info!("Logger initialized with log-level: {log_level}"); -} diff --git a/watch/src/main.rs b/watch/src/main.rs deleted file mode 100644 index f971747da4..0000000000 --- a/watch/src/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -#[cfg(unix)] -use std::process; - -#[cfg(unix)] -mod block_packing; -#[cfg(unix)] -mod block_rewards; -#[cfg(unix)] -mod blockprint; -#[cfg(unix)] -mod cli; -#[cfg(unix)] -mod config; -#[cfg(unix)] -mod database; -#[cfg(unix)] -mod logger; -#[cfg(unix)] -mod server; -#[cfg(unix)] -mod suboptimal_attestations; -#[cfg(unix)] -mod updater; - -#[cfg(unix)] -#[tokio::main] -async fn main() { - match cli::run().await { - Ok(()) => process::exit(0), - Err(e) => { - eprintln!("Command failed with: {}", e); - drop(e); - process::exit(1) - } - } -} - -#[cfg(windows)] -fn main() { - eprintln!("Windows is not supported. Exiting."); -} diff --git a/watch/src/server/config.rs b/watch/src/server/config.rs deleted file mode 100644 index a7d38e706f..0000000000 --- a/watch/src/server/config.rs +++ /dev/null @@ -1,28 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::net::IpAddr; - -pub const LISTEN_ADDR: &str = "127.0.0.1"; - -pub const fn listen_port() -> u16 { - 5059 -} -fn listen_addr() -> IpAddr { - LISTEN_ADDR.parse().expect("Server address is not valid") -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(default = "listen_addr")] - pub listen_addr: IpAddr, - #[serde(default = "listen_port")] - pub listen_port: u16, -} - -impl Default for Config { - fn default() -> Self { - Self { - listen_addr: listen_addr(), - listen_port: listen_port(), - } - } -} diff --git a/watch/src/server/error.rs b/watch/src/server/error.rs deleted file mode 100644 index e2c8f0f42a..0000000000 --- a/watch/src/server/error.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::database::Error as DbError; -use axum::Error as AxumError; -use axum::{http::StatusCode, response::IntoResponse, Json}; -use hyper::Error as HyperError; -use serde_json::json; -use std::io::Error as IoError; - -#[derive(Debug)] -#[allow(dead_code)] -pub enum Error { - Axum(AxumError), - Hyper(HyperError), - Database(DbError), - IoError(IoError), - BadRequest, - NotFound, - Other(String), -} - -impl IntoResponse for Error { - fn into_response(self) -> axum::response::Response { - let (status, error_message) = match self { - Self::BadRequest => (StatusCode::BAD_REQUEST, "Bad Request"), - Self::NotFound => (StatusCode::NOT_FOUND, "Not Found"), - _ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"), - }; - (status, Json(json!({ "error": error_message }))).into_response() - } -} - -impl From for Error { - fn from(e: HyperError) -> Self { - Error::Hyper(e) - } -} - -impl From for Error { - fn from(e: AxumError) -> Self { - Error::Axum(e) - } -} - -impl From for Error { - fn from(e: DbError) -> Self { - Error::Database(e) - } -} - -impl From for Error { - fn from(e: IoError) -> Self { - Error::IoError(e) - } -} - -impl From for Error { - fn from(e: String) -> Self { - Error::Other(e) - } -} diff --git a/watch/src/server/handler.rs b/watch/src/server/handler.rs deleted file mode 100644 index 6777026867..0000000000 --- a/watch/src/server/handler.rs +++ /dev/null @@ -1,266 +0,0 @@ -use crate::database::{ - self, Error as DbError, PgPool, WatchBeaconBlock, WatchCanonicalSlot, WatchHash, WatchPK, - WatchProposerInfo, WatchSlot, WatchValidator, -}; -use crate::server::Error; -use axum::{ - extract::{Path, Query}, - Extension, Json, -}; -use eth2::types::BlockId; -use std::collections::HashMap; -use std::str::FromStr; - -pub async fn get_slot( - Path(slot): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_canonical_slot( - &mut conn, - WatchSlot::new(slot), - )?)) -} - -pub async fn get_slot_lowest( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_lowest_canonical_slot(&mut conn)?)) -} - -pub async fn get_slot_highest( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_highest_canonical_slot(&mut conn)?)) -} - -pub async fn get_slots_by_range( - Query(query): Query>, - Extension(pool): Extension, -) -> Result>>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - if let Some(start_slot) = query.get("start_slot") { - if let Some(end_slot) = query.get("end_slot") { - if start_slot > end_slot { - Err(Error::BadRequest) - } else { - Ok(Json(database::get_canonical_slots_by_range( - &mut conn, - WatchSlot::new(*start_slot), - WatchSlot::new(*end_slot), - )?)) - } - } else { - Err(Error::BadRequest) - } - } else { - Err(Error::BadRequest) - } -} - -pub async fn get_block( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - let block_id: BlockId = BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)?; - match block_id { - BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - BlockId::Root(root) => Ok(Json(database::get_beacon_block_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_block_lowest( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_lowest_beacon_block(&mut conn)?)) -} - -pub async fn get_block_highest( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_highest_beacon_block(&mut conn)?)) -} - -pub async fn get_block_previous( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => { - if let Some(block) = - database::get_beacon_block_by_root(&mut conn, WatchHash::from_hash(root))? - .map(|block| block.parent_root) - { - Ok(Json(database::get_beacon_block_by_root(&mut conn, block)?)) - } else { - Err(Error::NotFound) - } - } - BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot( - &mut conn, - WatchSlot::new(slot.as_u64().checked_sub(1_u64).ok_or(Error::NotFound)?), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_block_next( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(database::get_beacon_block_with_parent( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot( - &mut conn, - WatchSlot::from_slot(slot + 1_u64), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_blocks_by_range( - Query(query): Query>, - Extension(pool): Extension, -) -> Result>>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - if let Some(start_slot) = query.get("start_slot") { - if let Some(end_slot) = query.get("end_slot") { - if start_slot > end_slot { - Err(Error::BadRequest) - } else { - Ok(Json(database::get_beacon_blocks_by_range( - &mut conn, - WatchSlot::new(*start_slot), - WatchSlot::new(*end_slot), - )?)) - } - } else { - Err(Error::BadRequest) - } - } else { - Err(Error::BadRequest) - } -} - -pub async fn get_block_proposer( - Path(block_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? { - BlockId::Root(root) => Ok(Json(database::get_proposer_info_by_root( - &mut conn, - WatchHash::from_hash(root), - )?)), - BlockId::Slot(slot) => Ok(Json(database::get_proposer_info_by_slot( - &mut conn, - WatchSlot::from_slot(slot), - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_validator( - Path(validator_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - if validator_query.starts_with("0x") { - let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - Ok(Json(database::get_validator_by_public_key( - &mut conn, pubkey, - )?)) - } else { - let index = i32::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - Ok(Json(database::get_validator_by_index(&mut conn, index)?)) - } -} - -pub async fn get_all_validators( - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - Ok(Json(database::get_all_validators(&mut conn)?)) -} - -pub async fn get_validator_latest_proposal( - Path(validator_query): Path, - Extension(pool): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - if validator_query.starts_with("0x") { - let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - let validator = - database::get_validator_by_public_key(&mut conn, pubkey)?.ok_or(Error::NotFound)?; - Ok(Json(database::get_validators_latest_proposer_info( - &mut conn, - vec![validator.index], - )?)) - } else { - let index = i32::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - Ok(Json(database::get_validators_latest_proposer_info( - &mut conn, - vec![index], - )?)) - } -} - -pub async fn get_client_breakdown( - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - - if let Some(target_slot) = database::get_highest_canonical_slot(&mut conn)? { - Ok(Json(database::get_validators_clients_at_slot( - &mut conn, - target_slot.slot, - slots_per_epoch, - )?)) - } else { - Err(Error::Database(DbError::Other( - "No slots found in database.".to_string(), - ))) - } -} - -pub async fn get_client_breakdown_percentages( - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = database::get_connection(&pool).map_err(Error::Database)?; - - let mut result = HashMap::new(); - if let Some(target_slot) = database::get_highest_canonical_slot(&mut conn)? { - let total = database::count_validators_activated_before_slot( - &mut conn, - target_slot.slot, - slots_per_epoch, - )?; - let clients = - database::get_validators_clients_at_slot(&mut conn, target_slot.slot, slots_per_epoch)?; - for (client, number) in clients.iter() { - let percentage: f64 = *number as f64 / total as f64 * 100.0; - result.insert(client.to_string(), percentage); - } - } - - Ok(Json(result)) -} diff --git a/watch/src/server/mod.rs b/watch/src/server/mod.rs deleted file mode 100644 index 08036db951..0000000000 --- a/watch/src/server/mod.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::block_packing::block_packing_routes; -use crate::block_rewards::block_rewards_routes; -use crate::blockprint::blockprint_routes; -use crate::config::Config as FullConfig; -use crate::database::{self, PgPool}; -use crate::suboptimal_attestations::{attestation_routes, blockprint_attestation_routes}; -use axum::{ - http::{StatusCode, Uri}, - routing::get, - Extension, Json, Router, -}; -use eth2::types::ErrorMessage; -use log::info; -use std::future::{Future, IntoFuture}; -use std::net::{SocketAddr, TcpListener}; - -pub use config::Config; -pub use error::Error; - -mod config; -mod error; -mod handler; - -pub async fn serve(config: FullConfig) -> Result<(), Error> { - let db = database::build_connection_pool(&config.database)?; - let (_, slots_per_epoch) = database::get_active_config(&mut database::get_connection(&db)?)? - .ok_or_else(|| { - Error::Other( - "Database not found. Please run the updater prior to starting the server" - .to_string(), - ) - })?; - - let (_addr, server) = start_server(&config, slots_per_epoch as u64, db)?; - - server.await?; - - Ok(()) -} - -/// Creates a server that will serve requests using information from `config`. -/// -/// The server will create its own connection pool to serve connections to the database. -/// This is separate to the connection pool that is used for the `updater`. -/// -/// The server will shut down gracefully when the `shutdown` future resolves. -/// -/// ## Returns -/// -/// This function will bind the server to the address specified in the config and then return a -/// Future representing the actual server that will need to be awaited. -/// -/// ## Errors -/// -/// Returns an error if the server is unable to bind or there is another error during -/// configuration. -pub fn start_server( - config: &FullConfig, - slots_per_epoch: u64, - pool: PgPool, -) -> Result< - ( - SocketAddr, - impl Future> + 'static, - ), - Error, -> { - let mut routes = Router::new() - .route("/v1/slots", get(handler::get_slots_by_range)) - .route("/v1/slots/:slot", get(handler::get_slot)) - .route("/v1/slots/lowest", get(handler::get_slot_lowest)) - .route("/v1/slots/highest", get(handler::get_slot_highest)) - .route("/v1/slots/:slot/block", get(handler::get_block)) - .route("/v1/blocks", get(handler::get_blocks_by_range)) - .route("/v1/blocks/:block", get(handler::get_block)) - .route("/v1/blocks/lowest", get(handler::get_block_lowest)) - .route("/v1/blocks/highest", get(handler::get_block_highest)) - .route( - "/v1/blocks/:block/previous", - get(handler::get_block_previous), - ) - .route("/v1/blocks/:block/next", get(handler::get_block_next)) - .route( - "/v1/blocks/:block/proposer", - get(handler::get_block_proposer), - ) - .route("/v1/validators/:validator", get(handler::get_validator)) - .route("/v1/validators/all", get(handler::get_all_validators)) - .route( - "/v1/validators/:validator/latest_proposal", - get(handler::get_validator_latest_proposal), - ) - .route("/v1/clients", get(handler::get_client_breakdown)) - .route( - "/v1/clients/percentages", - get(handler::get_client_breakdown_percentages), - ) - .merge(attestation_routes()) - .merge(blockprint_routes()) - .merge(block_packing_routes()) - .merge(block_rewards_routes()); - - if config.blockprint.enabled && config.updater.attestations { - routes = routes.merge(blockprint_attestation_routes()) - } - - let app = routes - .fallback(route_not_found) - .layer(Extension(pool)) - .layer(Extension(slots_per_epoch)); - - let addr = SocketAddr::new(config.server.listen_addr, config.server.listen_port); - let listener = TcpListener::bind(addr)?; - listener.set_nonblocking(true)?; - - // Read the socket address (it may be different from `addr` if listening on port 0). - let socket_addr = listener.local_addr()?; - - let serve = axum::serve(tokio::net::TcpListener::from_std(listener)?, app); - - info!("HTTP server listening on {}", addr); - - Ok((socket_addr, serve.into_future())) -} - -// The default route indicating that no available routes matched the request. -async fn route_not_found(uri: Uri) -> (StatusCode, Json) { - ( - StatusCode::METHOD_NOT_ALLOWED, - Json(ErrorMessage { - code: StatusCode::METHOD_NOT_ALLOWED.as_u16(), - message: format!("No route for {uri}"), - stacktraces: vec![], - }), - ) -} diff --git a/watch/src/suboptimal_attestations/database.rs b/watch/src/suboptimal_attestations/database.rs deleted file mode 100644 index cb947d250a..0000000000 --- a/watch/src/suboptimal_attestations/database.rs +++ /dev/null @@ -1,224 +0,0 @@ -use crate::database::{ - schema::{suboptimal_attestations, validators}, - watch_types::{WatchPK, WatchSlot}, - Error, PgConn, MAX_SIZE_BATCH_INSERT, -}; - -use diesel::prelude::*; -use diesel::{Insertable, Queryable}; -use log::debug; -use serde::{Deserialize, Serialize}; -use std::time::Instant; - -use types::Epoch; - -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -pub struct WatchAttestation { - pub index: i32, - pub epoch: Epoch, - pub source: bool, - pub head: bool, - pub target: bool, -} - -impl WatchAttestation { - pub fn optimal(index: i32, epoch: Epoch) -> WatchAttestation { - WatchAttestation { - index, - epoch, - source: true, - head: true, - target: true, - } - } -} - -#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)] -#[diesel(table_name = suboptimal_attestations)] -pub struct WatchSuboptimalAttestation { - pub epoch_start_slot: WatchSlot, - pub index: i32, - pub source: bool, - pub head: bool, - pub target: bool, -} - -impl WatchSuboptimalAttestation { - pub fn to_attestation(&self, slots_per_epoch: u64) -> WatchAttestation { - WatchAttestation { - index: self.index, - epoch: self.epoch_start_slot.epoch(slots_per_epoch), - source: self.source, - head: self.head, - target: self.target, - } - } -} - -/// Insert a batch of values into the `suboptimal_attestations` table -/// -/// Since attestations technically occur per-slot but we only store them per-epoch (via its -/// `start_slot`) so if any slot in the epoch changes, we need to resync the whole epoch as a -/// 'suboptimal' attestation could now be 'optimal'. -/// -/// This is handled in the update code, where in the case of a re-org, the affected epoch is -/// deleted completely. -/// -/// On a conflict, it will do nothing. -pub fn insert_batch_suboptimal_attestations( - conn: &mut PgConn, - attestations: Vec, -) -> Result<(), Error> { - use self::suboptimal_attestations::dsl::*; - - let mut count = 0; - let timer = Instant::now(); - - for chunk in attestations.chunks(MAX_SIZE_BATCH_INSERT) { - count += diesel::insert_into(suboptimal_attestations) - .values(chunk) - .on_conflict_do_nothing() - .execute(conn)?; - } - - let time_taken = timer.elapsed(); - debug!("Attestations inserted, count: {count}, time taken: {time_taken:?}"); - Ok(()) -} - -/// Selects the row from the `suboptimal_attestations` table where `epoch_start_slot` is minimum. -pub fn get_lowest_attestation( - conn: &mut PgConn, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .order_by(epoch_start_slot.asc()) - .limit(1) - .first::(conn) - .optional()?) -} - -/// Selects the row from the `suboptimal_attestations` table where `epoch_start_slot` is maximum. -pub fn get_highest_attestation( - conn: &mut PgConn, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .order_by(epoch_start_slot.desc()) - .limit(1) - .first::(conn) - .optional()?) -} - -/// Selects a single row from the `suboptimal_attestations` table corresponding to a given -/// `index_query` and `epoch_query`. -pub fn get_attestation_by_index( - conn: &mut PgConn, - index_query: i32, - epoch_query: Epoch, - slots_per_epoch: u64, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - let timer = Instant::now(); - - let result = suboptimal_attestations - .filter(epoch_start_slot.eq(WatchSlot::from_slot( - epoch_query.start_slot(slots_per_epoch), - ))) - .filter(index.eq(index_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Attestation requested for validator: {index_query}, epoch: {epoch_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects a single row from the `suboptimal_attestations` table corresponding -/// to a given `pubkey_query` and `epoch_query`. -#[allow(dead_code)] -pub fn get_attestation_by_pubkey( - conn: &mut PgConn, - pubkey_query: WatchPK, - epoch_query: Epoch, - slots_per_epoch: u64, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - use self::validators::dsl::{public_key, validators}; - let timer = Instant::now(); - - let join = validators.inner_join(suboptimal_attestations); - - let result = join - .select((epoch_start_slot, index, source, head, target)) - .filter(epoch_start_slot.eq(WatchSlot::from_slot( - epoch_query.start_slot(slots_per_epoch), - ))) - .filter(public_key.eq(pubkey_query)) - .first::(conn) - .optional()?; - - let time_taken = timer.elapsed(); - debug!("Attestation requested for validator: {pubkey_query}, epoch: {epoch_query}, time taken: {time_taken:?}"); - Ok(result) -} - -/// Selects `index` for all validators in the suboptimal_attestations table -/// that have `source == false` for the corresponding `epoch_start_slot_query`. -pub fn get_validators_missed_source( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .select(index) - .filter(epoch_start_slot.eq(epoch_start_slot_query)) - .filter(source.eq(false)) - .load::(conn)?) -} - -/// Selects `index` for all validators in the suboptimal_attestations table -/// that have `head == false` for the corresponding `epoch_start_slot_query`. -pub fn get_validators_missed_head( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .select(index) - .filter(epoch_start_slot.eq(epoch_start_slot_query)) - .filter(head.eq(false)) - .load::(conn)?) -} - -/// Selects `index` for all validators in the suboptimal_attestations table -/// that have `target == false` for the corresponding `epoch_start_slot_query`. -pub fn get_validators_missed_target( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .select(index) - .filter(epoch_start_slot.eq(epoch_start_slot_query)) - .filter(target.eq(false)) - .load::(conn)?) -} - -/// Selects all rows from the `suboptimal_attestations` table for the given -/// `epoch_start_slot_query`. -pub fn get_all_suboptimal_attestations_for_epoch( - conn: &mut PgConn, - epoch_start_slot_query: WatchSlot, -) -> Result, Error> { - use self::suboptimal_attestations::dsl::*; - - Ok(suboptimal_attestations - .filter(epoch_start_slot.eq(epoch_start_slot_query)) - .load::(conn)?) -} diff --git a/watch/src/suboptimal_attestations/mod.rs b/watch/src/suboptimal_attestations/mod.rs deleted file mode 100644 index a94532e8ab..0000000000 --- a/watch/src/suboptimal_attestations/mod.rs +++ /dev/null @@ -1,56 +0,0 @@ -pub mod database; -pub mod server; -pub mod updater; - -use crate::database::watch_types::WatchSlot; -use crate::updater::error::Error; - -pub use database::{ - get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, get_attestation_by_pubkey, - get_highest_attestation, get_lowest_attestation, insert_batch_suboptimal_attestations, - WatchAttestation, WatchSuboptimalAttestation, -}; - -pub use server::{attestation_routes, blockprint_attestation_routes}; - -use eth2::BeaconNodeHttpClient; -use types::Epoch; - -/// Sends a request to `lighthouse/analysis/attestation_performance`. -/// Formats the response into a vector of `WatchSuboptimalAttestation`. -/// -/// Any attestations with `source == true && head == true && target == true` are ignored. -pub async fn get_attestation_performances( - bn: &BeaconNodeHttpClient, - start_epoch: Epoch, - end_epoch: Epoch, - slots_per_epoch: u64, -) -> Result, Error> { - let mut output = Vec::new(); - let result = bn - .get_lighthouse_analysis_attestation_performance( - start_epoch, - end_epoch, - "global".to_string(), - ) - .await?; - for index in result { - for epoch in index.epochs { - if epoch.1.active { - // Check if the attestation is suboptimal. - if !epoch.1.source || !epoch.1.head || !epoch.1.target { - output.push(WatchSuboptimalAttestation { - epoch_start_slot: WatchSlot::from_slot( - Epoch::new(epoch.0).start_slot(slots_per_epoch), - ), - index: index.index as i32, - source: epoch.1.source, - head: epoch.1.head, - target: epoch.1.target, - }) - } - } - } - } - Ok(output) -} diff --git a/watch/src/suboptimal_attestations/server.rs b/watch/src/suboptimal_attestations/server.rs deleted file mode 100644 index 391db9a41b..0000000000 --- a/watch/src/suboptimal_attestations/server.rs +++ /dev/null @@ -1,299 +0,0 @@ -use crate::database::{ - get_canonical_slot, get_connection, get_validator_by_index, get_validator_by_public_key, - get_validators_clients_at_slot, get_validators_latest_proposer_info, PgPool, WatchPK, - WatchSlot, -}; - -use crate::blockprint::database::construct_validator_blockprints_at_slot; -use crate::server::Error; -use crate::suboptimal_attestations::database::{ - get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, - get_validators_missed_head, get_validators_missed_source, get_validators_missed_target, - WatchAttestation, WatchSuboptimalAttestation, -}; - -use axum::{extract::Path, routing::get, Extension, Json, Router}; -use std::collections::{HashMap, HashSet}; -use std::str::FromStr; -use types::Epoch; - -// Will return Ok(None) if the epoch is not synced or if the validator does not exist. -// In the future it might be worth differentiating these events. -pub async fn get_validator_attestation( - Path((validator_query, epoch_query)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - let epoch = Epoch::new(epoch_query); - - // Ensure the database has synced the target epoch. - if get_canonical_slot( - &mut conn, - WatchSlot::from_slot(epoch.end_slot(slots_per_epoch)), - )? - .is_none() - { - // Epoch is not fully synced. - return Ok(Json(None)); - } - - let index = if validator_query.starts_with("0x") { - let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?; - get_validator_by_public_key(&mut conn, pubkey)? - .ok_or(Error::NotFound)? - .index - } else { - i32::from_str(&validator_query).map_err(|_| Error::BadRequest)? - }; - let attestation = if let Some(suboptimal_attestation) = - get_attestation_by_index(&mut conn, index, epoch, slots_per_epoch)? - { - Some(suboptimal_attestation.to_attestation(slots_per_epoch)) - } else { - // Attestation was not in database. Check if the validator was active. - match get_validator_by_index(&mut conn, index)? { - Some(validator) => { - if let Some(activation_epoch) = validator.activation_epoch { - if activation_epoch <= epoch.as_u64() as i32 { - if let Some(exit_epoch) = validator.exit_epoch { - if exit_epoch > epoch.as_u64() as i32 { - // Validator is active and has not yet exited. - Some(WatchAttestation::optimal(index, epoch)) - } else { - // Validator has exited. - None - } - } else { - // Validator is active and has not yet exited. - Some(WatchAttestation::optimal(index, epoch)) - } - } else { - // Validator is not yet active. - None - } - } else { - // Validator is not yet active. - None - } - } - None => return Err(Error::Other("Validator index does not exist".to_string())), - } - }; - Ok(Json(attestation)) -} - -pub async fn get_all_validators_attestations( - Path(epoch): Path, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - - let epoch_start_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch)); - - Ok(Json(get_all_suboptimal_attestations_for_epoch( - &mut conn, - epoch_start_slot, - )?)) -} - -pub async fn get_validators_missed_vote( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - - let epoch_start_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch)); - match vote.to_lowercase().as_str() { - "source" => Ok(Json(get_validators_missed_source( - &mut conn, - epoch_start_slot, - )?)), - "head" => Ok(Json(get_validators_missed_head( - &mut conn, - epoch_start_slot, - )?)), - "target" => Ok(Json(get_validators_missed_target( - &mut conn, - epoch_start_slot, - )?)), - _ => Err(Error::BadRequest), - } -} - -pub async fn get_validators_missed_vote_graffiti( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - - let Json(indices) = get_validators_missed_vote( - Path((vote, epoch)), - Extension(pool), - Extension(slots_per_epoch), - ) - .await?; - - let graffitis = get_validators_latest_proposer_info(&mut conn, indices)? - .values() - .map(|info| info.graffiti.clone()) - .collect::>(); - - let mut result = HashMap::new(); - for graffiti in graffitis { - if !result.contains_key(&graffiti) { - result.insert(graffiti.clone(), 0); - } - *result - .get_mut(&graffiti) - .ok_or_else(|| Error::Other("An unexpected error occurred".to_string()))? += 1; - } - - Ok(Json(result)) -} - -pub fn attestation_routes() -> Router { - Router::new() - .route( - "/v1/validators/:validator/attestation/:epoch", - get(get_validator_attestation), - ) - .route( - "/v1/validators/all/attestation/:epoch", - get(get_all_validators_attestations), - ) - .route( - "/v1/validators/missed/:vote/:epoch", - get(get_validators_missed_vote), - ) - .route( - "/v1/validators/missed/:vote/:epoch/graffiti", - get(get_validators_missed_vote_graffiti), - ) -} - -/// The functions below are dependent on Blockprint and if it is disabled, the endpoints will be -/// disabled. -pub async fn get_clients_missed_vote( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let mut conn = get_connection(&pool).map_err(Error::Database)?; - - let Json(indices) = get_validators_missed_vote( - Path((vote, epoch)), - Extension(pool), - Extension(slots_per_epoch), - ) - .await?; - - // All validators which missed the vote. - let indices_map = indices.into_iter().collect::>(); - - let target_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch)); - - // All validators. - let client_map = - construct_validator_blockprints_at_slot(&mut conn, target_slot, slots_per_epoch)?; - - let mut result = HashMap::new(); - - for index in indices_map { - if let Some(print) = client_map.get(&index) { - if !result.contains_key(print) { - result.insert(print.clone(), 0); - } - *result - .get_mut(print) - .ok_or_else(|| Error::Other("An unexpected error occurred".to_string()))? += 1; - } - } - - Ok(Json(result)) -} - -pub async fn get_clients_missed_vote_percentages( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let Json(clients_counts) = get_clients_missed_vote( - Path((vote, epoch)), - Extension(pool.clone()), - Extension(slots_per_epoch), - ) - .await?; - - let target_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch)); - - let mut conn = get_connection(&pool)?; - let totals = get_validators_clients_at_slot(&mut conn, target_slot, slots_per_epoch)?; - - let mut result = HashMap::new(); - for (client, count) in clients_counts.iter() { - let client_total: f64 = *totals - .get(client) - .ok_or_else(|| Error::Other("Client type mismatch".to_string()))? - as f64; - // `client_total` should never be `0`, but if it is, return `0` instead of `inf`. - if client_total == 0.0 { - result.insert(client.to_string(), 0.0); - } else { - let percentage: f64 = *count as f64 / client_total * 100.0; - result.insert(client.to_string(), percentage); - } - } - - Ok(Json(result)) -} - -pub async fn get_clients_missed_vote_percentages_relative( - Path((vote, epoch)): Path<(String, u64)>, - Extension(pool): Extension, - Extension(slots_per_epoch): Extension, -) -> Result>, Error> { - let Json(clients_counts) = get_clients_missed_vote( - Path((vote, epoch)), - Extension(pool), - Extension(slots_per_epoch), - ) - .await?; - - let mut total: u64 = 0; - for (_, count) in clients_counts.iter() { - total += *count - } - - let mut result = HashMap::new(); - for (client, count) in clients_counts.iter() { - // `total` should never be 0, but if it is, return `-` instead of `inf`. - if total == 0 { - result.insert(client.to_string(), 0.0); - } else { - let percentage: f64 = *count as f64 / total as f64 * 100.0; - result.insert(client.to_string(), percentage); - } - } - - Ok(Json(result)) -} - -pub fn blockprint_attestation_routes() -> Router { - Router::new() - .route( - "/v1/clients/missed/:vote/:epoch", - get(get_clients_missed_vote), - ) - .route( - "/v1/clients/missed/:vote/:epoch/percentages", - get(get_clients_missed_vote_percentages), - ) - .route( - "/v1/clients/missed/:vote/:epoch/percentages/relative", - get(get_clients_missed_vote_percentages_relative), - ) -} diff --git a/watch/src/suboptimal_attestations/updater.rs b/watch/src/suboptimal_attestations/updater.rs deleted file mode 100644 index d8f6ec57d5..0000000000 --- a/watch/src/suboptimal_attestations/updater.rs +++ /dev/null @@ -1,236 +0,0 @@ -use crate::database::{self, Error as DbError}; -use crate::updater::{Error, UpdateHandler}; - -use crate::suboptimal_attestations::get_attestation_performances; - -use eth2::types::EthSpec; -use log::{debug, error, warn}; - -const MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS: u64 = 50; - -impl UpdateHandler { - /// Forward fills the `suboptimal_attestations` table starting from the entry with the highest - /// slot. - /// - /// It construts a request to the `attestation_performance` API endpoint with: - /// `start_epoch` -> highest completely filled epoch + 1 (or epoch of lowest canonical slot) - /// `end_epoch` -> epoch of highest canonical slot - /// - /// It will resync the latest epoch if it is not fully filled but will not overwrite existing - /// values unless there is a re-org. - /// That is, `if highest_filled_slot % slots_per_epoch != 31`. - /// - /// In the event the most recent epoch has no suboptimal attestations, it will attempt to - /// resync that epoch. The odds of this occuring on mainnet are vanishingly small so it is not - /// accounted for. - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`. - pub async fn fill_suboptimal_attestations(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - let highest_filled_slot_opt = if self.config.attestations { - database::get_highest_attestation(&mut conn)? - .map(|attestation| attestation.epoch_start_slot.as_slot()) - } else { - return Err(Error::NotEnabled("attestations".to_string())); - }; - - let start_epoch = if let Some(highest_filled_slot) = highest_filled_slot_opt { - if highest_filled_slot % self.slots_per_epoch == self.slots_per_epoch.saturating_sub(1) - { - // The whole epoch is filled so we can begin syncing the next one. - highest_filled_slot.epoch(self.slots_per_epoch) + 1 - } else { - // The epoch is only partially synced. Try to sync it fully. - highest_filled_slot.epoch(self.slots_per_epoch) - } - } else { - // No rows present in the `suboptimal_attestations` table. Use `canonical_slots` - // instead. - if let Some(lowest_canonical_slot) = database::get_lowest_canonical_slot(&mut conn)? { - lowest_canonical_slot - .slot - .as_slot() - .epoch(self.slots_per_epoch) - } else { - // There are no slots in the database, do not fill the `suboptimal_attestations` - // table. - warn!("Refusing to fill the `suboptimal_attestations` table as there are no slots in the database"); - return Ok(()); - } - }; - - if let Some(highest_canonical_slot) = - database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - let mut end_epoch = highest_canonical_slot.epoch(self.slots_per_epoch); - - // The `lighthouse/analysis/attestation_performance` endpoint can only retrieve attestations - // which are more than 1 epoch old. - // We assume that `highest_canonical_slot` is near the head of the chain. - end_epoch = end_epoch.saturating_sub(2_u64); - - // If end_epoch == 0 then the chain just started so we need to wait until - // `current_epoch >= 2`. - if end_epoch == 0 { - debug!("Chain just begun, refusing to sync attestations"); - return Ok(()); - } - - if start_epoch > end_epoch { - debug!("Attestations are up to date with the head of the database"); - return Ok(()); - } - - // Ensure the size of the request does not exceed the maximum allowed value. - if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS) { - end_epoch = start_epoch + MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS - } - - if let Some(lowest_canonical_slot) = - database::get_lowest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - let mut attestations = get_attestation_performances( - &self.bn, - start_epoch, - end_epoch, - self.slots_per_epoch, - ) - .await?; - - // Only insert attestations with corresponding `canonical_slot`s. - attestations.retain(|attestation| { - attestation.epoch_start_slot.as_slot() >= lowest_canonical_slot - && attestation.epoch_start_slot.as_slot() <= highest_canonical_slot - }); - database::insert_batch_suboptimal_attestations(&mut conn, attestations)?; - } else { - return Err(Error::Database(DbError::Other( - "Database did not return a lowest canonical slot when one exists".to_string(), - ))); - } - } else { - // There are no slots in the `canonical_slots` table, but there are entries in the - // `suboptimal_attestations` table. This is a critical failure. It usually means - // someone has manually tampered with the database tables and should not occur during - // normal operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } - - /// Backfill the `suboptimal_attestations` table starting from the entry with the lowest slot. - /// - /// It constructs a request to the `attestation_performance` API endpoint with: - /// `start_epoch` -> epoch of the lowest `canonical_slot`. - /// `end_epoch` -> epoch of the lowest filled `suboptimal_attestation` - 1 (or epoch of highest - /// canonical slot) - /// - /// It will resync the lowest epoch if it is not fully filled. - /// That is, `if lowest_filled_slot % slots_per_epoch != 0` - /// - /// In the event there are no suboptimal attestations present in the lowest epoch, it will attempt to - /// resync the epoch. The odds of this occuring on mainnet are vanishingly small so it is not - /// accounted for. - /// - /// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`. - pub async fn backfill_suboptimal_attestations(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let max_attestation_backfill = self.config.max_backfill_size_epochs; - - // Get the slot of the lowest entry in the `suboptimal_attestations` table. - let lowest_filled_slot_opt = if self.config.attestations { - database::get_lowest_attestation(&mut conn)? - .map(|attestation| attestation.epoch_start_slot.as_slot()) - } else { - return Err(Error::NotEnabled("attestations".to_string())); - }; - - let end_epoch = if let Some(lowest_filled_slot) = lowest_filled_slot_opt { - if lowest_filled_slot % self.slots_per_epoch == 0 { - lowest_filled_slot - .epoch(self.slots_per_epoch) - .saturating_sub(1_u64) - } else { - // The epoch is only partially synced. Try to sync it fully. - lowest_filled_slot.epoch(self.slots_per_epoch) - } - } else { - // No entries in the `suboptimal_attestations` table. Use `canonical_slots` instead. - if let Some(highest_canonical_slot) = - database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - // Subtract 2 since `end_epoch` must be less than the current epoch - 1. - // We assume that `highest_canonical_slot` is near the head of the chain. - highest_canonical_slot - .epoch(self.slots_per_epoch) - .saturating_sub(2_u64) - } else { - // There are no slots in the database, do not backfill the - // `suboptimal_attestations` table. - warn!("Refusing to backfill attestations as there are no slots in the database"); - return Ok(()); - } - }; - - if end_epoch == 0 { - debug!("Attestations backfill is complete"); - return Ok(()); - } - - if let Some(lowest_canonical_slot) = - database::get_lowest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - let mut start_epoch = lowest_canonical_slot.epoch(self.slots_per_epoch); - - if start_epoch > end_epoch { - debug!("Attestations are up to date with the base of the database"); - return Ok(()); - } - - // Ensure the request range does not exceed `max_attestation_backfill` or - // `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`. - if start_epoch < end_epoch.saturating_sub(max_attestation_backfill) { - start_epoch = end_epoch.saturating_sub(max_attestation_backfill) - } - if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS) { - start_epoch = end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS) - } - - if let Some(highest_canonical_slot) = - database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot()) - { - let mut attestations = get_attestation_performances( - &self.bn, - start_epoch, - end_epoch, - self.slots_per_epoch, - ) - .await?; - - // Only insert `suboptimal_attestations` with corresponding `canonical_slots`. - attestations.retain(|attestation| { - attestation.epoch_start_slot.as_slot() >= lowest_canonical_slot - && attestation.epoch_start_slot.as_slot() <= highest_canonical_slot - }); - - database::insert_batch_suboptimal_attestations(&mut conn, attestations)?; - } else { - return Err(Error::Database(DbError::Other( - "Database did not return a lowest slot when one exists".to_string(), - ))); - } - } else { - // There are no slots in the `canonical_slot` table, but there are entries in the - // `suboptimal_attestations` table. This is a critical failure. It usually means - // someone has manually tampered with the database tables and should not occur during - // normal operation. - error!("Database is corrupted. Please re-sync the database"); - return Err(Error::Database(DbError::DatabaseCorrupted)); - } - - Ok(()) - } -} diff --git a/watch/src/updater/config.rs b/watch/src/updater/config.rs deleted file mode 100644 index 0179be73db..0000000000 --- a/watch/src/updater/config.rs +++ /dev/null @@ -1,65 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub const BEACON_NODE_URL: &str = "http://127.0.0.1:5052"; - -pub const fn max_backfill_size_epochs() -> u64 { - 2 -} -pub const fn backfill_stop_epoch() -> u64 { - 0 -} -pub const fn attestations() -> bool { - true -} -pub const fn proposer_info() -> bool { - true -} -pub const fn block_rewards() -> bool { - true -} -pub const fn block_packing() -> bool { - true -} - -fn beacon_node_url() -> String { - BEACON_NODE_URL.to_string() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - /// The URL of the beacon you wish to sync from. - #[serde(default = "beacon_node_url")] - pub beacon_node_url: String, - /// The maximum size each backfill iteration will allow per request (in epochs). - #[serde(default = "max_backfill_size_epochs")] - pub max_backfill_size_epochs: u64, - /// The epoch at which to never backfill past. - #[serde(default = "backfill_stop_epoch")] - pub backfill_stop_epoch: u64, - /// Whether to sync the suboptimal_attestations table. - #[serde(default = "attestations")] - pub attestations: bool, - /// Whether to sync the proposer_info table. - #[serde(default = "proposer_info")] - pub proposer_info: bool, - /// Whether to sync the block_rewards table. - #[serde(default = "block_rewards")] - pub block_rewards: bool, - /// Whether to sync the block_packing table. - #[serde(default = "block_packing")] - pub block_packing: bool, -} - -impl Default for Config { - fn default() -> Self { - Self { - beacon_node_url: beacon_node_url(), - max_backfill_size_epochs: max_backfill_size_epochs(), - backfill_stop_epoch: backfill_stop_epoch(), - attestations: attestations(), - proposer_info: proposer_info(), - block_rewards: block_rewards(), - block_packing: block_packing(), - } - } -} diff --git a/watch/src/updater/error.rs b/watch/src/updater/error.rs deleted file mode 100644 index 13c83bcf01..0000000000 --- a/watch/src/updater/error.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::blockprint::Error as BlockprintError; -use crate::database::Error as DbError; -use beacon_node::beacon_chain::BeaconChainError; -use eth2::{Error as Eth2Error, SensitiveError}; -use std::fmt; - -#[derive(Debug)] -#[allow(dead_code)] -pub enum Error { - BeaconChain(BeaconChainError), - Eth2(Eth2Error), - SensitiveUrl(SensitiveError), - Database(DbError), - Blockprint(BlockprintError), - UnableToGetRemoteHead, - BeaconNodeSyncing, - NotEnabled(String), - NoValidatorsFound, - BeaconNodeNotCompatible(String), - InvalidConfig(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From for Error { - fn from(e: BeaconChainError) -> Self { - Error::BeaconChain(e) - } -} - -impl From for Error { - fn from(e: Eth2Error) -> Self { - Error::Eth2(e) - } -} - -impl From for Error { - fn from(e: SensitiveError) -> Self { - Error::SensitiveUrl(e) - } -} - -impl From for Error { - fn from(e: DbError) -> Self { - Error::Database(e) - } -} - -impl From for Error { - fn from(e: BlockprintError) -> Self { - Error::Blockprint(e) - } -} diff --git a/watch/src/updater/handler.rs b/watch/src/updater/handler.rs deleted file mode 100644 index 8f5e3f8e4a..0000000000 --- a/watch/src/updater/handler.rs +++ /dev/null @@ -1,471 +0,0 @@ -use crate::blockprint::WatchBlockprintClient; -use crate::config::Config as FullConfig; -use crate::database::{self, PgPool, WatchCanonicalSlot, WatchHash, WatchSlot}; -use crate::updater::{Config, Error, WatchSpec}; -use beacon_node::beacon_chain::BeaconChainError; -use eth2::{ - types::{BlockId, SyncingData}, - BeaconNodeHttpClient, SensitiveUrl, -}; -use log::{debug, error, info, warn}; -use std::collections::HashSet; -use std::marker::PhantomData; -use types::{BeaconBlockHeader, EthSpec, Hash256, SignedBeaconBlock, Slot}; - -use crate::updater::{get_beacon_block, get_header, get_validators}; - -const MAX_EXPECTED_REORG_LENGTH: u64 = 32; - -/// Ensure the existing database is valid for this run. -pub async fn ensure_valid_database( - spec: &WatchSpec, - pool: &mut PgPool, -) -> Result<(), Error> { - let mut conn = database::get_connection(pool)?; - - let bn_slots_per_epoch = spec.slots_per_epoch(); - let bn_config_name = spec.network.clone(); - - if let Some((db_config_name, db_slots_per_epoch)) = database::get_active_config(&mut conn)? { - if db_config_name != bn_config_name || db_slots_per_epoch != bn_slots_per_epoch as i32 { - Err(Error::InvalidConfig( - "The config stored in the database does not match the beacon node.".to_string(), - )) - } else { - // Configs match. - Ok(()) - } - } else { - // No config exists in the DB. - database::insert_active_config(&mut conn, bn_config_name, bn_slots_per_epoch)?; - Ok(()) - } -} - -pub struct UpdateHandler { - pub pool: PgPool, - pub bn: BeaconNodeHttpClient, - pub blockprint: Option, - pub config: Config, - pub slots_per_epoch: u64, - pub _phantom: PhantomData, -} - -impl UpdateHandler { - pub async fn new( - bn: BeaconNodeHttpClient, - spec: WatchSpec, - config: FullConfig, - ) -> Result, Error> { - let blockprint = if config.blockprint.enabled { - if let Some(server) = config.blockprint.url { - let blockprint_url = SensitiveUrl::parse(&server).map_err(Error::SensitiveUrl)?; - Some(WatchBlockprintClient { - client: reqwest::Client::new(), - server: blockprint_url, - username: config.blockprint.username, - password: config.blockprint.password, - }) - } else { - return Err(Error::NotEnabled( - "blockprint was enabled but url was not set".to_string(), - )); - } - } else { - None - }; - - let mut pool = database::build_connection_pool(&config.database)?; - - ensure_valid_database(&spec, &mut pool).await?; - - Ok(Self { - pool, - bn, - blockprint, - config: config.updater, - slots_per_epoch: spec.slots_per_epoch(), - _phantom: PhantomData, - }) - } - - /// Gets the syncing status of the connected beacon node. - pub async fn get_bn_syncing_status(&mut self) -> Result { - Ok(self.bn.get_node_syncing().await?.data) - } - - /// Gets a list of block roots from the database which do not yet contain a corresponding - /// entry in the `beacon_blocks` table and inserts them. - pub async fn update_unknown_blocks(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let roots = database::get_unknown_canonical_blocks(&mut conn)?; - for root in roots { - let block_opt: Option> = - get_beacon_block(&self.bn, BlockId::Root(root.as_hash())).await?; - if let Some(block) = block_opt { - database::insert_beacon_block(&mut conn, block, root)?; - } - } - - Ok(()) - } - - /// Performs a head update with the following steps: - /// 1. Pull the latest header from the beacon node and the latest canonical slot from the - /// database. - /// 2. Loop back through the beacon node and database to find the first matching slot -> root - /// pair. - /// 3. Go back `MAX_EXPECTED_REORG_LENGTH` slots through the database ensuring it is - /// consistent with the beacon node. If a re-org occurs beyond this range, we cannot recover. - /// 4. Remove any invalid slots from the database. - /// 5. Sync all blocks between the first valid block of the database and the head of the beacon - /// chain. - /// - /// In the event there are no slots present in the database, it will sync from the head block - /// block back to the first slot of the epoch. - /// This will ensure backfills are always done in full epochs (which helps keep certain syncing - /// tasks efficient). - pub async fn perform_head_update(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - // Load the head from the beacon node. - let bn_header = get_header(&self.bn, BlockId::Head) - .await? - .ok_or(Error::UnableToGetRemoteHead)?; - let header_root = bn_header.canonical_root(); - - if let Some(latest_matching_canonical_slot) = - self.get_first_matching_block(bn_header.clone()).await? - { - // Check for reorgs. - let latest_db_slot = self.check_for_reorg(latest_matching_canonical_slot).await?; - - // Remove all slots above `latest_db_slot` from the database. - let result = database::delete_canonical_slots_above( - &mut conn, - WatchSlot::from_slot(latest_db_slot), - )?; - info!("{result} old records removed during head update"); - - if result > 0 { - // If slots were removed, we need to resync the suboptimal_attestations table for - // the epoch since they will have changed and cannot be fixed by a simple update. - let epoch = latest_db_slot - .epoch(self.slots_per_epoch) - .saturating_sub(1_u64); - debug!("Preparing to resync attestations above epoch {epoch}"); - database::delete_suboptimal_attestations_above( - &mut conn, - WatchSlot::from_slot(epoch.start_slot(self.slots_per_epoch)), - )?; - } - - // Since we are syncing backwards, `start_slot > `end_slot`. - let start_slot = bn_header.slot; - let end_slot = latest_db_slot + 1; - self.reverse_fill_canonical_slots(bn_header, header_root, false, start_slot, end_slot) - .await?; - info!("Reverse sync begun at slot {start_slot} and stopped at slot {end_slot}"); - - // Attempt to sync new blocks with blockprint. - //self.sync_blockprint_until(start_slot).await?; - } else { - // There are no matching parent blocks. Sync from the head block back until the first - // block of the epoch. - let start_slot = bn_header.slot; - let end_slot = start_slot.saturating_sub(start_slot % self.slots_per_epoch); - self.reverse_fill_canonical_slots(bn_header, header_root, false, start_slot, end_slot) - .await?; - info!("Reverse sync begun at slot {start_slot} and stopped at slot {end_slot}"); - } - - Ok(()) - } - - /// Attempt to find a row in the `canonical_slots` table which matches the `canonical_root` of - /// the block header as reported by the beacon node. - /// - /// Any blocks above this value are not canonical according to the beacon node. - /// - /// Note: In the event that there are skip slots above the slot returned by the function, - /// they will not be returned, so may be pruned or re-synced by other code despite being - /// canonical. - pub async fn get_first_matching_block( - &mut self, - mut bn_header: BeaconBlockHeader, - ) -> Result, Error> { - let mut conn = database::get_connection(&self.pool)?; - - // Load latest non-skipped canonical slot from database. - if let Some(db_canonical_slot) = - database::get_highest_non_skipped_canonical_slot(&mut conn)? - { - // Check if the header or parent root matches the entry in the database. - if bn_header.parent_root == db_canonical_slot.root.as_hash() - || bn_header.canonical_root() == db_canonical_slot.root.as_hash() - { - Ok(Some(db_canonical_slot)) - } else { - // Header is not the child of the highest entry in the database. - // From here we need to iterate backwards through the database until we find - // a slot -> root pair that matches the beacon node. - loop { - // Store working `parent_root`. - let parent_root = bn_header.parent_root; - - // Try the next header. - let next_header = get_header(&self.bn, BlockId::Root(parent_root)).await?; - if let Some(header) = next_header { - bn_header = header.clone(); - if let Some(db_canonical_slot) = database::get_canonical_slot_by_root( - &mut conn, - WatchHash::from_hash(header.parent_root), - )? { - // Check if the entry in the database matches the parent of - // the header. - if header.parent_root == db_canonical_slot.root.as_hash() { - return Ok(Some(db_canonical_slot)); - } else { - // Move on to the next header. - continue; - } - } else { - // Database does not have the referenced root. Try the next header. - continue; - } - } else { - // If we get this error it means that the `parent_root` of the header - // did not reference a canonical block. - return Err(Error::BeaconChain(BeaconChainError::MissingBeaconBlock( - parent_root, - ))); - } - } - } - } else { - // There are no non-skipped blocks present in the database. - Ok(None) - } - } - - /// Given the latest slot in the database which matches a root in the beacon node, - /// traverse back through the database for `MAX_EXPECTED_REORG_LENGTH` slots to ensure the tip - /// of the database is consistent with the beacon node (in the case that reorgs have occured). - /// - /// Returns the slot before the oldest canonical_slot which has an invalid child. - pub async fn check_for_reorg( - &mut self, - latest_canonical_slot: WatchCanonicalSlot, - ) -> Result { - let mut conn = database::get_connection(&self.pool)?; - - let end_slot = latest_canonical_slot.slot.as_u64(); - let start_slot = end_slot.saturating_sub(MAX_EXPECTED_REORG_LENGTH); - - for i in start_slot..end_slot { - let slot = Slot::new(i); - let db_canonical_slot_opt = - database::get_canonical_slot(&mut conn, WatchSlot::from_slot(slot))?; - if let Some(db_canonical_slot) = db_canonical_slot_opt { - let header_opt = get_header(&self.bn, BlockId::Slot(slot)).await?; - if let Some(header) = header_opt { - if header.canonical_root() == db_canonical_slot.root.as_hash() { - // The roots match (or are both skip slots). - continue; - } else { - // The block roots do not match. We need to re-sync from here. - warn!("Block {slot} does not match the beacon node. Resyncing"); - return Ok(slot.saturating_sub(1_u64)); - } - } else if !db_canonical_slot.skipped { - // The block exists in the database, but does not exist on the beacon node. - // We need to re-sync from here. - warn!("Block {slot} does not exist on the beacon node. Resyncing"); - return Ok(slot.saturating_sub(1_u64)); - } - } else { - // This slot does not exist in the database. - let lowest_slot = database::get_lowest_canonical_slot(&mut conn)? - .map(|canonical_slot| canonical_slot.slot.as_slot()); - if lowest_slot > Some(slot) { - // The database has not back-filled this slot yet, so skip it. - continue; - } else { - // The database does not contain this block, but has back-filled past it. - // We need to resync from here. - warn!("Slot {slot} missing from database. Resyncing"); - return Ok(slot.saturating_sub(1_u64)); - } - } - } - - // The database is consistent with the beacon node, so return the head of the database. - Ok(latest_canonical_slot.slot.as_slot()) - } - - /// Fills the canonical slots table beginning from `start_slot` and ending at `end_slot`. - /// It fills in reverse order, that is, `start_slot` is higher than `end_slot`. - /// - /// Skip slots set `root` to the root of the previous non-skipped slot and also sets - /// `skipped == true`. - /// - /// Since it uses `insert_canonical_slot` to interact with the database, it WILL NOT overwrite - /// existing rows. This means that any part of the chain within `end_slot..=start_slot` that - /// needs to be resynced, must first be deleted from the database. - pub async fn reverse_fill_canonical_slots( - &mut self, - mut header: BeaconBlockHeader, - mut header_root: Hash256, - mut skipped: bool, - start_slot: Slot, - end_slot: Slot, - ) -> Result { - let mut count = 0; - - let mut conn = database::get_connection(&self.pool)?; - - // Iterate, descending from `start_slot` (higher) to `end_slot` (lower). - for slot in (end_slot.as_u64()..=start_slot.as_u64()).rev() { - // Insert header. - database::insert_canonical_slot( - &mut conn, - WatchCanonicalSlot { - slot: WatchSlot::new(slot), - root: WatchHash::from_hash(header_root), - skipped, - beacon_block: None, - }, - )?; - count += 1; - - // Load the next header: - // We must use BlockId::Slot since we want to include skip slots. - header = if let Some(new_header) = get_header( - &self.bn, - BlockId::Slot(Slot::new(slot.saturating_sub(1_u64))), - ) - .await? - { - header_root = new_header.canonical_root(); - skipped = false; - new_header - } else { - if header.slot == 0 { - info!("Reverse fill exhausted at slot 0"); - break; - } - // Slot was skipped, so use the parent_root (most recent non-skipped block). - skipped = true; - header_root = header.parent_root; - header - }; - } - - Ok(count) - } - - /// Backfills the `canonical_slots` table starting from the lowest non-skipped slot and - /// stopping after `max_backfill_size_epochs` epochs. - pub async fn backfill_canonical_slots(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - let backfill_stop_slot = self.config.backfill_stop_epoch * self.slots_per_epoch; - // Check to see if we have finished backfilling. - if let Some(lowest_slot) = database::get_lowest_canonical_slot(&mut conn)? { - if lowest_slot.slot.as_slot() == backfill_stop_slot { - debug!("Backfill sync complete, all slots filled"); - return Ok(()); - } - } - - let backfill_slot_count = self.config.max_backfill_size_epochs * self.slots_per_epoch; - - if let Some(lowest_non_skipped_canonical_slot) = - database::get_lowest_non_skipped_canonical_slot(&mut conn)? - { - // Set `start_slot` equal to the lowest non-skipped slot in the database. - // While this will attempt to resync some parts of the bottom of the chain, it reduces - // complexity when dealing with skip slots. - let start_slot = lowest_non_skipped_canonical_slot.slot.as_slot(); - let mut end_slot = lowest_non_skipped_canonical_slot - .slot - .as_slot() - .saturating_sub(backfill_slot_count); - - // Ensure end_slot doesn't go below `backfill_stop_epoch` - if end_slot <= backfill_stop_slot { - end_slot = Slot::new(backfill_stop_slot); - } - - let header_opt = get_header(&self.bn, BlockId::Slot(start_slot)).await?; - - if let Some(header) = header_opt { - let header_root = header.canonical_root(); - let count = self - .reverse_fill_canonical_slots(header, header_root, false, start_slot, end_slot) - .await?; - - info!("Backfill completed to slot: {end_slot}, records added: {count}"); - } else { - // The lowest slot of the database is inconsistent with the beacon node. - // Currently we have no way to recover from this. The entire database will need to - // be re-synced. - error!( - "Database is inconsistent with the beacon node. \ - Please ensure your beacon node is set to the right network, \ - otherwise you may need to resync" - ); - } - } else { - // There are no blocks in the database. Forward sync needs to happen first. - info!("Backfill was not performed since there are no blocks in the database"); - return Ok(()); - }; - - Ok(()) - } - - // Attempt to update the validator set. - // This downloads the latest validator set from the beacon node, and pulls the known validator - // set from the database. - // We then take any new or updated validators and insert them into the database (overwriting - // exiting validators). - // - // In the event there are no validators in the database, it will initialize the validator set. - pub async fn update_validator_set(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - let current_validators = database::get_all_validators(&mut conn)?; - - if !current_validators.is_empty() { - let old_validators = HashSet::from_iter(current_validators); - - // Pull the new validator set from the beacon node. - let new_validators = get_validators(&self.bn).await?; - - // The difference should only contain validators that contain either a new `exit_epoch` (implying an - // exit) or a new `index` (implying a validator activation). - let val_diff = new_validators.difference(&old_validators); - - for diff in val_diff { - database::insert_validator(&mut conn, diff.clone())?; - } - } else { - info!("No validators present in database. Initializing the validator set"); - self.initialize_validator_set().await?; - } - - Ok(()) - } - - // Initialize the validator set by downloading it from the beacon node, inserting blockprint - // data (if required) and writing it to the database. - pub async fn initialize_validator_set(&mut self) -> Result<(), Error> { - let mut conn = database::get_connection(&self.pool)?; - - // Pull all validators from the beacon node. - let validators = Vec::from_iter(get_validators(&self.bn).await?); - - database::insert_batch_validators(&mut conn, validators)?; - - Ok(()) - } -} diff --git a/watch/src/updater/mod.rs b/watch/src/updater/mod.rs deleted file mode 100644 index 65e0a90a2b..0000000000 --- a/watch/src/updater/mod.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::config::Config as FullConfig; -use crate::database::{WatchPK, WatchValidator}; -use eth2::{ - types::{BlockId, StateId}, - BeaconNodeHttpClient, SensitiveUrl, Timeouts, -}; -use log::{debug, error, info}; -use std::collections::{HashMap, HashSet}; -use std::marker::PhantomData; -use std::time::{Duration, Instant}; -use types::{BeaconBlockHeader, EthSpec, GnosisEthSpec, MainnetEthSpec, SignedBeaconBlock}; - -pub use config::Config; -pub use error::Error; -pub use handler::UpdateHandler; - -mod config; -pub mod error; -pub mod handler; - -const FAR_FUTURE_EPOCH: u64 = u64::MAX; -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); - -const MAINNET: &str = "mainnet"; -const GNOSIS: &str = "gnosis"; - -pub struct WatchSpec { - network: String, - spec: PhantomData, -} - -impl WatchSpec { - fn slots_per_epoch(&self) -> u64 { - E::slots_per_epoch() - } -} - -impl WatchSpec { - pub fn mainnet(network: String) -> Self { - Self { - network, - spec: PhantomData, - } - } -} - -impl WatchSpec { - fn gnosis(network: String) -> Self { - Self { - network, - spec: PhantomData, - } - } -} - -pub async fn run_updater(config: FullConfig) -> Result<(), Error> { - let beacon_node_url = - SensitiveUrl::parse(&config.updater.beacon_node_url).map_err(Error::SensitiveUrl)?; - let bn = BeaconNodeHttpClient::new(beacon_node_url, Timeouts::set_all(DEFAULT_TIMEOUT)); - - let config_map = bn.get_config_spec::>().await?.data; - - let config_name = config_map - .get("CONFIG_NAME") - .ok_or_else(|| { - Error::BeaconNodeNotCompatible("No field CONFIG_NAME on beacon node spec".to_string()) - })? - .clone(); - - match config_map - .get("PRESET_BASE") - .ok_or_else(|| { - Error::BeaconNodeNotCompatible("No field PRESET_BASE on beacon node spec".to_string()) - })? - .to_lowercase() - .as_str() - { - MAINNET => { - let spec = WatchSpec::mainnet(config_name); - run_once(bn, spec, config).await - } - GNOSIS => { - let spec = WatchSpec::gnosis(config_name); - run_once(bn, spec, config).await - } - _ => unimplemented!("unsupported PRESET_BASE"), - } -} - -pub async fn run_once( - bn: BeaconNodeHttpClient, - spec: WatchSpec, - config: FullConfig, -) -> Result<(), Error> { - let mut watch = UpdateHandler::new(bn, spec, config.clone()).await?; - - let sync_data = watch.get_bn_syncing_status().await?; - if sync_data.is_syncing { - error!( - "Connected beacon node is still syncing: head_slot => {:?}, distance => {}", - sync_data.head_slot, sync_data.sync_distance - ); - return Err(Error::BeaconNodeSyncing); - } - - info!("Performing head update"); - let head_timer = Instant::now(); - watch.perform_head_update().await?; - let head_timer_elapsed = head_timer.elapsed(); - debug!("Head update complete, time taken: {head_timer_elapsed:?}"); - - info!("Performing block backfill"); - let block_backfill_timer = Instant::now(); - watch.backfill_canonical_slots().await?; - let block_backfill_timer_elapsed = block_backfill_timer.elapsed(); - debug!("Block backfill complete, time taken: {block_backfill_timer_elapsed:?}"); - - info!("Updating validator set"); - let validator_timer = Instant::now(); - watch.update_validator_set().await?; - let validator_timer_elapsed = validator_timer.elapsed(); - debug!("Validator update complete, time taken: {validator_timer_elapsed:?}"); - - // Update blocks after updating the validator set since the `proposer_index` must exist in the - // `validators` table. - info!("Updating unknown blocks"); - let unknown_block_timer = Instant::now(); - watch.update_unknown_blocks().await?; - let unknown_block_timer_elapsed = unknown_block_timer.elapsed(); - debug!("Unknown block update complete, time taken: {unknown_block_timer_elapsed:?}"); - - // Run additional modules - if config.updater.attestations { - info!("Updating suboptimal attestations"); - let attestation_timer = Instant::now(); - watch.fill_suboptimal_attestations().await?; - watch.backfill_suboptimal_attestations().await?; - let attestation_timer_elapsed = attestation_timer.elapsed(); - debug!("Attestation update complete, time taken: {attestation_timer_elapsed:?}"); - } - - if config.updater.block_rewards { - info!("Updating block rewards"); - let rewards_timer = Instant::now(); - watch.fill_block_rewards().await?; - watch.backfill_block_rewards().await?; - let rewards_timer_elapsed = rewards_timer.elapsed(); - debug!("Block Rewards update complete, time taken: {rewards_timer_elapsed:?}"); - } - - if config.updater.block_packing { - info!("Updating block packing statistics"); - let packing_timer = Instant::now(); - watch.fill_block_packing().await?; - watch.backfill_block_packing().await?; - let packing_timer_elapsed = packing_timer.elapsed(); - debug!("Block packing update complete, time taken: {packing_timer_elapsed:?}"); - } - - if config.blockprint.enabled { - info!("Updating blockprint"); - let blockprint_timer = Instant::now(); - watch.fill_blockprint().await?; - watch.backfill_blockprint().await?; - let blockprint_timer_elapsed = blockprint_timer.elapsed(); - debug!("Blockprint update complete, time taken: {blockprint_timer_elapsed:?}"); - } - - Ok(()) -} - -/// Queries the beacon node for a given `BlockId` and returns the `BeaconBlockHeader` if it exists. -pub async fn get_header( - bn: &BeaconNodeHttpClient, - block_id: BlockId, -) -> Result, Error> { - let resp = bn - .get_beacon_headers_block_id(block_id) - .await? - .map(|resp| (resp.data.root, resp.data.header.message)); - // When quering with root == 0x000... , slot 0 will be returned with parent_root == 0x0000... - // This check escapes the loop. - if let Some((root, header)) = resp { - if root == header.parent_root { - return Ok(None); - } else { - return Ok(Some(header)); - } - } - Ok(None) -} - -pub async fn get_beacon_block( - bn: &BeaconNodeHttpClient, - block_id: BlockId, -) -> Result>, Error> { - let block = bn.get_beacon_blocks(block_id).await?.map(|resp| resp.data); - - Ok(block) -} - -/// Queries the beacon node for the current validator set. -pub async fn get_validators(bn: &BeaconNodeHttpClient) -> Result, Error> { - let mut validator_map = HashSet::new(); - - let validators = bn - .get_beacon_states_validators(StateId::Head, None, None) - .await? - .ok_or(Error::NoValidatorsFound)? - .data; - - for val in validators { - // Only store `activation_epoch` if it not the `FAR_FUTURE_EPOCH`. - let activation_epoch = if val.validator.activation_epoch.as_u64() == FAR_FUTURE_EPOCH { - None - } else { - Some(val.validator.activation_epoch.as_u64() as i32) - }; - // Only store `exit_epoch` if it is not the `FAR_FUTURE_EPOCH`. - let exit_epoch = if val.validator.exit_epoch.as_u64() == FAR_FUTURE_EPOCH { - None - } else { - Some(val.validator.exit_epoch.as_u64() as i32) - }; - validator_map.insert(WatchValidator { - index: val.index as i32, - public_key: WatchPK::from_pubkey(val.validator.pubkey), - status: val.status.to_string(), - activation_epoch, - exit_epoch, - }); - } - Ok(validator_map) -} diff --git a/watch/tests/tests.rs b/watch/tests/tests.rs deleted file mode 100644 index e21cf151b1..0000000000 --- a/watch/tests/tests.rs +++ /dev/null @@ -1,1294 +0,0 @@ -#![recursion_limit = "256"] -#![cfg(unix)] - -use beacon_chain::{ - test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, - ChainConfig, -}; -use eth2::{types::BlockId, BeaconNodeHttpClient, SensitiveUrl, Timeouts}; -use http_api::test_utils::{create_api_server, ApiServer}; -use log::error; -use logging::test_logger; -use network::NetworkReceivers; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; -use std::collections::HashMap; -use std::env; -use std::time::Duration; -use testcontainers::{clients::Cli, core::WaitFor, Image, RunnableImage}; -use tokio::{runtime, task::JoinHandle}; -use tokio_postgres::{config::Config as PostgresConfig, Client, NoTls}; -use types::{Hash256, MainnetEthSpec, Slot}; -use unused_port::unused_tcp4_port; -use url::Url; -use watch::{ - client::WatchHttpClient, - config::Config, - database::{self, Config as DatabaseConfig, PgPool, WatchSlot}, - server::{start_server, Config as ServerConfig}, - updater::{handler::*, run_updater, Config as UpdaterConfig, WatchSpec}, -}; - -#[derive(Debug)] -pub struct Postgres(HashMap); - -impl Default for Postgres { - fn default() -> Self { - let mut env_vars = HashMap::new(); - env_vars.insert("POSTGRES_DB".to_owned(), "postgres".to_owned()); - env_vars.insert("POSTGRES_HOST_AUTH_METHOD".into(), "trust".into()); - - Self(env_vars) - } -} - -impl Image for Postgres { - type Args = (); - - fn name(&self) -> String { - "postgres".to_owned() - } - - fn tag(&self) -> String { - "11-alpine".to_owned() - } - - fn ready_conditions(&self) -> Vec { - vec![WaitFor::message_on_stderr( - "database system is ready to accept connections", - )] - } - - fn env_vars(&self) -> Box + '_> { - Box::new(self.0.iter()) - } -} - -type E = MainnetEthSpec; - -const VALIDATOR_COUNT: usize = 32; -const SLOTS_PER_EPOCH: u64 = 32; -const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); - -/// Set this environment variable to use a different hostname for connecting to -/// the database. Can be set to `host.docker.internal` for docker-in-docker -/// setups. -const WATCH_HOST_ENV_VARIABLE: &str = "WATCH_HOST"; - -fn build_test_config(config: &DatabaseConfig) -> PostgresConfig { - let mut postgres_config = PostgresConfig::new(); - postgres_config - .user(&config.user) - .password(&config.password) - .dbname(&config.default_dbname) - .host(&config.host) - .port(config.port) - .connect_timeout(Duration::from_millis(config.connect_timeout_millis)); - postgres_config -} - -async fn connect(config: &DatabaseConfig) -> (Client, JoinHandle<()>) { - let db_config = build_test_config(config); - let (client, conn) = db_config - .connect(NoTls) - .await - .expect("Could not connect to db"); - let connection = runtime::Handle::current().spawn(async move { - if let Err(e) = conn.await { - error!("Connection error {:?}", e); - } - }); - - (client, connection) -} - -pub async fn create_test_database(config: &DatabaseConfig) { - let (db, _) = connect(config).await; - - db.execute(&format!("CREATE DATABASE {};", config.dbname), &[]) - .await - .expect("Database creation failed"); -} - -pub fn get_host_from_env() -> String { - env::var(WATCH_HOST_ENV_VARIABLE).unwrap_or_else(|_| "localhost".to_string()) -} - -struct TesterBuilder { - pub harness: BeaconChainHarness>, - pub config: Config, - _bn_network_rx: NetworkReceivers, -} - -impl TesterBuilder { - pub async fn new() -> TesterBuilder { - let harness = BeaconChainHarness::builder(E::default()) - .default_spec() - .chain_config(ChainConfig { - reconstruct_historic_states: true, - ..ChainConfig::default() - }) - .logger(test_logger()) - .deterministic_keypairs(VALIDATOR_COUNT) - .fresh_ephemeral_store() - .build(); - - /* - * Spawn a Beacon Node HTTP API. - */ - let ApiServer { - server, - listening_socket: bn_api_listening_socket, - network_rx: _bn_network_rx, - .. - } = create_api_server( - harness.chain.clone(), - &harness.runtime, - harness.logger().clone(), - ) - .await; - tokio::spawn(server); - - /* - * Create a watch configuration - */ - let database_port = unused_tcp4_port().expect("Unable to find unused port."); - let server_port = 0; - let config = Config { - database: DatabaseConfig { - dbname: random_dbname(), - port: database_port, - host: get_host_from_env(), - ..Default::default() - }, - server: ServerConfig { - listen_port: server_port, - ..Default::default() - }, - updater: UpdaterConfig { - beacon_node_url: format!( - "http://{}:{}", - bn_api_listening_socket.ip(), - bn_api_listening_socket.port() - ), - ..Default::default() - }, - ..Default::default() - }; - - Self { - harness, - config, - _bn_network_rx, - } - } - pub async fn build(self, pool: PgPool) -> Tester { - /* - * Spawn a Watch HTTP API. - */ - let (addr, watch_server) = start_server(&self.config, SLOTS_PER_EPOCH, pool).unwrap(); - tokio::spawn(watch_server); - - /* - * Create a HTTP client to talk to the watch HTTP API. - */ - let client = WatchHttpClient { - client: reqwest::Client::new(), - server: Url::parse(&format!("http://{}:{}", addr.ip(), addr.port())).unwrap(), - }; - - /* - * Create a HTTP client to talk to the Beacon Node API. - */ - let beacon_node_url = SensitiveUrl::parse(&self.config.updater.beacon_node_url).unwrap(); - let bn = BeaconNodeHttpClient::new(beacon_node_url, Timeouts::set_all(DEFAULT_TIMEOUT)); - let spec = WatchSpec::mainnet("mainnet".to_string()); - - /* - * Build update service - */ - let updater = UpdateHandler::new(bn, spec, self.config.clone()) - .await - .unwrap(); - - Tester { - harness: self.harness, - client, - config: self.config, - updater, - _bn_network_rx: self._bn_network_rx, - } - } - async fn initialize_database(&self) -> PgPool { - create_test_database(&self.config.database).await; - database::utils::run_migrations(&self.config.database); - database::build_connection_pool(&self.config.database) - .expect("Could not build connection pool") - } -} - -struct Tester { - pub harness: BeaconChainHarness>, - pub client: WatchHttpClient, - pub config: Config, - pub updater: UpdateHandler, - _bn_network_rx: NetworkReceivers, -} - -impl Tester { - /// Extend the chain on the beacon chain harness. Do not update the beacon watch database. - pub async fn extend_chain(&mut self, num_blocks: u64) -> &mut Self { - self.harness.advance_slot(); - self.harness - .extend_chain( - num_blocks as usize, - BlockStrategy::OnCanonicalHead, - AttestationStrategy::AllValidators, - ) - .await; - self - } - - // Advance the slot clock without a block. This results in a skipped slot. - pub fn skip_slot(&mut self) -> &mut Self { - self.harness.advance_slot(); - self - } - - // Perform a single slot re-org. - pub async fn reorg_chain(&mut self) -> &mut Self { - let previous_slot = self.harness.get_current_slot(); - self.harness.advance_slot(); - let first_slot = self.harness.get_current_slot(); - self.harness - .extend_chain( - 1, - BlockStrategy::ForkCanonicalChainAt { - previous_slot, - first_slot, - }, - AttestationStrategy::AllValidators, - ) - .await; - self - } - - /// Run the watch updater service. - pub async fn run_update_service(&mut self, num_runs: usize) -> &mut Self { - for _ in 0..num_runs { - run_updater(self.config.clone()).await.unwrap(); - } - self - } - - pub async fn perform_head_update(&mut self) -> &mut Self { - self.updater.perform_head_update().await.unwrap(); - self - } - - pub async fn perform_backfill(&mut self) -> &mut Self { - self.updater.backfill_canonical_slots().await.unwrap(); - self - } - - pub async fn update_unknown_blocks(&mut self) -> &mut Self { - self.updater.update_unknown_blocks().await.unwrap(); - self - } - - pub async fn update_validator_set(&mut self) -> &mut Self { - self.updater.update_validator_set().await.unwrap(); - self - } - - pub async fn fill_suboptimal_attestations(&mut self) -> &mut Self { - self.updater.fill_suboptimal_attestations().await.unwrap(); - - self - } - - pub async fn backfill_suboptimal_attestations(&mut self) -> &mut Self { - self.updater - .backfill_suboptimal_attestations() - .await - .unwrap(); - - self - } - - pub async fn fill_block_rewards(&mut self) -> &mut Self { - self.updater.fill_block_rewards().await.unwrap(); - - self - } - - pub async fn backfill_block_rewards(&mut self) -> &mut Self { - self.updater.backfill_block_rewards().await.unwrap(); - - self - } - - pub async fn fill_block_packing(&mut self) -> &mut Self { - self.updater.fill_block_packing().await.unwrap(); - - self - } - - pub async fn backfill_block_packing(&mut self) -> &mut Self { - self.updater.backfill_block_packing().await.unwrap(); - - self - } - - pub async fn assert_canonical_slots_empty(&mut self) -> &mut Self { - let lowest_slot = self - .client - .get_lowest_canonical_slot() - .await - .unwrap() - .map(|slot| slot.slot.as_slot()); - - assert_eq!(lowest_slot, None); - - self - } - - pub async fn assert_lowest_canonical_slot(&mut self, expected: u64) -> &mut Self { - let slot = self - .client - .get_lowest_canonical_slot() - .await - .unwrap() - .unwrap() - .slot - .as_slot(); - - assert_eq!(slot, Slot::new(expected)); - - self - } - - pub async fn assert_highest_canonical_slot(&mut self, expected: u64) -> &mut Self { - let slot = self - .client - .get_highest_canonical_slot() - .await - .unwrap() - .unwrap() - .slot - .as_slot(); - - assert_eq!(slot, Slot::new(expected)); - - self - } - - pub async fn assert_canonical_slots_not_empty(&mut self) -> &mut Self { - self.client - .get_lowest_canonical_slot() - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_slot_is_skipped(&mut self, slot: u64) -> &mut Self { - assert!(self - .client - .get_beacon_blocks(BlockId::Slot(Slot::new(slot))) - .await - .unwrap() - .is_none()); - self - } - - pub async fn assert_all_validators_exist(&mut self) -> &mut Self { - assert_eq!( - self.client - .get_all_validators() - .await - .unwrap() - .unwrap() - .len(), - VALIDATOR_COUNT - ); - self - } - - pub async fn assert_lowest_block_has_proposer_info(&mut self) -> &mut Self { - let mut block = self - .client - .get_lowest_beacon_block() - .await - .unwrap() - .unwrap(); - - if block.slot.as_slot() == 0 { - block = self - .client - .get_next_beacon_block(block.root.as_hash()) - .await - .unwrap() - .unwrap() - } - - self.client - .get_proposer_info(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_highest_block_has_proposer_info(&mut self) -> &mut Self { - let block = self - .client - .get_highest_beacon_block() - .await - .unwrap() - .unwrap(); - - self.client - .get_proposer_info(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_lowest_block_has_block_rewards(&mut self) -> &mut Self { - let mut block = self - .client - .get_lowest_beacon_block() - .await - .unwrap() - .unwrap(); - - if block.slot.as_slot() == 0 { - block = self - .client - .get_next_beacon_block(block.root.as_hash()) - .await - .unwrap() - .unwrap() - } - - self.client - .get_block_reward(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_highest_block_has_block_rewards(&mut self) -> &mut Self { - let block = self - .client - .get_highest_beacon_block() - .await - .unwrap() - .unwrap(); - - self.client - .get_block_reward(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_lowest_block_has_block_packing(&mut self) -> &mut Self { - let mut block = self - .client - .get_lowest_beacon_block() - .await - .unwrap() - .unwrap(); - - while block.slot.as_slot() <= SLOTS_PER_EPOCH { - block = self - .client - .get_next_beacon_block(block.root.as_hash()) - .await - .unwrap() - .unwrap() - } - - self.client - .get_block_packing(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - pub async fn assert_highest_block_has_block_packing(&mut self) -> &mut Self { - let block = self - .client - .get_highest_beacon_block() - .await - .unwrap() - .unwrap(); - - self.client - .get_block_packing(BlockId::Root(block.root.as_hash())) - .await - .unwrap() - .unwrap(); - - self - } - - /// Check that the canonical chain in watch matches that of the harness. Also check that all - /// canonical blocks can be retrieved. - pub async fn assert_canonical_chain_consistent(&mut self, last_slot: u64) -> &mut Self { - let head_root = self.harness.chain.head_beacon_block_root(); - let mut chain: Vec<(Hash256, Slot)> = self - .harness - .chain - .rev_iter_block_roots_from(head_root) - .unwrap() - .map(Result::unwrap) - .collect(); - - // `chain` contains skip slots, but the `watch` API will not return blocks that do not - // exist. - // We need to filter them out. - chain.reverse(); - chain.dedup_by(|(hash1, _), (hash2, _)| hash1 == hash2); - - // Remove any slots below `last_slot` since it is known that the database has not - // backfilled past it. - chain.retain(|(_, slot)| slot.as_u64() >= last_slot); - - for (root, slot) in &chain { - let block = self - .client - .get_beacon_blocks(BlockId::Root(*root)) - .await - .unwrap() - .unwrap(); - assert_eq!(block.slot.as_slot(), *slot); - } - - self - } - - /// Check that every block in the `beacon_blocks` table has corresponding entries in the - /// `proposer_info`, `block_rewards` and `block_packing` tables. - pub async fn assert_all_blocks_have_metadata(&mut self) -> &mut Self { - let pool = database::build_connection_pool(&self.config.database).unwrap(); - - let mut conn = database::get_connection(&pool).unwrap(); - let highest_block_slot = database::get_highest_beacon_block(&mut conn) - .unwrap() - .unwrap() - .slot - .as_slot(); - let lowest_block_slot = database::get_lowest_beacon_block(&mut conn) - .unwrap() - .unwrap() - .slot - .as_slot(); - for slot in lowest_block_slot.as_u64()..=highest_block_slot.as_u64() { - let canonical_slot = database::get_canonical_slot(&mut conn, WatchSlot::new(slot)) - .unwrap() - .unwrap(); - if !canonical_slot.skipped { - database::get_block_rewards_by_slot(&mut conn, WatchSlot::new(slot)) - .unwrap() - .unwrap(); - database::get_proposer_info_by_slot(&mut conn, WatchSlot::new(slot)) - .unwrap() - .unwrap(); - database::get_block_packing_by_slot(&mut conn, WatchSlot::new(slot)) - .unwrap() - .unwrap(); - } - } - - self - } -} - -pub fn random_dbname() -> String { - let mut s: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(8) - .map(char::from) - .collect(); - // Postgres gets weird about capitals in database names. - s.make_ascii_lowercase(); - format!("test_{}", s) -} - -#[cfg(unix)] -#[tokio::test] -async fn short_chain() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - .extend_chain(16) - .await - .assert_canonical_slots_empty() - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_canonical_slots_not_empty() - .await - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn short_chain_sync_starts_on_skip_slot() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - .skip_slot() - .skip_slot() - .extend_chain(6) - .await - .skip_slot() - .extend_chain(6) - .await - .skip_slot() - .assert_canonical_slots_empty() - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_canonical_slots_not_empty() - .await - .assert_canonical_chain_consistent(0) - .await - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn short_chain_with_skip_slot() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - .extend_chain(5) - .await - .assert_canonical_slots_empty() - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_canonical_slots_not_empty() - .await - .assert_highest_canonical_slot(5) - .await - .assert_lowest_canonical_slot(0) - .await - .assert_canonical_chain_consistent(0) - .await - .skip_slot() - .extend_chain(1) - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_highest_canonical_slot(7) - .await - .assert_slot_is_skipped(6) - .await - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn short_chain_with_reorg() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - .extend_chain(5) - .await - .assert_canonical_slots_empty() - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_canonical_slots_not_empty() - .await - .assert_highest_canonical_slot(5) - .await - .assert_lowest_canonical_slot(0) - .await - .assert_canonical_chain_consistent(0) - .await - .skip_slot() - .reorg_chain() - .await - .extend_chain(1) - .await - .run_update_service(1) - .await - .assert_all_validators_exist() - .await - .assert_highest_canonical_slot(8) - .await - .assert_slot_is_skipped(6) - .await - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn chain_grows() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - // Apply four blocks to the chain. - tester - .extend_chain(4) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(4) - .await - // And also backfill to the epoch boundary. - .assert_lowest_canonical_slot(0) - .await - // Fill back to genesis. - .perform_backfill() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(4) - .await - // Apply one block to the chain. - .extend_chain(1) - .await - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(5) - .await - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(7) - .await - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -#[allow(clippy::large_stack_frames)] -async fn chain_grows_with_metadata() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - tester - // Apply four blocks to the chain. - .extend_chain(4) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(4) - .await - // And also backfill to the epoch boundary. - .assert_lowest_canonical_slot(0) - .await - // Fill back to genesis. - .perform_backfill() - .await - // Insert all validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await - // Get other chain data. - // Backfill before forward fill to ensure order is arbitrary. - .backfill_block_rewards() - .await - .fill_block_rewards() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(4) - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await - // Apply one block to the chain. - .extend_chain(1) - .await - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(5) - .await - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(7) - .await - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await - // Get other chain data. - .fill_block_rewards() - .await - .backfill_block_rewards() - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await; -} - -#[cfg(unix)] -#[tokio::test] -#[allow(clippy::large_stack_frames)] -async fn chain_grows_with_metadata_and_multiple_skip_slots() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - - // Apply four blocks to the chain. - tester - .extend_chain(4) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(4) - // And also backfill to the epoch boundary. - .await - .assert_lowest_canonical_slot(0) - .await - // Fill back to genesis. - .perform_backfill() - .await - // Insert all validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Check the chain is consistent. - .assert_canonical_chain_consistent(0) - .await - // Get other chain data. - .fill_block_rewards() - .await - .backfill_block_rewards() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(4) - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await - // Add multiple skip slots. - .skip_slot() - .skip_slot() - .skip_slot() - // Apply one block to the chain. - .extend_chain(1) - .await - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(8) - .await - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(10) - .await - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await - // Get other chain data. - // Backfill before forward fill to ensure order is arbitrary. - .backfill_block_rewards() - .await - .fill_block_rewards() - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn chain_grows_to_second_epoch() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - // Apply 40 blocks to the chain. - tester - .extend_chain(40) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(40) - .await - // And also backfill to the epoch boundary. - .assert_lowest_canonical_slot(32) - .await - // Fill back to genesis. - .perform_backfill() - .await - // Insert all validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Check the chain is consistent. - .assert_canonical_chain_consistent(0) - .await - // Get block packings. - .fill_block_packing() - .await - .backfill_block_packing() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(40) - .await - // All packings should be present. - .assert_lowest_block_has_block_packing() - .await - .assert_highest_block_has_block_packing() - .await - // Skip a slot - .skip_slot() - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - // All blocks should be present. - .assert_lowest_canonical_slot(0) - .await - .assert_highest_canonical_slot(43) - .await - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // Update new block_packing - // Backfill before forward fill to ensure order is arbitrary - .backfill_block_packing() - .await - .fill_block_packing() - .await - // All packings should be present. - .assert_lowest_block_has_block_packing() - .await - .assert_highest_block_has_block_packing() - .await - // Check the chain is consistent - .assert_canonical_chain_consistent(0) - .await; -} - -#[cfg(unix)] -#[tokio::test] -async fn large_chain() { - let builder = TesterBuilder::new().await; - - let docker = Cli::default(); - let image = RunnableImage::from(Postgres::default()) - .with_mapped_port((builder.config.database.port, 5432)); - let _node = docker.run(image); - - let pool = builder.initialize_database().await; - let mut tester = builder.build(pool).await; - // Apply 40 blocks to the chain. - tester - .extend_chain(400) - .await - .perform_head_update() - .await - // Head update should insert the head block. - .assert_highest_canonical_slot(400) - .await - // And also backfill to the epoch boundary. - .assert_lowest_canonical_slot(384) - .await - // Backfill 2 epochs as per default config. - .perform_backfill() - .await - // Insert all validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Check the chain is consistent. - .assert_canonical_chain_consistent(384) - .await - // Get block rewards and proposer info. - .fill_block_rewards() - .await - .backfill_block_rewards() - .await - // Get block packings. - .fill_block_packing() - .await - .backfill_block_packing() - .await - // Should have backfilled 2 more epochs. - .assert_lowest_canonical_slot(320) - .await - .assert_highest_canonical_slot(400) - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await - // All packings should be present. - .assert_lowest_block_has_block_packing() - .await - .assert_highest_block_has_block_packing() - .await - // Skip a slot - .skip_slot() - // Apply two blocks to the chain. - .extend_chain(2) - .await - // Update the head. - .perform_head_update() - .await - .perform_backfill() - .await - // Should have backfilled 2 more epochs - .assert_lowest_canonical_slot(256) - .await - .assert_highest_canonical_slot(403) - .await - // Update validators - .update_validator_set() - .await - // Insert all blocks. - .update_unknown_blocks() - .await - // All validators should be present. - .assert_all_validators_exist() - .await - // Get suboptimal attestations. - .fill_suboptimal_attestations() - .await - .backfill_suboptimal_attestations() - .await - // Get block rewards and proposer info. - .fill_block_rewards() - .await - .backfill_block_rewards() - .await - // Get block packing. - // Backfill before forward fill to ensure order is arbitrary. - .backfill_block_packing() - .await - .fill_block_packing() - .await - // All rewards should be present. - .assert_lowest_block_has_block_rewards() - .await - .assert_highest_block_has_block_rewards() - .await - // All proposers should be present. - .assert_lowest_block_has_proposer_info() - .await - .assert_highest_block_has_proposer_info() - .await - // All packings should be present. - .assert_lowest_block_has_block_packing() - .await - .assert_highest_block_has_block_packing() - .await - // Check the chain is consistent. - .assert_canonical_chain_consistent(256) - .await - // Check every block has rewards, proposer info and packing statistics. - .assert_all_blocks_have_metadata() - .await; -} From 3bc5f1f2a58b1df9454884672c8100fd5f79ba8b Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Wed, 5 Mar 2025 19:21:10 -0800 Subject: [PATCH 13/13] Validator Registration ssz support (#7081) N/A Derive ssz::Encode and Decode on the `SignedValidatorRegistrationData` type to use in the builder --- consensus/types/src/validator_registration_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/consensus/types/src/validator_registration_data.rs b/consensus/types/src/validator_registration_data.rs index cdafd355e7..345771074c 100644 --- a/consensus/types/src/validator_registration_data.rs +++ b/consensus/types/src/validator_registration_data.rs @@ -4,7 +4,7 @@ use ssz_derive::{Decode, Encode}; use tree_hash_derive::TreeHash; /// Validator registration, for use in interacting with servers implementing the builder API. -#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] pub struct SignedValidatorRegistrationData { pub message: ValidatorRegistrationData, pub signature: Signature,