Skip to content

Commit c5786a8

Browse files
realbigseanmichaelsproulpaulhauner
committed
Doppelganger detection (#2230)
## Issue Addressed Resolves #2069 ## Proposed Changes - Adds a `--doppelganger-detection` flag - Adds a `lighthouse/seen_validators` endpoint, which will make it so the lighthouse VC is not interopable with other client beacon nodes if the `--doppelganger-detection` flag is used, but hopefully this will become standardized. Relevant Eth2 API repo issue: ethereum/beacon-APIs#64 - If the `--doppelganger-detection` flag is used, the VC will wait until the beacon node is synced, and then wait an additional 2 epochs. The reason for this is to make sure the beacon node is able to subscribe to the subnets our validators should be attesting on. I think an alternative would be to have the beacon node subscribe to all subnets for 2+ epochs on startup by default. ## Additional Info I'd like to add tests and would appreciate feedback. TODO: handle validators started via the API, potentially make this default behavior Co-authored-by: realbigsean <[email protected]> Co-authored-by: Michael Sproul <[email protected]> Co-authored-by: Paul Hauner <[email protected]>
1 parent 834ee98 commit c5786a8

38 files changed

+2301
-200
lines changed

.github/workflows/test-suite.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ jobs:
146146
run: sudo npm install -g ganache-cli
147147
- name: Run the syncing simulator
148148
run: cargo run --release --bin simulator syncing-sim
149+
doppelganger-protection-test:
150+
name: doppelganger-protection-test
151+
runs-on: ubuntu-latest
152+
needs: cargo-fmt
153+
steps:
154+
- uses: actions/checkout@v1
155+
- name: Get latest version of stable Rust
156+
run: rustup update stable
157+
- name: Install ganache-cli
158+
run: sudo npm install -g ganache-cli
159+
- name: Install lighthouse and lcli
160+
run: |
161+
make
162+
make install-lcli
163+
- name: Run the doppelganger protection success test script
164+
run: |
165+
cd scripts/tests
166+
./doppelganger_protection.sh success
167+
- name: Run the doppelganger protection failure test script
168+
run: |
169+
cd scripts/tests
170+
./doppelganger_protection.sh failure
149171
check-benchmarks:
150172
name: check-benchmarks
151173
runs-on: ubuntu-latest

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3443,6 +3443,28 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
34433443
let mut file = std::fs::File::create(file_name).unwrap();
34443444
self.dump_as_dot(&mut file);
34453445
}
3446+
3447+
/// Checks if attestations have been seen from the given `validator_index` at the
3448+
/// given `epoch`.
3449+
pub fn validator_seen_at_epoch(&self, validator_index: usize, epoch: Epoch) -> bool {
3450+
// It's necessary to assign these checks to intermediate variables to avoid a deadlock.
3451+
//
3452+
// See: https://github.com/sigp/lighthouse/pull/2230#discussion_r620013993
3453+
let attested = self
3454+
.observed_attesters
3455+
.read()
3456+
.index_seen_at_epoch(validator_index, epoch);
3457+
let aggregated = self
3458+
.observed_aggregators
3459+
.read()
3460+
.index_seen_at_epoch(validator_index, epoch);
3461+
let produced_block = self
3462+
.observed_block_producers
3463+
.read()
3464+
.index_seen_at_epoch(validator_index as u64, epoch);
3465+
3466+
attested || aggregated || produced_block
3467+
}
34463468
}
34473469

34483470
impl<T: BeaconChainTypes> Drop for BeaconChain<T> {

beacon_node/beacon_chain/src/observed_attesters.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,16 @@ impl<T: Item, E: EthSpec> AutoPruningEpochContainer<T, E> {
381381
pub(crate) fn get_lowest_permissible(&self) -> Epoch {
382382
self.lowest_permissible_epoch
383383
}
384+
385+
/// Returns `true` if the given `index` has been stored in `self` at `epoch`.
386+
///
387+
/// This is useful for doppelganger detection.
388+
pub fn index_seen_at_epoch(&self, index: usize, epoch: Epoch) -> bool {
389+
self.items
390+
.get(&epoch)
391+
.map(|item| item.contains(index))
392+
.unwrap_or(false)
393+
}
384394
}
385395

386396
/// A container that stores some number of `V` items.

beacon_node/beacon_chain/src/observed_block_producers.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
use std::collections::{HashMap, HashSet};
55
use std::marker::PhantomData;
6-
use types::{BeaconBlockRef, EthSpec, Slot, Unsigned};
6+
use types::{BeaconBlockRef, Epoch, EthSpec, Slot, Unsigned};
77

88
#[derive(Debug, PartialEq)]
99
pub enum Error {
@@ -114,6 +114,15 @@ impl<E: EthSpec> ObservedBlockProducers<E> {
114114
self.finalized_slot = finalized_slot;
115115
self.items.retain(|slot, _set| *slot > finalized_slot);
116116
}
117+
118+
/// Returns `true` if the given `validator_index` has been stored in `self` at `epoch`.
119+
///
120+
/// This is useful for doppelganger detection.
121+
pub fn index_seen_at_epoch(&self, validator_index: u64, epoch: Epoch) -> bool {
122+
self.items.iter().any(|(slot, producers)| {
123+
slot.epoch(E::slots_per_epoch()) == epoch && producers.contains(&validator_index)
124+
})
125+
}
117126
}
118127

119128
#[cfg(test)]

beacon_node/http_api/src/lib.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1907,6 +1907,49 @@ pub fn serve<T: BeaconChainTypes>(
19071907
},
19081908
);
19091909

