mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-09 03:17:55 +00:00
Custody backfill sync (#7907)
#7603 #### Custody backfill sync service Similar in many ways to the current backfill service. There may be ways to unify the two services. The difficulty there is that the current backfill service tightly couples blocks and their associated blobs/data columns. Any attempts to unify the two services should be left to a separate PR in my opinion. #### `SyncNeworkContext` `SyncNetworkContext` manages custody sync data columns by range requests separetly from other sync RPC requests. I think this is a nice separation considering that custody backfill is its own service. #### Data column import logic The import logic verifies KZG committments and that the data columns block root matches the block root in the nodes store before importing columns #### New channel to send messages to `SyncManager` Now external services can communicate with the `SyncManager`. In this PR this channel is used to trigger a custody sync. Alternatively we may be able to use the existing `mpsc` channel that the `SyncNetworkContext` uses to communicate with the `SyncManager`. I will spend some time reviewing this. Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu> Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com> Co-Authored-By: dapplion <35266934+dapplion@users.noreply.github.com>
This commit is contained in:
@@ -4,12 +4,14 @@ use beacon_chain::attestation_verification::Error as AttnError;
|
||||
use beacon_chain::block_verification_types::RpcBlock;
|
||||
use beacon_chain::builder::BeaconChainBuilder;
|
||||
use beacon_chain::data_availability_checker::AvailableBlock;
|
||||
use beacon_chain::historical_data_columns::HistoricalDataColumnError;
|
||||
use beacon_chain::schema_change::migrate_schema;
|
||||
use beacon_chain::test_utils::SyncCommitteeStrategy;
|
||||
use beacon_chain::test_utils::{
|
||||
AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, get_kzg,
|
||||
mock_execution_layer_from_parts, test_spec,
|
||||
};
|
||||
use beacon_chain::validator_custody::CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS;
|
||||
use beacon_chain::{
|
||||
BeaconChain, BeaconChainError, BeaconChainTypes, BeaconSnapshot, BlockError, ChainConfig,
|
||||
NotifyExecutionLayer, ServerSentEventHandler, WhenSlotSkipped,
|
||||
@@ -3169,6 +3171,245 @@ async fn weak_subjectivity_sync_test(
|
||||
assert_eq!(store.get_anchor_info().state_upper_limit, Slot::new(0));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_import_historical_data_columns_batch() {
|
||||
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
|
||||
let db_path = tempdir().unwrap();
|
||||
let store = get_store_generic(&db_path, StoreConfig::default(), spec);
|
||||
let start_slot = Epoch::new(0).start_slot(E::slots_per_epoch()) + 1;
|
||||
let end_slot = Epoch::new(0).end_slot(E::slots_per_epoch());
|
||||
|
||||
let harness = get_harness_import_all_data_columns(store.clone(), LOW_VALIDATOR_COUNT);
|
||||
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * 2) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
harness.advance_slot();
|
||||
|
||||
let block_root_iter = harness
|
||||
.chain
|
||||
.forwards_iter_block_roots_until(start_slot, end_slot)
|
||||
.unwrap();
|
||||
|
||||
let mut data_columns_list = vec![];
|
||||
|
||||
for block in block_root_iter {
|
||||
let (block_root, _) = block.unwrap();
|
||||
let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap();
|
||||
assert!(data_columns.is_some());
|
||||
for data_column in data_columns.unwrap() {
|
||||
data_columns_list.push(data_column);
|
||||
}
|
||||
}
|
||||
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * 4) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
harness.advance_slot();
|
||||
|
||||
harness
|
||||
.chain
|
||||
.store
|
||||
.try_prune_blobs(true, Epoch::new(2))
|
||||
.unwrap();
|
||||
|
||||
let block_root_iter = harness
|
||||
.chain
|
||||
.forwards_iter_block_roots_until(start_slot, end_slot)
|
||||
.unwrap();
|
||||
|
||||
for block in block_root_iter {
|
||||
let (block_root, _) = block.unwrap();
|
||||
let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap();
|
||||
assert!(data_columns.is_none())
|
||||
}
|
||||
|
||||
harness
|
||||
.chain
|
||||
.import_historical_data_column_batch(Epoch::new(0), data_columns_list)
|
||||
.unwrap();
|
||||
let block_root_iter = harness
|
||||
.chain
|
||||
.forwards_iter_block_roots_until(start_slot, end_slot)
|
||||
.unwrap();
|
||||
|
||||
for block in block_root_iter {
|
||||
let (block_root, _) = block.unwrap();
|
||||
let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap();
|
||||
assert!(data_columns.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
// This should verify that a data column sidecar containing mismatched block roots should fail to be imported.
|
||||
#[tokio::test]
|
||||
async fn test_import_historical_data_columns_batch_mismatched_block_root() {
|
||||
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
|
||||
let db_path = tempdir().unwrap();
|
||||
let store = get_store_generic(&db_path, StoreConfig::default(), spec);
|
||||
let start_slot = Slot::new(1);
|
||||
let end_slot = Slot::new(E::slots_per_epoch() * 2 - 1);
|
||||
|
||||
let harness = get_harness_import_all_data_columns(store.clone(), LOW_VALIDATOR_COUNT);
|
||||
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * 2) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
harness.advance_slot();
|
||||
|
||||
let block_root_iter = harness
|
||||
.chain
|
||||
.forwards_iter_block_roots_until(start_slot, end_slot)
|
||||
.unwrap();
|
||||
|
||||
let mut data_columns_list = vec![];
|
||||
|
||||
for block in block_root_iter {
|
||||
let (block_root, _) = block.unwrap();
|
||||
let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap();
|
||||
assert!(data_columns.is_some());
|
||||
|
||||
for data_column in data_columns.unwrap() {
|
||||
let mut data_column = (*data_column).clone();
|
||||
if data_column.index % 2 == 0 {
|
||||
data_column.signed_block_header.message.body_root = Hash256::ZERO;
|
||||
}
|
||||
|
||||
data_columns_list.push(Arc::new(data_column));
|
||||
}
|
||||
}
|
||||
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * 4) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
harness.advance_slot();
|
||||
|
||||
harness
|
||||
.chain
|
||||
.store
|
||||
.try_prune_blobs(true, Epoch::new(2))
|
||||
.unwrap();
|
||||
|
||||
let block_root_iter = harness
|
||||
.chain
|
||||
.forwards_iter_block_roots_until(start_slot, end_slot)
|
||||
.unwrap();
|
||||
|
||||
for block in block_root_iter {
|
||||
let (block_root, _) = block.unwrap();
|
||||
let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap();
|
||||
assert!(data_columns.is_none())
|
||||
}
|
||||
|
||||
let error = harness
|
||||
.chain
|
||||
.import_historical_data_column_batch(
|
||||
start_slot.epoch(E::slots_per_epoch()),
|
||||
data_columns_list,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
HistoricalDataColumnError::NoBlockFound { .. }
|
||||
));
|
||||
}
|
||||
|
||||
// This should verify that a data column sidecar associated to a block root that doesn't exist in the store cannot
|
||||
// be imported.
|
||||
#[tokio::test]
|
||||
async fn test_import_historical_data_columns_batch_no_block_found() {
|
||||
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
|
||||
let db_path = tempdir().unwrap();
|
||||
let store = get_store_generic(&db_path, StoreConfig::default(), spec);
|
||||
let start_slot = Slot::new(1);
|
||||
let end_slot = Slot::new(E::slots_per_epoch() * 2 - 1);
|
||||
|
||||
let harness = get_harness_import_all_data_columns(store.clone(), LOW_VALIDATOR_COUNT);
|
||||
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * 2) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
harness.advance_slot();
|
||||
|
||||
let block_root_iter = harness
|
||||
.chain
|
||||
.forwards_iter_block_roots_until(start_slot, end_slot)
|
||||
.unwrap();
|
||||
|
||||
let mut data_columns_list = vec![];
|
||||
|
||||
for block in block_root_iter {
|
||||
let (block_root, _) = block.unwrap();
|
||||
let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap();
|
||||
assert!(data_columns.is_some());
|
||||
|
||||
for data_column in data_columns.unwrap() {
|
||||
let mut data_column = (*data_column).clone();
|
||||
data_column.signed_block_header.message.body_root = Hash256::ZERO;
|
||||
data_columns_list.push(Arc::new(data_column));
|
||||
}
|
||||
}
|
||||
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * 4) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
harness.advance_slot();
|
||||
|
||||
harness
|
||||
.chain
|
||||
.store
|
||||
.try_prune_blobs(true, Epoch::new(2))
|
||||
.unwrap();
|
||||
|
||||
let block_root_iter = harness
|
||||
.chain
|
||||
.forwards_iter_block_roots_until(start_slot, end_slot)
|
||||
.unwrap();
|
||||
|
||||
for block in block_root_iter {
|
||||
let (block_root, _) = block.unwrap();
|
||||
let data_columns = harness.chain.store.get_data_columns(&block_root).unwrap();
|
||||
assert!(data_columns.is_none())
|
||||
}
|
||||
|
||||
let error = harness
|
||||
.chain
|
||||
.import_historical_data_column_batch(Epoch::new(0), data_columns_list)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
HistoricalDataColumnError::NoBlockFound { .. }
|
||||
));
|
||||
}
|
||||
|
||||
/// Test that blocks and attestations that refer to states around an unaligned split state are
|
||||
/// processed correctly.
|
||||
#[tokio::test]
|
||||
@@ -4845,6 +5086,166 @@ async fn test_custody_column_filtering_supernode() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_missing_columns_after_cgc_change() {
|
||||
let spec = test_spec::<E>();
|
||||
|
||||
let num_validators = 8;
|
||||
|
||||
let num_epochs_before_increase = 4;
|
||||
|
||||
let harness = BeaconChainHarness::builder(E::default())
|
||||
.spec(spec.clone().into())
|
||||
.deterministic_keypairs(num_validators)
|
||||
.fresh_ephemeral_store()
|
||||
.mock_execution_layer()
|
||||
.build();
|
||||
|
||||
let state = harness.chain.head_beacon_state_cloned();
|
||||
|
||||
if !state.fork_name_unchecked().fulu_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let custody_context = harness.chain.data_availability_checker.custody_context();
|
||||
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * num_epochs_before_increase) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
let epoch_before_increase = Epoch::new(num_epochs_before_increase);
|
||||
|
||||
let missing_columns = harness
|
||||
.chain
|
||||
.get_missing_columns_for_epoch(epoch_before_increase);
|
||||
|
||||
// We should have no missing columns
|
||||
assert_eq!(missing_columns.len(), 0);
|
||||
|
||||
let epoch_after_increase = Epoch::new(num_epochs_before_increase + 2);
|
||||
|
||||
let cgc_change_slot = epoch_before_increase.end_slot(E::slots_per_epoch());
|
||||
custody_context.register_validators(vec![(1, 32_000_000_000 * 9)], cgc_change_slot, &spec);
|
||||
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * 5) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
// We should have missing columns from before the cgc increase
|
||||
let missing_columns = harness
|
||||
.chain
|
||||
.get_missing_columns_for_epoch(epoch_before_increase);
|
||||
|
||||
assert!(!missing_columns.is_empty());
|
||||
|
||||
// We should have no missing columns after the cgc increase
|
||||
let missing_columns = harness
|
||||
.chain
|
||||
.get_missing_columns_for_epoch(epoch_after_increase);
|
||||
|
||||
assert!(missing_columns.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_safely_backfill_data_column_custody_info() {
|
||||
let spec = test_spec::<E>();
|
||||
|
||||
let num_validators = 8;
|
||||
|
||||
let start_epochs = 4;
|
||||
|
||||
let harness = BeaconChainHarness::builder(E::default())
|
||||
.spec(spec.clone().into())
|
||||
.deterministic_keypairs(num_validators)
|
||||
.fresh_ephemeral_store()
|
||||
.mock_execution_layer()
|
||||
.build();
|
||||
|
||||
let state = harness.chain.head_beacon_state_cloned();
|
||||
|
||||
if !state.fork_name_unchecked().fulu_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let custody_context = harness.chain.data_availability_checker.custody_context();
|
||||
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * start_epochs) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
let epoch_before_increase = Epoch::new(start_epochs);
|
||||
let effective_delay_slots =
|
||||
CUSTODY_CHANGE_DA_EFFECTIVE_DELAY_SECONDS / harness.chain.spec.seconds_per_slot;
|
||||
|
||||
let cgc_change_slot = epoch_before_increase.end_slot(E::slots_per_epoch());
|
||||
|
||||
custody_context.register_validators(vec![(1, 32_000_000_000 * 16)], cgc_change_slot, &spec);
|
||||
|
||||
let epoch_after_increase =
|
||||
(cgc_change_slot + effective_delay_slots).epoch(E::slots_per_epoch());
|
||||
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain(
|
||||
(E::slots_per_epoch() * 5) as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
)
|
||||
.await;
|
||||
|
||||
let head_slot = harness.chain.head().snapshot.beacon_block.slot();
|
||||
|
||||
harness
|
||||
.chain
|
||||
.update_data_column_custody_info(Some(head_slot));
|
||||
|
||||
// We can only safely update custody column info 1 epoch at a time
|
||||
// Skipping an epoch should return an error
|
||||
harness
|
||||
.chain
|
||||
.safely_backfill_data_column_custody_info(head_slot.epoch(E::slots_per_epoch()) - 2)
|
||||
.unwrap_err();
|
||||
|
||||
// Iterate from the head epoch back to 0 and try to backfill data column custody info
|
||||
for epoch in (0..head_slot.epoch(E::slots_per_epoch()).into()).rev() {
|
||||
// This is an epoch before the cgc change took into effect, we shouldnt be able to update
|
||||
// without performing custody backfill sync
|
||||
if epoch <= epoch_after_increase.into() {
|
||||
harness
|
||||
.chain
|
||||
.safely_backfill_data_column_custody_info(Epoch::new(epoch))
|
||||
.unwrap_err();
|
||||
} else {
|
||||
// This is an epoch after the cgc change took into effect, we should be able to update
|
||||
// as long as we iterate epoch by epoch
|
||||
harness
|
||||
.chain
|
||||
.safely_backfill_data_column_custody_info(Epoch::new(epoch))
|
||||
.unwrap();
|
||||
let earliest_available_epoch = harness
|
||||
.chain
|
||||
.earliest_custodied_data_column_epoch()
|
||||
.unwrap();
|
||||
assert_eq!(Epoch::new(epoch), earliest_available_epoch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that two chains are the same, for the purpose of these tests.
|
||||
///
|
||||
/// Several fields that are hard/impossible to check are ignored (e.g., the store).
|
||||
|
||||
Reference in New Issue
Block a user