mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-08 17:26:04 +00:00
Implement proposer duties v2 endpoint (#8918)
Fix the issue with the `proposer_duties` endpoint using the wrong dependent root post-Fulu by implementing the new v2 endpoint: - https://github.com/ethereum/beacon-APIs/pull/563 We need to add this in time for Gloas, and then we can we can deprecate and remove v1. - Add a new API handler for the v2 endpoint - Add client code in the `eth2` crate - Update existing tests and add some new ones to confirm the different behaviour of v1 and v2 There's a bit of test duplication with v1, but this will be resolved once v1 and its tests are deleted. Co-Authored-By: Michael Sproul <michael@sigmaprime.io> Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com> Co-Authored-By: chonghe <44791194+chong-he@users.noreply.github.com>
This commit is contained in:
@@ -263,6 +263,7 @@ pub fn prometheus_metrics() -> warp::filters::log::Log<impl Fn(warp::filters::lo
|
||||
.or_else(|| starts_with("v1/validator/contribution_and_proofs"))
|
||||
.or_else(|| starts_with("v1/validator/duties/attester"))
|
||||
.or_else(|| starts_with("v1/validator/duties/proposer"))
|
||||
.or_else(|| starts_with("v2/validator/duties/proposer"))
|
||||
.or_else(|| starts_with("v1/validator/duties/sync"))
|
||||
.or_else(|| starts_with("v1/validator/liveness"))
|
||||
.or_else(|| starts_with("v1/validator/prepare_beacon_proposer"))
|
||||
@@ -2464,7 +2465,7 @@ pub fn serve<T: BeaconChainTypes>(
|
||||
|
||||
// GET validator/duties/proposer/{epoch}
|
||||
let get_validator_duties_proposer = get_validator_duties_proposer(
|
||||
eth_v1.clone(),
|
||||
any_version.clone(),
|
||||
chain_filter.clone(),
|
||||
not_while_syncing_filter.clone(),
|
||||
task_spawner_filter.clone(),
|
||||
|
||||
@@ -13,13 +13,45 @@ use slot_clock::SlotClock;
|
||||
use tracing::debug;
|
||||
use types::{Epoch, EthSpec, Hash256, Slot};
|
||||
|
||||
/// Selects which dependent root to return in the API response.
|
||||
///
|
||||
/// - `Legacy`: the block root at the last slot of epoch N-1 (v1 behaviour, for backwards compat).
|
||||
/// - `True`: the fork-aware proposer shuffling decision root (v2 behaviour). Pre-Fulu this equals
|
||||
/// the legacy root; post-Fulu it uses epoch N-2.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum DependentRootSelection {
|
||||
Legacy,
|
||||
True,
|
||||
}
|
||||
|
||||
/// The struct that is returned to the requesting HTTP client.
|
||||
type ApiDuties = api_types::DutiesResponse<Vec<api_types::ProposerData>>;
|
||||
|
||||
/// Handles a request from the HTTP API for proposer duties.
|
||||
/// Handles a request from the HTTP API for v1 proposer duties.
|
||||
///
|
||||
/// Returns the legacy dependent root (block root at end of epoch N-1) for backwards compatibility.
|
||||
pub fn proposer_duties<T: BeaconChainTypes>(
|
||||
request_epoch: Epoch,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<ApiDuties, warp::reject::Rejection> {
|
||||
proposer_duties_internal(request_epoch, chain, DependentRootSelection::Legacy)
|
||||
}
|
||||
|
||||
/// Handles a request from the HTTP API for v2 proposer duties.
|
||||
///
|
||||
/// Returns the true fork-aware dependent root. Pre-Fulu this equals the legacy root; post-Fulu it
|
||||
/// uses epoch N-2 due to deterministic proposer lookahead with `min_seed_lookahead`.
|
||||
pub fn proposer_duties_v2<T: BeaconChainTypes>(
|
||||
request_epoch: Epoch,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<ApiDuties, warp::reject::Rejection> {
|
||||
proposer_duties_internal(request_epoch, chain, DependentRootSelection::True)
|
||||
}
|
||||
|
||||
fn proposer_duties_internal<T: BeaconChainTypes>(
|
||||
request_epoch: Epoch,
|
||||
chain: &BeaconChain<T>,
|
||||
root_selection: DependentRootSelection,
|
||||
) -> Result<ApiDuties, warp::reject::Rejection> {
|
||||
let current_epoch = chain
|
||||
.slot_clock
|
||||
@@ -49,24 +81,29 @@ pub fn proposer_duties<T: BeaconChainTypes>(
|
||||
if request_epoch == current_epoch || request_epoch == tolerant_current_epoch {
|
||||
// If we could consider ourselves in the `request_epoch` when allowing for clock disparity
|
||||
// tolerance then serve this request from the cache.
|
||||
if let Some(duties) = try_proposer_duties_from_cache(request_epoch, chain)? {
|
||||
if let Some(duties) = try_proposer_duties_from_cache(request_epoch, chain, root_selection)?
|
||||
{
|
||||
Ok(duties)
|
||||
} else {
|
||||
debug!(%request_epoch, "Proposer cache miss");
|
||||
compute_and_cache_proposer_duties(request_epoch, chain)
|
||||
compute_and_cache_proposer_duties(request_epoch, chain, root_selection)
|
||||
}
|
||||
} else if request_epoch
|
||||
== current_epoch
|
||||
.safe_add(1)
|
||||
.map_err(warp_utils::reject::arith_error)?
|
||||
{
|
||||
let (proposers, _dependent_root, legacy_dependent_root, execution_status, _fork) =
|
||||
let (proposers, dependent_root, legacy_dependent_root, execution_status, _fork) =
|
||||
compute_proposer_duties_from_head(request_epoch, chain)
|
||||
.map_err(warp_utils::reject::unhandled_error)?;
|
||||
let selected_root = match root_selection {
|
||||
DependentRootSelection::Legacy => legacy_dependent_root,
|
||||
DependentRootSelection::True => dependent_root,
|
||||
};
|
||||
convert_to_api_response(
|
||||
chain,
|
||||
request_epoch,
|
||||
legacy_dependent_root,
|
||||
selected_root,
|
||||
execution_status.is_optimistic_or_invalid(),
|
||||
proposers,
|
||||
)
|
||||
@@ -84,7 +121,7 @@ pub fn proposer_duties<T: BeaconChainTypes>(
|
||||
// request_epoch < current_epoch
|
||||
//
|
||||
// Queries about the past are handled with a slow path.
|
||||
compute_historic_proposer_duties(request_epoch, chain)
|
||||
compute_historic_proposer_duties(request_epoch, chain, root_selection)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +135,7 @@ pub fn proposer_duties<T: BeaconChainTypes>(
|
||||
fn try_proposer_duties_from_cache<T: BeaconChainTypes>(
|
||||
request_epoch: Epoch,
|
||||
chain: &BeaconChain<T>,
|
||||
root_selection: DependentRootSelection,
|
||||
) -> Result<Option<ApiDuties>, warp::reject::Rejection> {
|
||||
let head = chain.canonical_head.cached_head();
|
||||
let head_block = &head.snapshot.beacon_block;
|
||||
@@ -116,11 +154,14 @@ fn try_proposer_duties_from_cache<T: BeaconChainTypes>(
|
||||
.beacon_state
|
||||
.proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root, &chain.spec)
|
||||
.map_err(warp_utils::reject::beacon_state_error)?;
|
||||
let legacy_dependent_root = head
|
||||
.snapshot
|
||||
.beacon_state
|
||||
.legacy_proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root)
|
||||
.map_err(warp_utils::reject::beacon_state_error)?;
|
||||
let selected_root = match root_selection {
|
||||
DependentRootSelection::Legacy => head
|
||||
.snapshot
|
||||
.beacon_state
|
||||
.legacy_proposer_shuffling_decision_root_at_epoch(request_epoch, head_block_root)
|
||||
.map_err(warp_utils::reject::beacon_state_error)?,
|
||||
DependentRootSelection::True => head_decision_root,
|
||||
};
|
||||
let execution_optimistic = chain
|
||||
.is_optimistic_or_invalid_head_block(head_block)
|
||||
.map_err(warp_utils::reject::unhandled_error)?;
|
||||
@@ -134,7 +175,7 @@ fn try_proposer_duties_from_cache<T: BeaconChainTypes>(
|
||||
convert_to_api_response(
|
||||
chain,
|
||||
request_epoch,
|
||||
legacy_dependent_root,
|
||||
selected_root,
|
||||
execution_optimistic,
|
||||
indices.to_vec(),
|
||||
)
|
||||
@@ -155,6 +196,7 @@ fn try_proposer_duties_from_cache<T: BeaconChainTypes>(
|
||||
fn compute_and_cache_proposer_duties<T: BeaconChainTypes>(
|
||||
current_epoch: Epoch,
|
||||
chain: &BeaconChain<T>,
|
||||
root_selection: DependentRootSelection,
|
||||
) -> Result<ApiDuties, warp::reject::Rejection> {
|
||||
let (indices, dependent_root, legacy_dependent_root, execution_status, fork) =
|
||||
compute_proposer_duties_from_head(current_epoch, chain)
|
||||
@@ -168,10 +210,14 @@ fn compute_and_cache_proposer_duties<T: BeaconChainTypes>(
|
||||
.map_err(BeaconChainError::from)
|
||||
.map_err(warp_utils::reject::unhandled_error)?;
|
||||
|
||||
let selected_root = match root_selection {
|
||||
DependentRootSelection::Legacy => legacy_dependent_root,
|
||||
DependentRootSelection::True => dependent_root,
|
||||
};
|
||||
convert_to_api_response(
|
||||
chain,
|
||||
current_epoch,
|
||||
legacy_dependent_root,
|
||||
selected_root,
|
||||
execution_status.is_optimistic_or_invalid(),
|
||||
indices,
|
||||
)
|
||||
@@ -182,6 +228,7 @@ fn compute_and_cache_proposer_duties<T: BeaconChainTypes>(
|
||||
fn compute_historic_proposer_duties<T: BeaconChainTypes>(
|
||||
epoch: Epoch,
|
||||
chain: &BeaconChain<T>,
|
||||
root_selection: DependentRootSelection,
|
||||
) -> Result<ApiDuties, warp::reject::Rejection> {
|
||||
// If the head is quite old then it might still be relevant for a historical request.
|
||||
//
|
||||
@@ -219,9 +266,9 @@ fn compute_historic_proposer_duties<T: BeaconChainTypes>(
|
||||
};
|
||||
|
||||
// Ensure the state lookup was correct.
|
||||
if state.current_epoch() != epoch {
|
||||
if state.current_epoch() != epoch && state.current_epoch() + 1 != epoch {
|
||||
return Err(warp_utils::reject::custom_server_error(format!(
|
||||
"state epoch {} not equal to request epoch {}",
|
||||
"state from epoch {} cannot serve request epoch {}",
|
||||
state.current_epoch(),
|
||||
epoch
|
||||
)));
|
||||
@@ -234,18 +281,18 @@ fn compute_historic_proposer_duties<T: BeaconChainTypes>(
|
||||
|
||||
// We can supply the genesis block root as the block root since we know that the only block that
|
||||
// decides its own root is the genesis block.
|
||||
let legacy_dependent_root = state
|
||||
.legacy_proposer_shuffling_decision_root_at_epoch(epoch, chain.genesis_block_root)
|
||||
.map_err(BeaconChainError::from)
|
||||
.map_err(warp_utils::reject::unhandled_error)?;
|
||||
let selected_root = match root_selection {
|
||||
DependentRootSelection::Legacy => state
|
||||
.legacy_proposer_shuffling_decision_root_at_epoch(epoch, chain.genesis_block_root)
|
||||
.map_err(BeaconChainError::from)
|
||||
.map_err(warp_utils::reject::unhandled_error)?,
|
||||
DependentRootSelection::True => state
|
||||
.proposer_shuffling_decision_root_at_epoch(epoch, chain.genesis_block_root, &chain.spec)
|
||||
.map_err(BeaconChainError::from)
|
||||
.map_err(warp_utils::reject::unhandled_error)?,
|
||||
};
|
||||
|
||||
convert_to_api_response(
|
||||
chain,
|
||||
epoch,
|
||||
legacy_dependent_root,
|
||||
execution_optimistic,
|
||||
indices,
|
||||
)
|
||||
convert_to_api_response(chain, epoch, selected_root, execution_optimistic, indices)
|
||||
}
|
||||
|
||||
/// Converts the internal representation of proposer duties into one that is compatible with the
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::utils::{
|
||||
AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter,
|
||||
ResponseFilter, TaskSpawnerFilter, ValidatorSubscriptionTxFilter, publish_network_message,
|
||||
};
|
||||
use crate::version::V3;
|
||||
use crate::version::{V1, V2, V3, unsupported_version_rejection};
|
||||
use crate::{StateId, attester_duties, proposer_duties, sync_committees};
|
||||
use beacon_chain::attestation_verification::VerifiedAttestation;
|
||||
use beacon_chain::validator_monitor::timestamp_now;
|
||||
@@ -971,12 +971,12 @@ pub fn post_validator_aggregate_and_proofs<T: BeaconChainTypes>(
|
||||
|
||||
// GET validator/duties/proposer/{epoch}
|
||||
pub fn get_validator_duties_proposer<T: BeaconChainTypes>(
|
||||
eth_v1: EthV1Filter,
|
||||
any_version: AnyVersionFilter,
|
||||
chain_filter: ChainFilter<T>,
|
||||
not_while_syncing_filter: NotWhileSyncingFilter,
|
||||
task_spawner_filter: TaskSpawnerFilter<T>,
|
||||
) -> ResponseFilter {
|
||||
eth_v1
|
||||
any_version
|
||||
.and(warp::path("validator"))
|
||||
.and(warp::path("duties"))
|
||||
.and(warp::path("proposer"))
|
||||
@@ -990,13 +990,20 @@ pub fn get_validator_duties_proposer<T: BeaconChainTypes>(
|
||||
.and(task_spawner_filter)
|
||||
.and(chain_filter)
|
||||
.then(
|
||||
|epoch: Epoch,
|
||||
|endpoint_version: EndpointVersion,
|
||||
epoch: Epoch,
|
||||
not_synced_filter: Result<(), Rejection>,
|
||||
task_spawner: TaskSpawner<T::EthSpec>,
|
||||
chain: Arc<BeaconChain<T>>| {
|
||||
task_spawner.blocking_json_task(Priority::P0, move || {
|
||||
not_synced_filter?;
|
||||
proposer_duties::proposer_duties(epoch, &chain)
|
||||
if endpoint_version == V1 {
|
||||
proposer_duties::proposer_duties(epoch, &chain)
|
||||
} else if endpoint_version == V2 {
|
||||
proposer_duties::proposer_duties_v2(epoch, &chain)
|
||||
} else {
|
||||
Err(unsupported_version_rejection(endpoint_version))
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1053,6 +1053,240 @@ async fn proposer_duties_with_gossip_tolerance() {
|
||||
);
|
||||
}
|
||||
|
||||
// Test that a request for next epoch v2 proposer duties succeeds when the current slot clock is
|
||||
// within gossip clock disparity (500ms) of the new epoch.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn proposer_duties_v2_with_gossip_tolerance() {
|
||||
let validator_count = 24;
|
||||
|
||||
let tester = InteractiveTester::<E>::new(None, validator_count).await;
|
||||
let harness = &tester.harness;
|
||||
let spec = &harness.spec;
|
||||
let client = &tester.client;
|
||||
|
||||
let num_initial = 4 * E::slots_per_epoch() - 1;
|
||||
let next_epoch_start_slot = Slot::new(num_initial + 1);
|
||||
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain_with_sync(
|
||||
num_initial as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
SyncCommitteeStrategy::NoValidators,
|
||||
LightClientStrategy::Disabled,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(harness.chain.slot().unwrap(), num_initial);
|
||||
|
||||
// Set the clock to just before the next epoch.
|
||||
harness.chain.slot_clock.advance_time(
|
||||
Duration::from_secs(spec.seconds_per_slot) - spec.maximum_gossip_clock_disparity(),
|
||||
);
|
||||
assert_eq!(
|
||||
harness
|
||||
.chain
|
||||
.slot_clock
|
||||
.now_with_future_tolerance(spec.maximum_gossip_clock_disparity())
|
||||
.unwrap(),
|
||||
next_epoch_start_slot
|
||||
);
|
||||
|
||||
let head_state = harness.get_current_state();
|
||||
let head_block_root = harness.head_block_root();
|
||||
let tolerant_current_epoch = next_epoch_start_slot.epoch(E::slots_per_epoch());
|
||||
|
||||
// Prime the proposer shuffling cache with an incorrect entry (regression test).
|
||||
let wrong_decision_root = head_state
|
||||
.proposer_shuffling_decision_root(head_block_root, spec)
|
||||
.unwrap();
|
||||
let wrong_proposer_indices = vec![0; E::slots_per_epoch() as usize];
|
||||
harness
|
||||
.chain
|
||||
.beacon_proposer_cache
|
||||
.lock()
|
||||
.insert(
|
||||
tolerant_current_epoch,
|
||||
wrong_decision_root,
|
||||
wrong_proposer_indices.clone(),
|
||||
head_state.fork(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Request the v2 proposer duties.
|
||||
let proposer_duties_tolerant_current_epoch = client
|
||||
.get_validator_duties_proposer_v2(tolerant_current_epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
proposer_duties_tolerant_current_epoch.dependent_root,
|
||||
head_state
|
||||
.proposer_shuffling_decision_root_at_epoch(
|
||||
tolerant_current_epoch,
|
||||
head_block_root,
|
||||
spec,
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
assert_ne!(
|
||||
proposer_duties_tolerant_current_epoch
|
||||
.data
|
||||
.iter()
|
||||
.map(|data| data.validator_index as usize)
|
||||
.collect::<Vec<_>>(),
|
||||
wrong_proposer_indices,
|
||||
);
|
||||
|
||||
// We should get the exact same result after properly advancing into the epoch.
|
||||
harness
|
||||
.chain
|
||||
.slot_clock
|
||||
.advance_time(spec.maximum_gossip_clock_disparity());
|
||||
assert_eq!(harness.chain.slot().unwrap(), next_epoch_start_slot);
|
||||
let proposer_duties_current_epoch = client
|
||||
.get_validator_duties_proposer_v2(tolerant_current_epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
proposer_duties_tolerant_current_epoch,
|
||||
proposer_duties_current_epoch
|
||||
);
|
||||
}
|
||||
|
||||
// Test that post-Fulu, v1 and v2 proposer duties return different dependent roots.
|
||||
// Post-Fulu, the true dependent root shifts to the block root at the end of epoch N-2 (due to
|
||||
// `min_seed_lookahead`), while the legacy v1 root remains at the end of epoch N-1.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn proposer_duties_v2_post_fulu_dependent_root() {
|
||||
type E = MinimalEthSpec;
|
||||
let spec = test_spec::<E>();
|
||||
|
||||
if !spec.is_fulu_scheduled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let validator_count = 24;
|
||||
let slots_per_epoch = E::slots_per_epoch();
|
||||
|
||||
let tester = InteractiveTester::<E>::new(Some(spec.clone()), validator_count).await;
|
||||
let harness = &tester.harness;
|
||||
let client = &tester.client;
|
||||
let mock_el = harness.mock_execution_layer.as_ref().unwrap();
|
||||
mock_el.server.all_payloads_valid();
|
||||
|
||||
// Build 3 full epochs of chain so we're in epoch 3.
|
||||
let num_slots = 3 * slots_per_epoch;
|
||||
harness.advance_slot();
|
||||
harness
|
||||
.extend_chain_with_sync(
|
||||
num_slots as usize,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
SyncCommitteeStrategy::AllValidators,
|
||||
LightClientStrategy::Disabled,
|
||||
)
|
||||
.await;
|
||||
|
||||
let current_epoch = harness.chain.epoch().unwrap();
|
||||
assert_eq!(current_epoch, Epoch::new(3));
|
||||
|
||||
// For epoch 3 with min_seed_lookahead=1:
|
||||
// Post-Fulu decision slot: end of epoch N-2 = end of epoch 1 = slot 15
|
||||
// Legacy decision slot: end of epoch N-1 = end of epoch 2 = slot 23
|
||||
let true_decision_slot = Epoch::new(1).end_slot(slots_per_epoch);
|
||||
let legacy_decision_slot = Epoch::new(2).end_slot(slots_per_epoch);
|
||||
assert_eq!(true_decision_slot, Slot::new(15));
|
||||
assert_eq!(legacy_decision_slot, Slot::new(23));
|
||||
|
||||
// Fetch the block roots at these slots to compute expected dependent roots.
|
||||
let expected_v2_root = harness
|
||||
.chain
|
||||
.block_root_at_slot(true_decision_slot, beacon_chain::WhenSlotSkipped::Prev)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let expected_v1_root = harness
|
||||
.chain
|
||||
.block_root_at_slot(legacy_decision_slot, beacon_chain::WhenSlotSkipped::Prev)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Sanity check: the two roots should be different since they refer to different blocks.
|
||||
assert_ne!(
|
||||
expected_v1_root, expected_v2_root,
|
||||
"legacy and true decision roots should differ post-Fulu"
|
||||
);
|
||||
|
||||
// Query v1 and v2 proposer duties for the current epoch.
|
||||
let v1_result = client
|
||||
.get_validator_duties_proposer(current_epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
let v2_result = client
|
||||
.get_validator_duties_proposer_v2(current_epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// The proposer assignments (data) must be identical.
|
||||
assert_eq!(v1_result.data, v2_result.data);
|
||||
|
||||
// The dependent roots must differ.
|
||||
assert_ne!(
|
||||
v1_result.dependent_root, v2_result.dependent_root,
|
||||
"v1 and v2 dependent roots should differ post-Fulu"
|
||||
);
|
||||
|
||||
// Verify each root matches the expected value.
|
||||
assert_eq!(
|
||||
v1_result.dependent_root, expected_v1_root,
|
||||
"v1 dependent root should be block root at end of epoch N-1"
|
||||
);
|
||||
assert_eq!(
|
||||
v2_result.dependent_root, expected_v2_root,
|
||||
"v2 dependent root should be block root at end of epoch N-2"
|
||||
);
|
||||
|
||||
// Also verify the next-epoch path (epoch 4).
|
||||
let next_epoch = current_epoch + 1;
|
||||
let v1_next = client
|
||||
.get_validator_duties_proposer(next_epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
let v2_next = client
|
||||
.get_validator_duties_proposer_v2(next_epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(v1_next.data, v2_next.data);
|
||||
assert_ne!(
|
||||
v1_next.dependent_root, v2_next.dependent_root,
|
||||
"v1 and v2 next-epoch dependent roots should differ post-Fulu"
|
||||
);
|
||||
|
||||
// For epoch 4: true decision is end of epoch 2 (slot 23), legacy is end of epoch 3 (slot 31).
|
||||
let expected_v2_next_root = harness
|
||||
.chain
|
||||
.block_root_at_slot(
|
||||
Epoch::new(2).end_slot(slots_per_epoch),
|
||||
beacon_chain::WhenSlotSkipped::Prev,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let expected_v1_next_root = harness
|
||||
.chain
|
||||
.block_root_at_slot(
|
||||
Epoch::new(3).end_slot(slots_per_epoch),
|
||||
beacon_chain::WhenSlotSkipped::Prev,
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap_or(harness.head_block_root());
|
||||
assert_eq!(v1_next.dependent_root, expected_v1_next_root);
|
||||
assert_eq!(v2_next.dependent_root, expected_v2_next_root);
|
||||
assert_ne!(expected_v2_next_root, harness.head_block_root());
|
||||
}
|
||||
|
||||
// Test that a request to `lighthouse/custody/backfill` succeeds by verifying that `CustodyContext` and `DataColumnCustodyInfo`
|
||||
// have been updated with the correct values.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -3392,6 +3392,80 @@ impl ApiTester {
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_validator_duties_proposer_v2(self) -> Self {
|
||||
let current_epoch = self.chain.epoch().unwrap();
|
||||
|
||||
for epoch in 0..=current_epoch.as_u64() + 1 {
|
||||
let epoch = Epoch::from(epoch);
|
||||
|
||||
// Compute the true dependent root using the spec's decision slot.
|
||||
let decision_slot = self.chain.spec.proposer_shuffling_decision_slot::<E>(epoch);
|
||||
let dependent_root = self
|
||||
.chain
|
||||
.block_root_at_slot(decision_slot, WhenSlotSkipped::Prev)
|
||||
.unwrap()
|
||||
.unwrap_or(self.chain.head_beacon_block_root());
|
||||
|
||||
let result = self
|
||||
.client
|
||||
.get_validator_duties_proposer_v2(epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut state = self
|
||||
.chain
|
||||
.state_at_slot(
|
||||
epoch.start_slot(E::slots_per_epoch()),
|
||||
StateSkipConfig::WithStateRoots,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
state
|
||||
.build_committee_cache(RelativeEpoch::Current, &self.chain.spec)
|
||||
.unwrap();
|
||||
|
||||
let expected_duties = epoch
|
||||
.slot_iter(E::slots_per_epoch())
|
||||
.map(|slot| {
|
||||
let index = state
|
||||
.get_beacon_proposer_index(slot, &self.chain.spec)
|
||||
.unwrap();
|
||||
let pubkey = state.validators().get(index).unwrap().pubkey;
|
||||
|
||||
ProposerData {
|
||||
pubkey,
|
||||
validator_index: index as u64,
|
||||
slot,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = DutiesResponse {
|
||||
data: expected_duties,
|
||||
execution_optimistic: Some(false),
|
||||
dependent_root,
|
||||
};
|
||||
|
||||
assert_eq!(result, expected);
|
||||
|
||||
// v1 and v2 should return the same data.
|
||||
let v1_result = self
|
||||
.client
|
||||
.get_validator_duties_proposer(epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.data, v1_result.data);
|
||||
}
|
||||
|
||||
// Requests to the epochs after the next epoch should fail.
|
||||
self.client
|
||||
.get_validator_duties_proposer_v2(current_epoch + 2)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn test_get_validator_duties_early(self) -> Self {
|
||||
let current_epoch = self.chain.epoch().unwrap();
|
||||
let next_epoch = current_epoch + 1;
|
||||
@@ -7617,6 +7691,31 @@ async fn get_validator_duties_proposer_with_skip_slots() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_validator_duties_proposer_v2() {
|
||||
ApiTester::new_from_config(ApiTesterConfig {
|
||||
spec: test_spec::<E>(),
|
||||
retain_historic_states: true,
|
||||
..ApiTesterConfig::default()
|
||||
})
|
||||
.await
|
||||
.test_get_validator_duties_proposer_v2()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn get_validator_duties_proposer_v2_with_skip_slots() {
|
||||
ApiTester::new_from_config(ApiTesterConfig {
|
||||
spec: test_spec::<E>(),
|
||||
retain_historic_states: true,
|
||||
..ApiTesterConfig::default()
|
||||
})
|
||||
.await
|
||||
.skip_slots(E::slots_per_epoch() * 2)
|
||||
.test_get_validator_duties_proposer_v2()
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn block_production() {
|
||||
ApiTester::new().await.test_block_production().await;
|
||||
|
||||
@@ -2144,6 +2144,24 @@ impl BeaconNodeHttpClient {
|
||||
.await
|
||||
}
|
||||
|
||||
/// `GET v2/validator/duties/proposer/{epoch}`
|
||||
pub async fn get_validator_duties_proposer_v2(
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
) -> Result<DutiesResponse<Vec<ProposerData>>, Error> {
|
||||
let mut path = self.eth_path(V2)?;
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("validator")
|
||||
.push("duties")
|
||||
.push("proposer")
|
||||
.push(&epoch.to_string());
|
||||
|
||||
self.get_with_timeout(path, self.timeouts.proposer_duties)
|
||||
.await
|
||||
}
|
||||
|
||||
/// `GET v2/validator/blocks/{slot}`
|
||||
pub async fn get_validator_blocks<E: EthSpec>(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user