1910+
// POST lighthouse/liveness
1911+
let post_lighthouse_liveness = warp::path("lighthouse")
1912+
.and(warp::path("liveness"))
1913+
.and(warp::path::end())
1914+
.and(warp::body::json())
1915+
.and(chain_filter.clone())
1916+
.and_then(
1917+
|request_data: api_types::LivenessRequestData, chain: Arc<BeaconChain<T>>| {
1918+
blocking_json_task(move || {
1919+
// Ensure the request is for either the current, previous or next epoch.
1920+
let current_epoch = chain
1921+
.epoch()
1922+
.map_err(warp_utils::reject::beacon_chain_error)?;
1923+
let prev_epoch = current_epoch.saturating_sub(Epoch::new(1));
1924+
let next_epoch = current_epoch.saturating_add(Epoch::new(1));
1925+
1926+
if request_data.epoch < prev_epoch || request_data.epoch > next_epoch {
1927+
return Err(warp_utils::reject::custom_bad_request(format!(
1928+
"request epoch {} is more than one epoch from the current epoch {}",
1929+
request_data.epoch, current_epoch
1930+
)));
1931+
}
1932+
1933+
let liveness: Vec<api_types::LivenessResponseData> = request_data
1934+
.indices
1935+
.iter()
1936+
.cloned()
1937+
.map(|index| {
1938+
let is_live =
1939+
chain.validator_seen_at_epoch(index as usize, request_data.epoch);
1940+
api_types::LivenessResponseData {
1941+
index: index as u64,
1942+
epoch: request_data.epoch,
1943+
is_live,
1944+
}
1945+
})
1946+
.collect();
1947+
1948+
Ok(api_types::GenericResponse::from(liveness))
1949+
})
1950+
},
1951+
);
1952+
19101953
// GET lighthouse/health
19111954
let get_lighthouse_health = warp::path("lighthouse")
19121955
.and(warp::path("health"))
@@ -2249,6 +2292,7 @@ pub fn serve<T: BeaconChainTypes>(
22492292
.or(post_beacon_pool_voluntary_exits.boxed())
22502293
.or(post_validator_duties_attester.boxed())
22512294
.or(post_validator_aggregate_and_proofs.boxed())
2295+
.or(post_lighthouse_liveness.boxed())
22522296
.or(post_validator_beacon_committee_subscriptions.boxed()),
22532297
))
22542298
.recover(warp_utils::reject::handle_rejection)

beacon_node/http_api/tests/tests.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2149,6 +2149,71 @@ impl ApiTester {
21492149
self
21502150
}
21512151

