-
Notifications
You must be signed in to change notification settings - Fork 135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(census): add peer scoring #1579
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
use std::time::Duration; | ||
|
||
use ethportal_api::Enr; | ||
use itertools::Itertools; | ||
use rand::{seq::SliceRandom, thread_rng}; | ||
|
||
use super::peer::Peer; | ||
|
||
/// A trait for calculating peer's weight. | ||
pub trait Weight: Send + Sync { | ||
fn weight(&self, content_id: &[u8; 32], peer: &Peer) -> u32; | ||
|
||
fn weight_all<'a>( | ||
&self, | ||
content_id: &[u8; 32], | ||
peers: impl IntoIterator<Item = &'a Peer>, | ||
) -> impl Iterator<Item = (&'a Peer, u32)> { | ||
peers | ||
.into_iter() | ||
.map(|peer| (peer, self.weight(content_id, peer))) | ||
} | ||
} | ||
|
||
/// Calculates peer's weight by adding/subtracting weights of recent events. | ||
/// | ||
/// Weight is calculated using following rules: | ||
/// 1. If peer is not interested in content, `0` is returned | ||
/// 2. Weight's starting value is `starting_weight` | ||
/// 3. All recent events (based on `timeframe` parameter) are scored separately: | ||
/// - successful events increase weight by `success_weight` | ||
/// - failed events decrease weight by `failure_weight` | ||
/// 4. Final weight is restricted to `[0, maximum_weight]` range. | ||
#[derive(Debug, Clone)] | ||
pub struct AdditiveWeight { | ||
pub timeframe: Duration, | ||
pub starting_weight: u32, | ||
pub maximum_weight: u32, | ||
pub success_weight: i32, | ||
pub failure_weight: i32, | ||
} | ||
|
||
impl Default for AdditiveWeight { | ||
fn default() -> Self { | ||
Self { | ||
timeframe: Duration::from_secs(15 * 60), // 15 min | ||
starting_weight: 200, | ||
maximum_weight: 400, | ||
success_weight: 5, | ||
failure_weight: -10, | ||
} | ||
} | ||
} | ||
|
||
impl Weight for AdditiveWeight { | ||
fn weight(&self, content_id: &[u8; 32], peer: &Peer) -> u32 { | ||
if !peer.is_interested_in_content(content_id) { | ||
return 0; | ||
} | ||
let weight = self.starting_weight as i32 | ||
+ Iterator::chain( | ||
peer.iter_liveness_checks() | ||
.map(|liveness_check| (liveness_check.success, liveness_check.timestamp)), | ||
peer.iter_offer_events() | ||
.map(|offer_event| (offer_event.success, offer_event.timestamp)), | ||
) | ||
.map(|(success, timestamp)| { | ||
if timestamp.elapsed() > self.timeframe { | ||
return 0; | ||
} | ||
if success { | ||
self.success_weight | ||
} else { | ||
self.failure_weight | ||
} | ||
}) | ||
.sum::<i32>(); | ||
weight.clamp(0, self.maximum_weight as i32) as u32 | ||
} | ||
} | ||
|
||
/// Selects peers based on their weight provided by [Weight] trait. | ||
/// | ||
/// Selection is done using [SliceRandom::choose_multiple_weighted]. Peers are ranked so that | ||
/// probability of peer A being ranked higher than peer B is proportional to their weights. | ||
/// The top ranked peers are then selected and returned. | ||
#[derive(Debug, Clone)] | ||
pub struct PeerSelector<W: Weight> { | ||
weight: W, | ||
/// The maximum number of peers to select | ||
limit: usize, | ||
} | ||
|
||
impl<W: Weight> PeerSelector<W> { | ||
pub fn new(rank: W, limit: usize) -> Self { | ||
Self { | ||
weight: rank, | ||
limit, | ||
} | ||
} | ||
|
||
/// Selects up to `self.limit` peers based on their weights. | ||
pub fn select_peers<'a>( | ||
&self, | ||
content_id: &[u8; 32], | ||
peers: impl IntoIterator<Item = &'a Peer>, | ||
) -> Vec<Enr> { | ||
let weighted_peers = self.weight.weight_all(content_id, peers).collect_vec(); | ||
|
||
weighted_peers | ||
.choose_multiple_weighted(&mut thread_rng(), self.limit, |(_peer, weight)| *weight) | ||
.expect("choosing random sample shouldn't fail") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this fail if there are zero peers? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it doesn't. Weight 0 would also be picked, but only if there isn't enough weights that are non-zero, which is what we want. One thing that we don't differentiate at the moment is that weight can be 0 if:
It might be better to never select peer whose radius doesn't include content. But I left that for future optimization |
||
.map(|(peer, _weight)| peer.enr()) | ||
.collect() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there plans to use these in the future?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
Idea is to measure bandwidth and prefer peers that are faster than average.
But I didn't want to complicate initial work, and I don't think it works well with current implementation of "Weight" because peers that are working would quickly increase their weight to maximum and this wouldn't make a difference.