mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-27 01:33:33 +00:00
Ensure doppelganger detects attestations in blocks (#2495)
## Issue Addressed
NA
## Proposed Changes
When testing our (not-yet-released) Doppelganger implementation, I noticed that we aren't detecting attestations included in blocks (only those on the gossip network).
This is because during [block processing](e8c0d1f19b/beacon_node/beacon_chain/src/beacon_chain.rs (L2168)) we only update the `observed_attestations` cache with each attestation, but not the `observed_attesters` cache. This is the correct behaviour when we consider the [p2p spec](https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/p2p-interface.md):
> [IGNORE] There has been no other valid attestation seen on an attestation subnet that has an identical attestation.data.target.epoch and participating validator index.
We're doing the right thing here and still allowing attestations on gossip that we've seen in a block. However, this doesn't work so nicely for Doppelganger.
To resolve this, I've taken the following steps:
- Add a `observed_block_attesters` cache.
- Rename `observed_attesters` to `observed_gossip_attesters`.
## TODO
- [x] Add a test to ensure a validator that's been seen in a block attestation (but not a gossip attestation) returns `true` for `BeaconChain::validator_seen_at_epoch`.
- [x] Add a test to ensure `observed_block_attesters` isn't polluted via gossip attestations and vice versa.
Co-authored-by: realbigsean <seananderson33@gmail.com>
This commit is contained in:
@@ -963,3 +963,114 @@ fn attestation_that_skips_epochs() {
|
||||
.verify_unaggregated_attestation_for_gossip(attestation, Some(subnet_id))
|
||||
.expect("should gossip verify attestation that skips slots");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_aggregate_for_gossip_doppelganger_detection() {
|
||||
let harness = get_harness(VALIDATOR_COUNT);
|
||||
|
||||
// Extend the chain out a few epochs so we have some chain depth to play with.
|
||||
harness.extend_chain(
|
||||
MainnetEthSpec::slots_per_epoch() as usize * 3 - 1,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
);
|
||||
|
||||
// Advance into a slot where there have not been blocks or attestations produced.
|
||||
harness.advance_slot();
|
||||
|
||||
let current_slot = harness.chain.slot().expect("should get slot");
|
||||
|
||||
assert_eq!(
|
||||
current_slot % E::slots_per_epoch(),
|
||||
0,
|
||||
"the test requires a new epoch to avoid already-seen errors"
|
||||
);
|
||||
|
||||
let (valid_attestation, _attester_index, _attester_committee_index, _, _) =
|
||||
get_valid_unaggregated_attestation(&harness.chain);
|
||||
let (valid_aggregate, _, _) =
|
||||
get_valid_aggregated_attestation(&harness.chain, valid_attestation);
|
||||
|
||||
harness
|
||||
.chain
|
||||
.verify_aggregated_attestation_for_gossip(valid_aggregate.clone())
|
||||
.expect("should verify aggregate attestation");
|
||||
|
||||
let epoch = valid_aggregate.message.aggregate.data.target.epoch;
|
||||
let index = valid_aggregate.message.aggregator_index as usize;
|
||||
assert!(harness.chain.validator_seen_at_epoch(index, epoch));
|
||||
|
||||
// Check the correct beacon cache is populated
|
||||
assert!(!harness
|
||||
.chain
|
||||
.observed_block_attesters
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if block attester was observed"));
|
||||
assert!(!harness
|
||||
.chain
|
||||
.observed_gossip_attesters
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if gossip attester was observed"));
|
||||
assert!(harness
|
||||
.chain
|
||||
.observed_aggregators
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if gossip aggregator was observed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_attestation_for_gossip_doppelganger_detection() {
|
||||
let harness = get_harness(VALIDATOR_COUNT);
|
||||
|
||||
// Extend the chain out a few epochs so we have some chain depth to play with.
|
||||
harness.extend_chain(
|
||||
MainnetEthSpec::slots_per_epoch() as usize * 3 - 1,
|
||||
BlockStrategy::OnCanonicalHead,
|
||||
AttestationStrategy::AllValidators,
|
||||
);
|
||||
|
||||
// Advance into a slot where there have not been blocks or attestations produced.
|
||||
harness.advance_slot();
|
||||
|
||||
let current_slot = harness.chain.slot().expect("should get slot");
|
||||
|
||||
assert_eq!(
|
||||
current_slot % E::slots_per_epoch(),
|
||||
0,
|
||||
"the test requires a new epoch to avoid already-seen errors"
|
||||
);
|
||||
|
||||
let (valid_attestation, index, _attester_committee_index, _, subnet_id) =
|
||||
get_valid_unaggregated_attestation(&harness.chain);
|
||||
|
||||
harness
|
||||
.chain
|
||||
.verify_unaggregated_attestation_for_gossip(valid_attestation.clone(), Some(subnet_id))
|
||||
.expect("should verify attestation");
|
||||
|
||||
let epoch = valid_attestation.data.target.epoch;
|
||||
assert!(harness.chain.validator_seen_at_epoch(index, epoch));
|
||||
|
||||
// Check the correct beacon cache is populated
|
||||
assert!(!harness
|
||||
.chain
|
||||
.observed_block_attesters
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if block attester was observed"));
|
||||
assert!(harness
|
||||
.chain
|
||||
.observed_gossip_attesters
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if gossip attester was observed"));
|
||||
assert!(!harness
|
||||
.chain
|
||||
.observed_aggregators
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if gossip aggregator was observed"));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use beacon_chain::test_utils::{
|
||||
use beacon_chain::{BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult};
|
||||
use slasher::{Config as SlasherConfig, Slasher};
|
||||
use state_processing::{
|
||||
common::get_indexed_attestation,
|
||||
per_block_processing::{per_block_processing, BlockSignatureStrategy},
|
||||
per_slot_processing, BlockProcessingError,
|
||||
};
|
||||
@@ -868,6 +869,52 @@ fn verify_block_for_gossip_slashing_detection() {
|
||||
slasher_dir.close().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_block_for_gossip_doppelganger_detection() {
|
||||
let harness = get_harness(VALIDATOR_COUNT);
|
||||
|
||||
let state = harness.get_current_state();
|
||||
let (block, _) = harness.make_block(state.clone(), Slot::new(1));
|
||||
|
||||
let verified_block = harness.chain.verify_block_for_gossip(block).unwrap();
|
||||
let attestations = verified_block.block.message().body().attestations().clone();
|
||||
harness.chain.process_block(verified_block).unwrap();
|
||||
|
||||
for att in attestations.iter() {
|
||||
let epoch = att.data.target.epoch;
|
||||
let committee = state
|
||||
.get_beacon_committee(att.data.slot, att.data.index)
|
||||
.unwrap();
|
||||
let indexed_attestation = get_indexed_attestation(committee.committee, att).unwrap();
|
||||
|
||||
for &index in &indexed_attestation.attesting_indices {
|
||||
let index = index as usize;
|
||||
|
||||
assert!(harness.chain.validator_seen_at_epoch(index, epoch));
|
||||
|
||||
// Check the correct beacon cache is populated
|
||||
assert!(harness
|
||||
.chain
|
||||
.observed_block_attesters
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if block attester was observed"));
|
||||
assert!(!harness
|
||||
.chain
|
||||
.observed_gossip_attesters
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if gossip attester was observed"));
|
||||
assert!(!harness
|
||||
.chain
|
||||
.observed_aggregators
|
||||
.read()
|
||||
.validator_has_been_observed(epoch, index)
|
||||
.expect("should check if gossip aggregator was observed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_base_block_to_altair_chain() {
|
||||
let mut spec = MainnetEthSpec::default_spec();
|
||||
|
||||
Reference in New Issue
Block a user