2152+
pub async fn test_post_lighthouse_liveness(self) -> Self {
2153+
let epoch = self.chain.epoch().unwrap();
2154+
let head_state = self.chain.head_beacon_state().unwrap();
2155+
let indices = (0..head_state.validators().len())
2156+
.map(|i| i as u64)
2157+
.collect::<Vec<_>>();
2158+
2159+
// Construct the expected response
2160+
let expected: Vec<LivenessResponseData> = head_state
2161+
.validators()
2162+
.iter()
2163+
.enumerate()
2164+
.map(|(index, _)| LivenessResponseData {
2165+
index: index as u64,
2166+
is_live: false,
2167+
epoch,
2168+
})
2169+
.collect();
2170+
2171+
let result = self
2172+
.client
2173+
.post_lighthouse_liveness(indices.as_slice(), epoch)
2174+
.await
2175+
.unwrap()
2176+
.data;
2177+
2178+
assert_eq!(result, expected);
2179+
2180+
// Attest to the current slot
2181+
self.client
2182+
.post_beacon_pool_attestations(self.attestations.as_slice())
2183+
.await
2184+
.unwrap();
2185+
2186+
let result = self
2187+
.client
2188+
.post_lighthouse_liveness(indices.as_slice(), epoch)
2189+
.await
2190+
.unwrap()
2191+
.data;
2192+
2193+
let committees = head_state
2194+
.get_beacon_committees_at_slot(self.chain.slot().unwrap())
2195+
.unwrap();
2196+
let attesting_validators: Vec<usize> = committees
2197+
.into_iter()
2198+
.map(|committee| committee.committee.iter().cloned())
2199+
.flatten()
2200+
.collect();
2201+
// All attesters should now be considered live
2202+
let expected = expected
2203+
.into_iter()
2204+
.map(|mut a| {
2205+
if attesting_validators.contains(&(a.index as usize)) {
2206+
a.is_live = true;
2207+
}
2208+
a
2209+
})
2210+
.collect::<Vec<_>>();
2211+
2212+
assert_eq!(result, expected);
2213+
2214+
self
2215+
}
2216+
21522217
pub async fn test_get_events(self) -> Self {
21532218
// Subscribe to all events
21542219
let topics = vec![
@@ -2635,5 +2700,7 @@ async fn lighthouse_endpoints() {
26352700
.test_get_lighthouse_beacon_states_ssz()
26362701
.await
26372702
.test_get_lighthouse_staking()
2703+
.await
2704+
.test_post_lighthouse_liveness()
26382705
.await;
26392706
}

book/src/api-lighthouse.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,28 @@ curl -X GET "http://localhost:5052/lighthouse/beacon/states/0/ssz" | jq
329329
```
330330

331331
*Example omitted for brevity, the body simply contains SSZ bytes.*
332+
333+
### `/lighthouse/liveness`
334+
335+
POST request that checks if any of the given validators have attested in the given epoch. Returns a list
336+
of objects, each including the validator index, epoch, and `is_live` status of a requested validator.
337+
338+
This endpoint is used in doppelganger detection, and will only provide accurate information for the
339+
current, previous, or next epoch.
340+
341+
342+
```bash
343+
curl -X POST "http://localhost:5052/lighthouse/liveness" -d '{"indices":["0","1"],"epoch":"1"}' -H "content-type: application/json" | jq
344+
```
345+
346+
```json
347+
{
348+
"data": [
349+
{
350+
"index": "0",
351+
"epoch": "1",
352+
"is_live": true
353+
}
354+
]
355+
}
356+
```

book/src/api-vc-endpoints.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ Typical Responses | 200
351351
"checksum": {
352352
"function": "sha256",
353353
"params": {
354-
354+
355355
},
356356
"message": "abadc1285fd38b24a98ac586bda5b17a8f93fc1ff0778803dc32049578981236"
357357
},

common/eth2/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ impl fmt::Display for Error {
8383
pub struct Timeouts {
8484
pub attestation: Duration,
8585
pub attester_duties: Duration,
86+
pub liveness: Duration,
8687
pub proposal: Duration,
8788
pub proposer_duties: Duration,
8889
}
@@ -92,6 +93,7 @@ impl Timeouts {
9293
Timeouts {
9394
attestation: timeout,
9495
attester_duties: timeout,
96+
liveness: timeout,
9597
proposal: timeout,
9698
proposer_duties: timeout,
9799
}
@@ -1103,6 +1105,30 @@ impl BeaconNodeHttpClient {
11031105
.await
11041106
}
11051107

1108+
/// `POST lighthouse/liveness`
1109+
pub async fn post_lighthouse_liveness(
1110+
&self,
1111+
ids: &[u64],
1112+
epoch: Epoch,
1113+
) -> Result<GenericResponse<Vec<LivenessResponseData>>, Error> {
1114+
let mut path = self.server.full.clone();
1115+
1116+
path.path_segments_mut()
1117+
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
1118+
.push("lighthouse")
1119+
.push("liveness");
1120+
1121+
self.post_with_timeout_and_response(
1122+
path,
1123+
&LivenessRequestData {
1124+
indices: ids.to_vec(),
1125+
epoch,
1126+
},
1127+
self.timeouts.liveness,
1128+
)
1129+
.await
1130+
}
1131+
11061132
/// `POST validator/duties/attester/{epoch}`
11071133
pub async fn post_validator_duties_attester(
11081134
&self,

0 commit comments

Comments
 (0)