mirror of
https://github.com/sigp/lighthouse.git
synced 2026-07-02 04:14:33 +00:00
Fix transient bug in dequeue_attestation and optimization (#9524)
`dequeue_attestations` released votes by splitting the queue at the first entry with `slot >= current_slot`, which assumes the queue is sorted by slot. It isn't: `on_attestation` pushes attestations in arrival order and never sorts. When a future-slot vote sits ahead of a vote that is already due, the split happens at the future-slot vote and the due vote stays stuck behind it and is never applied to fork choice, even after its slot is in the past. The PR current uses a naive solution to solve the bug and also adds regression tests to exercise the bug. There are other competing solutions which can be used which also optimize this path at the same time. https://github.com/sigp/lighthouse/pull/8378 https://github.com/sigp/lighthouse/pull/8378#discussion_r2543322106 Co-Authored-By: hopinheimer <knmanas6@gmail.com> Co-Authored-By: hopinheimer <48147533+hopinheimer@users.noreply.github.com> Co-Authored-By: Michael Sproul <michael@sigmaprime.io>
This commit is contained in:
@@ -149,13 +149,17 @@ impl ForkChoiceTest {
|
||||
.fork_choice_write_lock()
|
||||
.update_time(self.harness.chain.slot().unwrap())
|
||||
.unwrap();
|
||||
func(
|
||||
self.harness
|
||||
.chain
|
||||
.canonical_head
|
||||
.fork_choice_read_lock()
|
||||
.queued_attestations(),
|
||||
);
|
||||
let queued = self
|
||||
.harness
|
||||
.chain
|
||||
.canonical_head
|
||||
.fork_choice_read_lock()
|
||||
.queued_attestations()
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
func(&queued);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -415,6 +419,24 @@ impl ForkChoiceTest {
|
||||
async fn apply_attestation_to_chain<F, G>(
|
||||
self,
|
||||
delay: MutationDelay,
|
||||
mutation_func: F,
|
||||
comparison_func: G,
|
||||
) -> Self
|
||||
where
|
||||
F: FnMut(&mut IndexedAttestation<E>, &BeaconChain<EphemeralHarnessType<E>>),
|
||||
G: FnMut(Result<(), BeaconChainError>),
|
||||
{
|
||||
self.apply_nth_attestation_to_chain(0, delay, mutation_func, comparison_func)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Like `apply_attestation_to_chain`, but attests with the validator at
|
||||
/// `validator_index_in_committee` within the committee. Lets a test enqueue multiple distinct
|
||||
/// votes for the same slot without tripping `PriorAttestationKnown`.
|
||||
async fn apply_nth_attestation_to_chain<F, G>(
|
||||
self,
|
||||
validator_index_in_committee: usize,
|
||||
delay: MutationDelay,
|
||||
mut mutation_func: F,
|
||||
mut comparison_func: G,
|
||||
) -> Self
|
||||
@@ -431,18 +453,16 @@ impl ForkChoiceTest {
|
||||
.produce_unaggregated_attestation(current_slot, 0)
|
||||
.expect("should not error while producing attestation");
|
||||
|
||||
let validator_committee_index = 0;
|
||||
// For these tests we always use committee index 0, which also matches the "dummy" committee
|
||||
// index used post-Electra.
|
||||
let committee_index = 0;
|
||||
|
||||
let validator_index = *head
|
||||
.beacon_state
|
||||
.get_beacon_committee(
|
||||
current_slot,
|
||||
attestation
|
||||
.committee_index()
|
||||
.expect("should get committee index"),
|
||||
)
|
||||
.get_beacon_committee(current_slot, committee_index)
|
||||
.expect("should get committees")
|
||||
.committee
|
||||
.get(validator_committee_index)
|
||||
.get(validator_index_in_committee)
|
||||
.expect("there should be an attesting validator");
|
||||
|
||||
let committee_count = head
|
||||
@@ -452,7 +472,7 @@ impl ForkChoiceTest {
|
||||
|
||||
let subnet_id = SubnetId::compute_subnet::<E>(
|
||||
current_slot,
|
||||
0,
|
||||
committee_index,
|
||||
committee_count,
|
||||
&self.harness.chain.spec,
|
||||
)
|
||||
@@ -463,7 +483,7 @@ impl ForkChoiceTest {
|
||||
attestation
|
||||
.sign(
|
||||
&validator_sk,
|
||||
validator_committee_index,
|
||||
committee_index as usize,
|
||||
&head.beacon_state.fork(),
|
||||
self.harness.chain.genesis_validators_root,
|
||||
&self.harness.chain.spec,
|
||||
@@ -472,7 +492,7 @@ impl ForkChoiceTest {
|
||||
|
||||
let single_attestation = SingleAttestation {
|
||||
attester_index: validator_index as u64,
|
||||
committee_index: validator_committee_index as u64,
|
||||
committee_index,
|
||||
data: attestation.data().clone(),
|
||||
signature: attestation.signature().clone(),
|
||||
};
|
||||
@@ -1039,6 +1059,100 @@ async fn invalid_attestation_delayed_slot() {
|
||||
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0));
|
||||
}
|
||||
|
||||
/// Regression test for dequeuing when votes for two different future slots are queued.
|
||||
///
|
||||
/// With votes queued for consecutive slots, advancing the clock past the earlier one must release
|
||||
/// only that vote and leave the later one queued until its own slot is in the past.
|
||||
#[tokio::test]
|
||||
async fn dequeue_attestations_consecutive_slot_divergence() {
|
||||
ForkChoiceTest::new()
|
||||
.apply_blocks_without_new_attestations(1)
|
||||
.await
|
||||
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0))
|
||||
// Queue a vote for `slot + 2`.
|
||||
.apply_nth_attestation_to_chain(
|
||||
0,
|
||||
MutationDelay::NoDelay,
|
||||
|attestation, _| {
|
||||
let slot = attestation.data().slot;
|
||||
attestation.data_mut().slot = slot + 2;
|
||||
},
|
||||
|result| assert!(result.is_ok()),
|
||||
)
|
||||
.await
|
||||
// Queue a vote for `slot + 1`, which becomes due sooner.
|
||||
// A different committee position avoids `PriorAttestationKnown`.
|
||||
.apply_nth_attestation_to_chain(
|
||||
1,
|
||||
MutationDelay::NoDelay,
|
||||
|attestation, _| {
|
||||
let slot = attestation.data().slot;
|
||||
attestation.data_mut().slot = slot + 1;
|
||||
},
|
||||
|result| assert!(result.is_ok()),
|
||||
)
|
||||
.await
|
||||
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 2))
|
||||
// Advance so the slot+1 vote is due (in the past) but the slot+2 vote is not yet.
|
||||
.skip_slots(2)
|
||||
.inspect_queued_attestations(|queue| {
|
||||
assert_eq!(
|
||||
queue.len(),
|
||||
1,
|
||||
"only the due slot+1 vote should be dequeued"
|
||||
);
|
||||
assert_eq!(
|
||||
queue[0].slot,
|
||||
Slot::new(3),
|
||||
"the surviving vote must be the not-yet-due slot+2 vote"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Companion to `dequeue_attestations_consecutive_slot_divergence`: votes for two different slots
|
||||
/// are queued, but the clock is advanced far enough that *both* are due at dequeue time.
|
||||
///
|
||||
/// When every queued vote is in the past, the whole queue drains in a single dequeue.
|
||||
#[tokio::test]
|
||||
async fn dequeue_attestations_conciliation() {
|
||||
ForkChoiceTest::new()
|
||||
.apply_blocks_without_new_attestations(1)
|
||||
.await
|
||||
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 0))
|
||||
// Queue a vote for `slot + 2`.
|
||||
.apply_nth_attestation_to_chain(
|
||||
0,
|
||||
MutationDelay::NoDelay,
|
||||
|attestation, _| {
|
||||
let slot = attestation.data().slot;
|
||||
attestation.data_mut().slot = slot + 2;
|
||||
},
|
||||
|result| assert!(result.is_ok()),
|
||||
)
|
||||
.await
|
||||
// Queue a vote for `slot + 1`.
|
||||
.apply_nth_attestation_to_chain(
|
||||
1,
|
||||
MutationDelay::NoDelay,
|
||||
|attestation, _| {
|
||||
let slot = attestation.data().slot;
|
||||
attestation.data_mut().slot = slot + 1;
|
||||
},
|
||||
|result| assert!(result.is_ok()),
|
||||
)
|
||||
.await
|
||||
.inspect_queued_attestations(|queue| assert_eq!(queue.len(), 2))
|
||||
// Advance past both votes (to slot + 3) so the whole queue drains.
|
||||
.skip_slots(3)
|
||||
.inspect_queued_attestations(|queue| {
|
||||
assert_eq!(
|
||||
queue.len(),
|
||||
0,
|
||||
"all votes are due, so the entire queue must drain"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Tests that the correct target root is used when the attested-to block is in a prior epoch to
|
||||
/// the attestation.
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user