Altair networking (#2300)

## Issue Addressed

Resolves #2278 

## Proposed Changes

Implements the networking components for the Altair hard fork https://github.com/ethereum/eth2.0-specs/blob/dev/specs/altair/p2p-interface.md

## Additional Info

This PR acts as the base branch for networking changes and tracks https://github.com/sigp/lighthouse/pull/2279 . Changes to gossip, rpc and discovery can be separate PRs to be merged here for ease of review.

Co-authored-by: realbigsean <seananderson33@gmail.com>
This commit is contained in:
Pawan Dhananjay
2021-08-04 01:44:57 +00:00
parent 6a620a31da
commit e8c0d1f19b
51 changed files with 4038 additions and 1354 deletions

View File

@@ -1,6 +1,7 @@
use crate::behaviour::gossipsub_scoring_parameters::{
lighthouse_gossip_thresholds, PeerScoreSettings,
};
use crate::config::gossipsub_config;
use crate::discovery::{subnet_predicate, Discovery, DiscoveryEvent, TARGET_SUBNET_PEERS};
use crate::peer_manager::{
score::ReportSource, ConnectionDirection, PeerManager, PeerManagerEvent,
@@ -8,7 +9,7 @@ use crate::peer_manager::{
use crate::rpc::*;
use crate::service::METADATA_FILENAME;
use crate::types::{
subnet_id_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, SnappyTransform,
subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, SnappyTransform, Subnet,
SubnetDiscovery,
};
use crate::Eth2Enr;
@@ -42,7 +43,10 @@ use std::{
sync::Arc,
task::{Context, Poll},
};
use types::{ChainSpec, EnrForkId, EthSpec, SignedBeaconBlock, Slot, SubnetId};
use types::{
consts::altair::SYNC_COMMITTEE_SUBNET_COUNT, ChainSpec, EnrForkId, EthSpec, ForkContext,
SignedBeaconBlock, Slot, SubnetId, SyncSubnetId,
};
pub mod gossipsub_scoring_parameters;
@@ -157,6 +161,8 @@ pub struct Behaviour<TSpec: EthSpec> {
/// Directory where metadata is stored.
#[behaviour(ignore)]
network_dir: PathBuf,
#[behaviour(ignore)]
fork_context: Arc<ForkContext>,
/// Gossipsub score parameters.
#[behaviour(ignore)]
score_settings: PeerScoreSettings<TSpec>,
@@ -172,9 +178,10 @@ pub struct Behaviour<TSpec: EthSpec> {
impl<TSpec: EthSpec> Behaviour<TSpec> {
pub async fn new(
local_key: &Keypair,
config: &NetworkConfig,
mut config: NetworkConfig,
network_globals: Arc<NetworkGlobals<TSpec>>,
log: &slog::Logger,
fork_context: Arc<ForkContext>,
chain_spec: &ChainSpec,
) -> error::Result<Self> {
let behaviour_log = log.new(o!());
@@ -191,7 +198,8 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
};
// Build and start the discovery sub-behaviour
let mut discovery = Discovery::new(local_key, config, network_globals.clone(), log).await?;
let mut discovery =
Discovery::new(local_key, &config, network_globals.clone(), log).await?;
// start searching for peers
discovery.discover_peers();
@@ -201,13 +209,19 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
.eth2()
.expect("Local ENR must have a fork id");
let possible_fork_digests = vec![enr_fork_id.fork_digest];
let possible_fork_digests = fork_context.all_fork_digests();
let filter = MaxCountSubscriptionFilter {
filter: Self::create_whitelist_filter(possible_fork_digests, 64), //TODO change this to a constant
filter: Self::create_whitelist_filter(
possible_fork_digests,
chain_spec.attestation_subnet_count,
SYNC_COMMITTEE_SUBNET_COUNT,
),
max_subscribed_topics: 200, //TODO change this to a constant
max_subscriptions_per_request: 100, //this is according to the current go implementation
};
config.gs_config = gossipsub_config(fork_context.clone());
// Build and configure the Gossipsub behaviour
let snappy_transform = SnappyTransform::new(config.gs_config.max_transmit_size());
let mut gossipsub = Gossipsub::new_with_subscription_filter_and_transform(
@@ -247,11 +261,11 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
Ok(Behaviour {
// Sub-behaviours
gossipsub,
eth2_rpc: RPC::new(log.clone()),
eth2_rpc: RPC::new(fork_context.clone(), log.clone()),
discovery,
identify: Identify::new(identify_config),
// Auxiliary fields
peer_manager: PeerManager::new(config, network_globals.clone(), log).await?,
peer_manager: PeerManager::new(&config, network_globals.clone(), log).await?,
events: VecDeque::new(),
internal_events: VecDeque::new(),
network_globals,
@@ -260,6 +274,7 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
network_dir: config.network_dir.clone(),
log: behaviour_log,
score_settings,
fork_context,
update_gossipsub_scores,
})
}
@@ -311,28 +326,20 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
self.unsubscribe(gossip_topic)
}
/// Subscribes to a specific subnet id;
pub fn subscribe_to_subnet(&mut self, subnet_id: SubnetId) -> bool {
let topic = GossipTopic::new(
subnet_id.into(),
GossipEncoding::default(),
self.enr_fork_id.fork_digest,
);
self.subscribe(topic)
}
/// Un-Subscribes from a specific subnet id;
pub fn unsubscribe_from_subnet(&mut self, subnet_id: SubnetId) -> bool {
let topic = GossipTopic::new(
subnet_id.into(),
GossipEncoding::default(),
self.enr_fork_id.fork_digest,
);
self.unsubscribe(topic)
/// Unsubscribe from all topics that doesn't have the given fork_digest
pub fn unsubscribe_from_fork_topics_except(&mut self, except: [u8; 4]) {
let subscriptions = self.network_globals.gossipsub_subscriptions.read().clone();
for topic in subscriptions
.iter()
.filter(|topic| topic.fork_digest != except)
.cloned()
{
self.unsubscribe(topic);
}
}
/// Subscribes to a gossipsub topic.
fn subscribe(&mut self, topic: GossipTopic) -> bool {
pub fn subscribe(&mut self, topic: GossipTopic) -> bool {
// update the network globals
self.network_globals
.gossipsub_subscriptions
@@ -354,7 +361,7 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
}
/// Unsubscribe from a gossipsub topic.
fn unsubscribe(&mut self, topic: GossipTopic) -> bool {
pub fn unsubscribe(&mut self, topic: GossipTopic) -> bool {
// update the network globals
self.network_globals
.gossipsub_subscriptions
@@ -537,15 +544,15 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
self.discovery.add_enr(enr);
}
/// Updates a subnet value to the ENR bitfield.
/// Updates a subnet value to the ENR attnets/syncnets bitfield.
///
/// The `value` is `true` if a subnet is being added and false otherwise.
pub fn update_enr_subnet(&mut self, subnet_id: SubnetId, value: bool) {
pub fn update_enr_subnet(&mut self, subnet_id: Subnet, value: bool) {
if let Err(e) = self.discovery.update_enr_bitfield(subnet_id, value) {
crit!(self.log, "Could not update ENR bitfield"; "error" => e);
}
// update the local meta data which informs our peers of the update during PINGS
self.update_metadata();
self.update_metadata_bitfields();
}
/// Attempts to discover new peers for a given subnet. The `min_ttl` gives the time at which we
@@ -564,20 +571,24 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
self.network_globals
.peers
.write()
.extend_peers_on_subnet(s.subnet_id, min_ttl);
.extend_peers_on_subnet(&s.subnet, min_ttl);
if let Subnet::SyncCommittee(sync_subnet) = s.subnet {
self.peer_manager_mut()
.add_sync_subnet(sync_subnet, min_ttl);
}
}
// Already have target number of peers, no need for subnet discovery
let peers_on_subnet = self
.network_globals
.peers
.read()
.good_peers_on_subnet(s.subnet_id)
.good_peers_on_subnet(s.subnet)
.count();
if peers_on_subnet >= TARGET_SUBNET_PEERS {
trace!(
self.log,
"Discovery query ignored";
"subnet_id" => ?s.subnet_id,
"subnet" => ?s.subnet,
"reason" => "Already connected to desired peers",
"connected_peers_on_subnet" => peers_on_subnet,
"target_subnet_peers" => TARGET_SUBNET_PEERS,
@@ -587,7 +598,7 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
// If we connect to the cached peers before the discovery query starts, then we potentially
// save a costly discovery query.
} else {
self.dial_cached_enrs_in_subnet(s.subnet_id);
self.dial_cached_enrs_in_subnet(s.subnet);
true
}
})
@@ -603,26 +614,6 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
pub fn update_fork_version(&mut self, enr_fork_id: EnrForkId) {
self.discovery.update_eth2_enr(enr_fork_id.clone());
// unsubscribe from all gossip topics and re-subscribe to their new fork counterparts
let subscribed_topics = self
.network_globals
.gossipsub_subscriptions
.read()
.iter()
.cloned()
.collect::<Vec<GossipTopic>>();
// unsubscribe from all topics
for topic in &subscribed_topics {
self.unsubscribe(topic.clone());
}
// re-subscribe modifying the fork version
for mut topic in subscribed_topics {
*topic.digest() = enr_fork_id.fork_digest;
self.subscribe(topic);
}
// update the local reference
self.enr_fork_id = enr_fork_id;
}
@@ -630,18 +621,28 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
/* Private internal functions */
/// Updates the current meta data of the node to match the local ENR.
fn update_metadata(&mut self) {
fn update_metadata_bitfields(&mut self) {
let local_attnets = self
.discovery
.local_enr()
.bitfield::<TSpec>()
.expect("Local discovery must have bitfield");
.attestation_bitfield::<TSpec>()
.expect("Local discovery must have attestation bitfield");
let local_syncnets = self
.discovery
.local_enr()
.sync_committee_bitfield::<TSpec>()
.expect("Local discovery must have sync committee bitfield");
{
// write lock scope
let mut meta_data = self.network_globals.local_metadata.write();
meta_data.seq_number += 1;
meta_data.attnets = local_attnets;
*meta_data.seq_number_mut() += 1;
*meta_data.attnets_mut() = local_attnets;
if let Ok(syncnets) = meta_data.syncnets_mut() {
*syncnets = local_syncnets;
}
}
// Save the updated metadata to disk
save_metadata_to_disk(
@@ -654,7 +655,7 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
/// Sends a Ping request to the peer.
fn ping(&mut self, id: RequestId, peer_id: PeerId) {
let ping = crate::rpc::Ping {
data: self.network_globals.local_metadata.read().seq_number,
data: *self.network_globals.local_metadata.read().seq_number(),
};
trace!(self.log, "Sending Ping"; "request_id" => id, "peer_id" => %peer_id);
@@ -665,7 +666,7 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
/// Sends a Pong response to the peer.
fn pong(&mut self, id: PeerRequestId, peer_id: PeerId) {
let ping = crate::rpc::Ping {
data: self.network_globals.local_metadata.read().seq_number,
data: *self.network_globals.local_metadata.read().seq_number(),
};
trace!(self.log, "Sending Pong"; "request_id" => id.1, "peer_id" => %peer_id);
let event = RPCCodedResponse::Success(RPCResponse::Pong(ping));
@@ -724,8 +725,8 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
/// Dial cached enrs in discovery service that are in the given `subnet_id` and aren't
/// in Connected, Dialing or Banned state.
fn dial_cached_enrs_in_subnet(&mut self, subnet_id: SubnetId) {
let predicate = subnet_predicate::<TSpec>(vec![subnet_id], &self.log);
fn dial_cached_enrs_in_subnet(&mut self, subnet: Subnet) {
let predicate = subnet_predicate::<TSpec>(vec![subnet], &self.log);
let peers_to_dial: Vec<PeerId> = self
.discovery
.cached_enrs()
@@ -752,6 +753,7 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
fn create_whitelist_filter(
possible_fork_digests: Vec<[u8; 4]>,
attestation_subnet_count: u64,
sync_committee_subnet_count: u64,
) -> WhitelistSubscriptionFilter {
let mut possible_hashes = HashSet::new();
for fork_digest in possible_fork_digests {
@@ -767,9 +769,13 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
add(VoluntaryExit);
add(ProposerSlashing);
add(AttesterSlashing);
add(SignedContributionAndProof);
for id in 0..attestation_subnet_count {
add(Attestation(SubnetId::new(id)));
}
for id in 0..sync_committee_subnet_count {
add(SyncCommitteeMessage(SyncSubnetId::new(id)));
}
}
WhitelistSubscriptionFilter(possible_hashes)
}
@@ -792,9 +798,9 @@ impl<TSpec: EthSpec> NetworkBehaviourEventProcess<GossipsubEvent> for Behaviour<
} => {
// Note: We are keeping track here of the peer that sent us the message, not the
// peer that originally published the message.
match PubsubMessage::decode(&gs_msg.topic, &gs_msg.data) {
match PubsubMessage::decode(&gs_msg.topic, &gs_msg.data, &self.fork_context) {
Err(e) => {
debug!(self.log, "Could not decode gossipsub message"; "error" => e);
debug!(self.log, "Could not decode gossipsub message"; "topic" => ?gs_msg.topic,"error" => e);
//reject the message
if let Err(e) = self.gossipsub.report_message_validation_result(
&id,
@@ -816,12 +822,12 @@ impl<TSpec: EthSpec> NetworkBehaviourEventProcess<GossipsubEvent> for Behaviour<
}
}
GossipsubEvent::Subscribed { peer_id, topic } => {
if let Some(subnet_id) = subnet_id_from_topic_hash(&topic) {
if let Some(subnet_id) = subnet_from_topic_hash(&topic) {
self.peer_manager.add_subscription(&peer_id, subnet_id);
}
}
GossipsubEvent::Unsubscribed { peer_id, topic } => {
if let Some(subnet_id) = subnet_id_from_topic_hash(&topic) {
if let Some(subnet_id) = subnet_from_topic_hash(&topic) {
self.peer_manager.remove_subscription(&peer_id, subnet_id);
}
}
@@ -1089,6 +1095,10 @@ impl<TSpec: EthSpec> Behaviour<TSpec> {
// Peer manager has requested a discovery query for more peers.
self.discovery.discover_peers();
}
PeerManagerEvent::DiscoverSubnetPeers(subnets_to_discover) => {
// Peer manager has requested a subnet discovery query for more peers.
self.discover_subnet_peers(subnets_to_discover);
}
PeerManagerEvent::Ping(peer_id) => {
// send a ping request to this peer
self.ping(RequestId::Behaviour, peer_id);

View File

@@ -12,7 +12,9 @@ use libp2p::Multiaddr;
use serde_derive::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use types::{ForkContext, ForkName};
/// The maximum transmit size of gossip messages in bytes.
pub const GOSSIP_MAX_SIZE: usize = 1_048_576;
@@ -109,47 +111,9 @@ impl Default for Config {
.join(DEFAULT_BEACON_NODE_DIR)
.join(DEFAULT_NETWORK_DIR);
// The function used to generate a gossipsub message id
// We use the first 8 bytes of SHA256(data) for content addressing
let fast_gossip_message_id = |message: &RawGossipsubMessage| {
FastMessageId::from(&Sha256::digest(&message.data)[..8])
};
fn prefix(prefix: [u8; 4], data: &[u8]) -> Vec<u8> {
let mut vec = Vec::with_capacity(prefix.len() + data.len());
vec.extend_from_slice(&prefix);
vec.extend_from_slice(data);
vec
}
let gossip_message_id = |message: &GossipsubMessage| {
MessageId::from(
&Sha256::digest(prefix(MESSAGE_DOMAIN_VALID_SNAPPY, &message.data).as_slice())
[..20],
)
};
// gossipsub configuration
// Note: The topics by default are sent as plain strings. Hashes are an optional
// parameter.
// Note: Using the default config here. Use `gossipsub_config` function for getting
// Lighthouse specific configuration for gossipsub.
let gs_config = GossipsubConfigBuilder::default()
.max_transmit_size(GOSSIP_MAX_SIZE)
.heartbeat_interval(Duration::from_millis(700))
.mesh_n(8)
.mesh_n_low(MESH_N_LOW)
.mesh_n_high(12)
.gossip_lazy(6)
.fanout_ttl(Duration::from_secs(60))
.history_length(12)
.max_messages_per_rpc(Some(500)) // Responses to IWANT can be quite large
.history_gossip(3)
.validate_messages() // require validation before propagation
.validation_mode(ValidationMode::Anonymous)
// prevent duplicates for 550 heartbeats(700millis * 550) = 385 secs
.duplicate_cache_time(Duration::from_secs(385))
.message_id_fn(gossip_message_id)
.fast_message_id_fn(fast_gossip_message_id)
.allow_self_origin(true)
.build()
.expect("valid gossipsub configuration");
@@ -209,3 +173,65 @@ impl Default for Config {
}
}
}
/// Return a Lighthouse specific `GossipsubConfig` where the `message_id_fn` depends on the current fork.
pub fn gossipsub_config(fork_context: Arc<ForkContext>) -> GossipsubConfig {
// The function used to generate a gossipsub message id
// We use the first 8 bytes of SHA256(data) for content addressing
let fast_gossip_message_id =
|message: &RawGossipsubMessage| FastMessageId::from(&Sha256::digest(&message.data)[..8]);
fn prefix(
prefix: [u8; 4],
message: &GossipsubMessage,
fork_context: Arc<ForkContext>,
) -> Vec<u8> {
let topic_bytes = message.topic.as_str().as_bytes();
match fork_context.current_fork() {
ForkName::Altair => {
let topic_len_bytes = topic_bytes.len().to_le_bytes();
let mut vec = Vec::with_capacity(
prefix.len() + topic_len_bytes.len() + topic_bytes.len() + message.data.len(),
);
vec.extend_from_slice(&prefix);
vec.extend_from_slice(&topic_len_bytes);
vec.extend_from_slice(topic_bytes);
vec.extend_from_slice(&message.data);
vec
}
ForkName::Base => {
let mut vec = Vec::with_capacity(prefix.len() + message.data.len());
vec.extend_from_slice(&prefix);
vec.extend_from_slice(&message.data);
vec
}
}
}
let gossip_message_id = move |message: &GossipsubMessage| {
MessageId::from(
&Sha256::digest(
prefix(MESSAGE_DOMAIN_VALID_SNAPPY, message, fork_context.clone()).as_slice(),
)[..20],
)
};
GossipsubConfigBuilder::default()
.max_transmit_size(GOSSIP_MAX_SIZE)
.heartbeat_interval(Duration::from_millis(700))
.mesh_n(8)
.mesh_n_low(MESH_N_LOW)
.mesh_n_high(12)
.gossip_lazy(6)
.fanout_ttl(Duration::from_secs(60))
.history_length(12)
.max_messages_per_rpc(Some(500)) // Responses to IWANT can be quite large
.history_gossip(3)
.validate_messages() // require validation before propagation
.validation_mode(ValidationMode::Anonymous)
// prevent duplicates for 550 heartbeats(700millis * 550) = 385 secs
.duplicate_cache_time(Duration::from_secs(385))
.message_id_fn(gossip_message_id)
.fast_message_id_fn(fast_gossip_message_id)
.allow_self_origin(true)
.build()
.expect("valid gossipsub configuration")
}

View File

@@ -4,7 +4,7 @@ pub use discv5::enr::{self, CombinedKey, EnrBuilder};
use super::enr_ext::CombinedKeyExt;
use super::ENR_FILENAME;
use crate::types::{Enr, EnrBitfield};
use crate::types::{Enr, EnrAttestationBitfield, EnrSyncCommitteeBitfield};
use crate::NetworkConfig;
use discv5::enr::EnrKey;
use libp2p::core::identity::Keypair;
@@ -19,25 +19,47 @@ use types::{EnrForkId, EthSpec};
/// The ENR field specifying the fork id.
pub const ETH2_ENR_KEY: &str = "eth2";
/// The ENR field specifying the subnet bitfield.
pub const BITFIELD_ENR_KEY: &str = "attnets";
/// The ENR field specifying the attestation subnet bitfield.
pub const ATTESTATION_BITFIELD_ENR_KEY: &str = "attnets";
/// The ENR field specifying the sync committee subnet bitfield.
pub const SYNC_COMMITTEE_BITFIELD_ENR_KEY: &str = "syncnets";
/// Extension trait for ENR's within Eth2.
pub trait Eth2Enr {
/// The subnet bitfield associated with the ENR.
fn bitfield<TSpec: EthSpec>(&self) -> Result<EnrBitfield<TSpec>, &'static str>;
/// The attestation subnet bitfield associated with the ENR.
fn attestation_bitfield<TSpec: EthSpec>(
&self,
) -> Result<EnrAttestationBitfield<TSpec>, &'static str>;
/// The sync committee subnet bitfield associated with the ENR.
fn sync_committee_bitfield<TSpec: EthSpec>(
&self,
) -> Result<EnrSyncCommitteeBitfield<TSpec>, &'static str>;
fn eth2(&self) -> Result<EnrForkId, &'static str>;
}
impl Eth2Enr for Enr {
fn bitfield<TSpec: EthSpec>(&self) -> Result<EnrBitfield<TSpec>, &'static str> {
fn attestation_bitfield<TSpec: EthSpec>(
&self,
) -> Result<EnrAttestationBitfield<TSpec>, &'static str> {
let bitfield_bytes = self
.get(BITFIELD_ENR_KEY)
.ok_or("ENR bitfield non-existent")?;
.get(ATTESTATION_BITFIELD_ENR_KEY)
.ok_or("ENR attestation bitfield non-existent")?;
BitVector::<TSpec::SubnetBitfieldLength>::from_ssz_bytes(bitfield_bytes)
.map_err(|_| "Could not decode the ENR SSZ bitfield")
.map_err(|_| "Could not decode the ENR attnets bitfield")
}
fn sync_committee_bitfield<TSpec: EthSpec>(
&self,
) -> Result<EnrSyncCommitteeBitfield<TSpec>, &'static str> {
let bitfield_bytes = self
.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY)
.ok_or("ENR sync committee bitfield non-existent")?;
BitVector::<TSpec::SyncCommitteeSubnetCount>::from_ssz_bytes(bitfield_bytes)
.map_err(|_| "Could not decode the ENR syncnets bitfield")
}
fn eth2(&self) -> Result<EnrForkId, &'static str> {
@@ -151,7 +173,12 @@ pub fn build_enr<T: EthSpec>(
// set the "attnets" field on our ENR
let bitfield = BitVector::<T::SubnetBitfieldLength>::new();
builder.add_value(BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes());
builder.add_value(ATTESTATION_BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes());
// set the "syncnets" field on our ENR
let bitfield = BitVector::<T::SyncCommitteeSubnetCount>::new();
builder.add_value(SYNC_COMMITTEE_BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes());
builder
.build(enr_key)
@@ -169,9 +196,10 @@ fn compare_enr(local_enr: &Enr, disk_enr: &Enr) -> bool {
&& local_enr.get(ETH2_ENR_KEY) == disk_enr.get(ETH2_ENR_KEY)
// take preference over disk udp port if one is not specified
&& (local_enr.udp().is_none() || local_enr.udp() == disk_enr.udp())
// we need the BITFIELD_ENR_KEY key to match, otherwise we use a new ENR. This will likely only
// be true for non-validating nodes
&& local_enr.get(BITFIELD_ENR_KEY) == disk_enr.get(BITFIELD_ENR_KEY)
// we need the ATTESTATION_BITFIELD_ENR_KEY and SYNC_COMMITTEE_BITFIELD_ENR_KEY key to match,
// otherwise we use a new ENR. This will likely only be true for non-validating nodes
&& local_enr.get(ATTESTATION_BITFIELD_ENR_KEY) == disk_enr.get(ATTESTATION_BITFIELD_ENR_KEY)
&& local_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY) == disk_enr.get(SYNC_COMMITTEE_BITFIELD_ENR_KEY)
}
/// Loads enr from the given directory

View File

@@ -8,31 +8,28 @@ pub mod enr_ext;
// Allow external use of the lighthouse ENR builder
use crate::{config, metrics};
use crate::{error, Enr, NetworkConfig, NetworkGlobals, SubnetDiscovery};
use crate::{error, Enr, NetworkConfig, NetworkGlobals, Subnet, SubnetDiscovery};
use discv5::{enr::NodeId, Discv5, Discv5Event};
pub use enr::{
build_enr, create_enr_builder_from_config, load_enr_from_disk, use_or_load_enr, CombinedKey,
Eth2Enr,
};
use enr::{BITFIELD_ENR_KEY, ETH2_ENR_KEY};
pub use enr_ext::{peer_id_to_node_id, CombinedKeyExt, EnrExt};
pub use libp2p::core::identity::{Keypair, PublicKey};
use enr::{ATTESTATION_BITFIELD_ENR_KEY, ETH2_ENR_KEY, SYNC_COMMITTEE_BITFIELD_ENR_KEY};
use futures::prelude::*;
use futures::stream::FuturesUnordered;
pub use libp2p::{
core::{
connection::ConnectionId,
identity::{Keypair, PublicKey},
ConnectedPoint, Multiaddr, PeerId,
},
core::{connection::ConnectionId, ConnectedPoint, Multiaddr, PeerId},
swarm::{
protocols_handler::ProtocolsHandler, NetworkBehaviour, NetworkBehaviourAction as NBAction,
NotifyHandler, PollParameters, SubstreamProtocol,
},
};
use lru::LruCache;
use slog::{crit, debug, error, info, warn};
use ssz::{Decode, Encode};
use ssz_types::BitVector;
use slog::{crit, debug, error, info, trace, warn};
use ssz::Encode;
use std::{
collections::{HashMap, VecDeque},
net::{IpAddr, SocketAddr},
@@ -43,7 +40,7 @@ use std::{
time::{Duration, Instant},
};
use tokio::sync::mpsc;
use types::{EnrForkId, EthSpec, SubnetId};
use types::{EnrForkId, EthSpec};
mod subnet_predicate;
pub use subnet_predicate::subnet_predicate;
@@ -77,13 +74,26 @@ pub enum DiscoveryEvent {
SocketUpdated(SocketAddr),
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Clone, PartialEq)]
struct SubnetQuery {
subnet_id: SubnetId,
subnet: Subnet,
min_ttl: Option<Instant>,
retries: usize,
}
impl std::fmt::Debug for SubnetQuery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let min_ttl_secs = self
.min_ttl
.map(|ttl| ttl.saturating_duration_since(Instant::now()).as_secs());
f.debug_struct("SubnetQuery")
.field("subnet", &self.subnet)
.field("min_ttl_secs", &min_ttl_secs)
.field("retries", &self.retries)
.finish()
}
}
#[derive(Debug, Clone, PartialEq)]
enum QueryType {
/// We are searching for subnet peers.
@@ -335,13 +345,13 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
if !self.started {
return;
}
debug!(
trace!(
self.log,
"Making discovery query for subnets";
"subnets" => ?subnets_to_discover.iter().map(|s| s.subnet_id).collect::<Vec<_>>()
"subnets" => ?subnets_to_discover.iter().map(|s| s.subnet).collect::<Vec<_>>()
);
for subnet in subnets_to_discover {
self.add_subnet_query(subnet.subnet_id, subnet.min_ttl, 0);
self.add_subnet_query(subnet.subnet, subnet.min_ttl, 0);
}
}
@@ -426,42 +436,84 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
Ok(())
}
/// Adds/Removes a subnet from the ENR Bitfield
pub fn update_enr_bitfield(&mut self, subnet_id: SubnetId, value: bool) -> Result<(), String> {
let id = *subnet_id as usize;
/// Adds/Removes a subnet from the ENR attnets/syncnets Bitfield
pub fn update_enr_bitfield(&mut self, subnet: Subnet, value: bool) -> Result<(), String> {
let local_enr = self.discv5.local_enr();
let mut current_bitfield = local_enr.bitfield::<TSpec>()?;
if id >= current_bitfield.len() {
return Err(format!(
"Subnet id: {} is outside the ENR bitfield length: {}",
id,
current_bitfield.len()
));
match subnet {
Subnet::Attestation(id) => {
let id = *id as usize;
let mut current_bitfield = local_enr.attestation_bitfield::<TSpec>()?;
if id >= current_bitfield.len() {
return Err(format!(
"Subnet id: {} is outside the ENR bitfield length: {}",
id,
current_bitfield.len()
));
}
if current_bitfield
.get(id)
.map_err(|_| String::from("Subnet ID out of bounds"))?
== value
{
return Err(format!(
"Subnet id: {} already in the local ENR already has value: {}",
id, value
));
}
// set the subnet bitfield in the ENR
current_bitfield.set(id, value).map_err(|_| {
String::from("Subnet ID out of bounds, could not set subnet ID")
})?;
// insert the bitfield into the ENR record
self.discv5
.enr_insert(
ATTESTATION_BITFIELD_ENR_KEY,
&current_bitfield.as_ssz_bytes(),
)
.map_err(|e| format!("{:?}", e))?;
}
Subnet::SyncCommittee(id) => {
let id = *id as usize;
let mut current_bitfield = local_enr.sync_committee_bitfield::<TSpec>()?;
if id >= current_bitfield.len() {
return Err(format!(
"Subnet id: {} is outside the ENR bitfield length: {}",
id,
current_bitfield.len()
));
}
if current_bitfield
.get(id)
.map_err(|_| String::from("Subnet ID out of bounds"))?
== value
{
return Err(format!(
"Subnet id: {} already in the local ENR already has value: {}",
id, value
));
}
// set the subnet bitfield in the ENR
current_bitfield.set(id, value).map_err(|_| {
String::from("Subnet ID out of bounds, could not set subnet ID")
})?;
// insert the bitfield into the ENR record
self.discv5
.enr_insert(
SYNC_COMMITTEE_BITFIELD_ENR_KEY,
&current_bitfield.as_ssz_bytes(),
)
.map_err(|e| format!("{:?}", e))?;
}
}
if current_bitfield
.get(id)
.map_err(|_| String::from("Subnet ID out of bounds"))?
== value
{
return Err(format!(
"Subnet id: {} already in the local ENR already has value: {}",
id, value
));
}
// set the subnet bitfield in the ENR
current_bitfield
.set(id, value)
.map_err(|_| String::from("Subnet ID out of bounds, could not set subnet ID"))?;
// insert the bitfield into the ENR record
self.discv5
.enr_insert(BITFIELD_ENR_KEY, &current_bitfield.as_ssz_bytes())
.map_err(|e| format!("{:?}", e))?;
// replace the global version
*self.network_globals.local_enr.write() = self.discv5.local_enr();
@@ -547,7 +599,7 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
/// Adds a subnet query if one doesn't exist. If a subnet query already exists, this
/// updates the min_ttl field.
fn add_subnet_query(&mut self, subnet_id: SubnetId, min_ttl: Option<Instant>, retries: usize) {
fn add_subnet_query(&mut self, subnet: Subnet, min_ttl: Option<Instant>, retries: usize) {
// remove the entry and complete the query if greater than the maximum search count
if retries > MAX_DISCOVERY_RETRY {
debug!(
@@ -562,7 +614,7 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
let mut found = false;
for query in self.queued_queries.iter_mut() {
if let QueryType::Subnet(ref mut subnet_query) = query {
if subnet_query.subnet_id == subnet_id {
if subnet_query.subnet == subnet {
if subnet_query.min_ttl < min_ttl {
subnet_query.min_ttl = min_ttl;
}
@@ -577,12 +629,12 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
if !found {
// Set up the query and add it to the queue
let query = QueryType::Subnet(SubnetQuery {
subnet_id,
subnet,
min_ttl,
retries,
});
// update the metrics and insert into the queue.
debug!(self.log, "Queuing subnet query"; "subnet" => *subnet_id, "retries" => retries);
trace!(self.log, "Queuing subnet query"; "subnet" => ?subnet, "retries" => retries);
self.queued_queries.push_back(query);
metrics::set_gauge(&metrics::DISCOVERY_QUEUE, self.queued_queries.len() as i64);
}
@@ -636,11 +688,6 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
// This query is for searching for peers of a particular subnet
// Drain subnet_queries so we can re-use it as we continue to process the queue
let grouped_queries: Vec<SubnetQuery> = subnet_queries.drain(..).collect();
debug!(
self.log,
"Starting grouped subnet query";
"subnets" => ?grouped_queries.iter().map(|q| q.subnet_id).collect::<Vec<_>>(),
);
self.start_subnet_query(grouped_queries);
processed = true;
}
@@ -661,7 +708,7 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
/// Runs a discovery request for a given group of subnets.
fn start_subnet_query(&mut self, subnet_queries: Vec<SubnetQuery>) {
let mut filtered_subnet_ids: Vec<SubnetId> = Vec::new();
let mut filtered_subnets: Vec<Subnet> = Vec::new();
// find subnet queries that are still necessary
let filtered_subnet_queries: Vec<SubnetQuery> = subnet_queries
@@ -672,7 +719,7 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
.network_globals
.peers
.read()
.good_peers_on_subnet(subnet_query.subnet_id)
.good_peers_on_subnet(subnet_query.subnet)
.count();
if peers_on_subnet >= TARGET_SUBNET_PEERS {
@@ -685,16 +732,13 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
}
let target_peers = TARGET_SUBNET_PEERS - peers_on_subnet;
debug!(self.log, "Discovery query started for subnet";
"subnet_id" => *subnet_query.subnet_id,
trace!(self.log, "Discovery query started for subnet";
"subnet_query" => ?subnet_query,
"connected_peers_on_subnet" => peers_on_subnet,
"target_subnet_peers" => TARGET_SUBNET_PEERS,
"peers_to_find" => target_peers,
"attempt" => subnet_query.retries,
"min_ttl" => ?subnet_query.min_ttl,
);
filtered_subnet_ids.push(subnet_query.subnet_id);
filtered_subnets.push(subnet_query.subnet);
true
})
.collect();
@@ -702,8 +746,13 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
// Only start a discovery query if we have a subnet to look for.
if !filtered_subnet_queries.is_empty() {
// build the subnet predicate as a combination of the eth2_fork_predicate and the subnet predicate
let subnet_predicate = subnet_predicate::<TSpec>(filtered_subnet_ids, &self.log);
let subnet_predicate = subnet_predicate::<TSpec>(filtered_subnets, &self.log);
debug!(
self.log,
"Starting grouped subnet query";
"subnets" => ?filtered_subnet_queries,
);
self.start_query(
GroupedQueryType::Subnet(filtered_subnet_queries),
TARGET_PEERS_FOR_GROUPED_QUERY,
@@ -798,17 +847,13 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
}
}
GroupedQueryType::Subnet(queries) => {
let subnets_searched_for: Vec<SubnetId> =
queries.iter().map(|query| query.subnet_id).collect();
let subnets_searched_for: Vec<Subnet> =
queries.iter().map(|query| query.subnet).collect();
match query_result.1 {
Ok(r) if r.is_empty() => {
debug!(self.log, "Grouped subnet discovery query yielded no results."; "subnets_searched_for" => ?subnets_searched_for);
queries.iter().for_each(|query| {
self.add_subnet_query(
query.subnet_id,
query.min_ttl,
query.retries + 1,
);
self.add_subnet_query(query.subnet, query.min_ttl, query.retries + 1);
})
}
Ok(r) => {
@@ -824,15 +869,11 @@ impl<TSpec: EthSpec> Discovery<TSpec> {
// Map each subnet query's min_ttl to the set of ENR's returned for that subnet.
queries.iter().for_each(|query| {
// A subnet query has completed. Add back to the queue, incrementing retries.
self.add_subnet_query(
query.subnet_id,
query.min_ttl,
query.retries + 1,
);
self.add_subnet_query(query.subnet, query.min_ttl, query.retries + 1);
// Check the specific subnet against the enr
let subnet_predicate =
subnet_predicate::<TSpec>(vec![query.subnet_id], &self.log);
subnet_predicate::<TSpec>(vec![query.subnet], &self.log);
r.iter()
.filter(|enr| subnet_predicate(enr))
@@ -1037,11 +1078,11 @@ impl<TSpec: EthSpec> NetworkBehaviour for Discovery<TSpec> {
#[cfg(test)]
mod tests {
use super::*;
use crate::rpc::methods::MetaData;
use crate::rpc::methods::{MetaData, MetaDataV2};
use enr::EnrBuilder;
use slog::{o, Drain};
use std::net::UdpSocket;
use types::MinimalEthSpec;
use types::{BitVector, MinimalEthSpec, SubnetId};
type E = MinimalEthSpec;
@@ -1076,10 +1117,11 @@ mod tests {
enr,
9000,
9000,
MetaData {
MetaData::V2(MetaDataV2 {
seq_number: 0,
attnets: Default::default(),
},
syncnets: Default::default(),
}),
vec![],
&log,
);
@@ -1093,12 +1135,12 @@ mod tests {
let mut discovery = build_discovery().await;
let now = Instant::now();
let mut subnet_query = SubnetQuery {
subnet_id: SubnetId::new(1),
subnet: Subnet::Attestation(SubnetId::new(1)),
min_ttl: Some(now),
retries: 0,
};
discovery.add_subnet_query(
subnet_query.subnet_id,
subnet_query.subnet,
subnet_query.min_ttl,
subnet_query.retries,
);
@@ -1109,7 +1151,7 @@ mod tests {
// New query should replace old query
subnet_query.min_ttl = Some(now + Duration::from_secs(1));
discovery.add_subnet_query(subnet_query.subnet_id, subnet_query.min_ttl, 1);
discovery.add_subnet_query(subnet_query.subnet, subnet_query.min_ttl, 1);
subnet_query.retries += 1;
@@ -1122,7 +1164,7 @@ mod tests {
// Retries > MAX_DISCOVERY_RETRY must return immediately without adding
// anything.
discovery.add_subnet_query(
subnet_query.subnet_id,
subnet_query.subnet,
subnet_query.min_ttl,
MAX_DISCOVERY_RETRY + 1,
);
@@ -1140,7 +1182,7 @@ mod tests {
let now = Instant::now();
let subnet_query = SubnetQuery {
subnet_id: SubnetId::new(1),
subnet: Subnet::Attestation(SubnetId::new(1)),
min_ttl: Some(now + Duration::from_secs(10)),
retries: 0,
};
@@ -1174,7 +1216,7 @@ mod tests {
bitfield.set(id, true).unwrap();
}
builder.add_value(BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes());
builder.add_value(ATTESTATION_BITFIELD_ENR_KEY, &bitfield.as_ssz_bytes());
builder.build(&enr_key).unwrap()
}
@@ -1187,12 +1229,12 @@ mod tests {
let query = GroupedQueryType::Subnet(vec![
SubnetQuery {
subnet_id: SubnetId::new(1),
subnet: Subnet::Attestation(SubnetId::new(1)),
min_ttl: instant1,
retries: 0,
},
SubnetQuery {
subnet_id: SubnetId::new(2),
subnet: Subnet::Attestation(SubnetId::new(2)),
min_ttl: instant2,
retries: 0,
},

View File

@@ -1,11 +1,12 @@
///! The subnet predicate used for searching for a particular subnet.
use super::*;
use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield};
use slog::trace;
use std::ops::Deref;
/// Returns the predicate for a given subnet.
pub fn subnet_predicate<TSpec>(
subnet_ids: Vec<SubnetId>,
subnets: Vec<Subnet>,
log: &slog::Logger,
) -> impl Fn(&Enr) -> bool + Send
where
@@ -14,39 +15,33 @@ where
let log_clone = log.clone();
move |enr: &Enr| {
if let Some(bitfield_bytes) = enr.get(BITFIELD_ENR_KEY) {
let bitfield = match BitVector::<TSpec::SubnetBitfieldLength>::from_ssz_bytes(
bitfield_bytes,
) {
Ok(v) => v,
Err(e) => {
warn!(log_clone, "Could not decode ENR bitfield for peer"; "peer_id" => format!("{}", enr.peer_id()), "error" => format!("{:?}", e));
return false;
}
let attestation_bitfield: EnrAttestationBitfield<TSpec> =
match enr.attestation_bitfield::<TSpec>() {
Ok(b) => b,
Err(_e) => return false,
};
let matches: Vec<&SubnetId> = subnet_ids
.iter()
.filter(|id| bitfield.get(**id.deref() as usize).unwrap_or(false))
.collect();
// Pre-fork/fork-boundary enrs may not contain a syncnets field.
// Don't return early here
let sync_committee_bitfield: Result<EnrSyncCommitteeBitfield<TSpec>, _> =
enr.sync_committee_bitfield::<TSpec>();
if matches.is_empty() {
trace!(
log_clone,
"Peer found but not on any of the desired subnets";
"peer_id" => %enr.peer_id()
);
return false;
} else {
trace!(
log_clone,
"Peer found on desired subnet(s)";
"peer_id" => %enr.peer_id(),
"subnets" => ?matches.as_slice()
);
return true;
}
let predicate = subnets.iter().any(|subnet| match subnet {
Subnet::Attestation(s) => attestation_bitfield
.get(*s.deref() as usize)
.unwrap_or(false),
Subnet::SyncCommittee(s) => sync_committee_bitfield
.as_ref()
.map_or(false, |b| b.get(*s.deref() as usize).unwrap_or(false)),
});
if !predicate {
trace!(
log_clone,
"Peer found but not on any of the desired subnets";
"peer_id" => %enr.peer_id()
);
}
false
predicate
}
}

View File

@@ -60,7 +60,10 @@ impl<'de> Deserialize<'de> for PeerIdSerialized {
}
}
pub use crate::types::{error, Enr, GossipTopic, NetworkGlobals, PubsubMessage, SubnetDiscovery};
pub use crate::types::{
error, Enr, EnrSyncCommitteeBitfield, GossipTopic, NetworkGlobals, PubsubMessage, Subnet,
SubnetDiscovery,
};
pub use behaviour::{BehaviourEvent, Gossipsub, PeerRequestId, Request, Response};
pub use config::Config as NetworkConfig;
pub use discovery::{CombinedKeyExt, EnrExt, Eth2Enr};

View File

@@ -1,10 +1,12 @@
//! Implementation of Lighthouse's peer management system.
pub use self::peerdb::*;
use crate::discovery::TARGET_SUBNET_PEERS;
use crate::rpc::{GoodbyeReason, MetaData, Protocol, RPCError, RPCResponseErrorCode};
use crate::types::SyncState;
use crate::{error, metrics, Gossipsub};
use crate::{NetworkConfig, NetworkGlobals, PeerId};
use crate::{Subnet, SubnetDiscovery};
use discv5::Enr;
use futures::prelude::*;
use futures::Stream;
@@ -19,7 +21,7 @@ use std::{
task::{Context, Poll},
time::{Duration, Instant},
};
use types::{EthSpec, SubnetId};
use types::{EthSpec, SyncSubnetId};
pub use libp2p::core::{identity::Keypair, Multiaddr};
@@ -34,7 +36,7 @@ pub use peer_info::{ConnectionDirection, PeerConnectionStatus, PeerConnectionSta
pub use peer_sync_status::{PeerSyncStatus, SyncInfo};
use score::{PeerAction, ReportSource, ScoreState};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::{hash_map::Entry, HashMap};
use std::net::IpAddr;
/// The time in seconds between re-status's peers.
@@ -78,6 +80,11 @@ pub struct PeerManager<TSpec: EthSpec> {
target_peers: usize,
/// The maximum number of peers we allow (exceptions for subnet peers)
max_peers: usize,
/// A collection of sync committee subnets that we need to stay subscribed to.
/// Sync committee subnets are longer term (256 epochs). Hence, we need to re-run
/// discovery queries for subnet peers if we disconnect from existing sync
/// committee subnet peers.
sync_committee_subnets: HashMap<SyncSubnetId, Instant>,
/// The heartbeat interval to perform routine maintenance.
heartbeat: tokio::time::Interval,
/// Keeps track of whether the discovery service is enabled or not.
@@ -108,6 +115,8 @@ pub enum PeerManagerEvent {
UnBanned(PeerId, Vec<IpAddr>),
/// Request the behaviour to discover more peers.
DiscoverPeers,
/// Request the behaviour to discover peers on subnets.
DiscoverSubnetPeers(Vec<SubnetDiscovery>),
}
impl<TSpec: EthSpec> PeerManager<TSpec> {
@@ -127,6 +136,7 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
outbound_ping_peers: HashSetDelay::new(Duration::from_secs(PING_INTERVAL_OUTBOUND)),
status_peers: HashSetDelay::new(Duration::from_secs(STATUS_INTERVAL)),
target_peers: config.target_peers,
sync_committee_subnets: Default::default(),
max_peers: (config.target_peers as f32 * (1.0 + PEER_EXCESS_FACTOR)).ceil() as usize,
heartbeat,
discovery_enabled: !config.disable_discovery,
@@ -264,16 +274,16 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
}
/// Adds a gossipsub subscription to a peer in the peerdb.
pub fn add_subscription(&self, peer_id: &PeerId, subnet_id: SubnetId) {
pub fn add_subscription(&self, peer_id: &PeerId, subnet: Subnet) {
if let Some(info) = self.network_globals.peers.write().peer_info_mut(peer_id) {
info.subnets.insert(subnet_id);
info.subnets.insert(subnet);
}
}
/// Removes a gossipsub subscription to a peer in the peerdb.
pub fn remove_subscription(&self, peer_id: &PeerId, subnet_id: SubnetId) {
pub fn remove_subscription(&self, peer_id: &PeerId, subnet: Subnet) {
if let Some(info) = self.network_globals.peers.write().peer_info_mut(peer_id) {
info.subnets.remove(&subnet_id);
info.subnets.remove(&subnet);
}
}
@@ -284,6 +294,21 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
}
}
/// Insert the sync subnet into list of long lived sync committee subnets that we need to
/// maintain adequate number of peers for.
pub fn add_sync_subnet(&mut self, subnet_id: SyncSubnetId, min_ttl: Instant) {
match self.sync_committee_subnets.entry(subnet_id) {
Entry::Vacant(_) => {
self.sync_committee_subnets.insert(subnet_id, min_ttl);
}
Entry::Occupied(old) => {
if *old.get() < min_ttl {
self.sync_committee_subnets.insert(subnet_id, min_ttl);
}
}
}
}
/* Notifications from the Swarm */
// A peer is being dialed.
@@ -599,9 +624,9 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
// if the sequence number is unknown send an update the meta data of the peer.
if let Some(meta_data) = &peer_info.meta_data {
if meta_data.seq_number < seq {
if *meta_data.seq_number() < seq {
debug!(self.log, "Requesting new metadata from peer";
"peer_id" => %peer_id, "known_seq_no" => meta_data.seq_number, "ping_seq_no" => seq);
"peer_id" => %peer_id, "known_seq_no" => meta_data.seq_number(), "ping_seq_no" => seq);
self.events.push(PeerManagerEvent::MetaData(*peer_id));
}
} else {
@@ -623,9 +648,9 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
// if the sequence number is unknown send update the meta data of the peer.
if let Some(meta_data) = &peer_info.meta_data {
if meta_data.seq_number < seq {
if *meta_data.seq_number() < seq {
debug!(self.log, "Requesting new metadata from peer";
"peer_id" => %peer_id, "known_seq_no" => meta_data.seq_number, "pong_seq_no" => seq);
"peer_id" => %peer_id, "known_seq_no" => meta_data.seq_number(), "pong_seq_no" => seq);
self.events.push(PeerManagerEvent::MetaData(*peer_id));
}
} else {
@@ -643,19 +668,19 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
pub fn meta_data_response(&mut self, peer_id: &PeerId, meta_data: MetaData<TSpec>) {
if let Some(peer_info) = self.network_globals.peers.write().peer_info_mut(peer_id) {
if let Some(known_meta_data) = &peer_info.meta_data {
if known_meta_data.seq_number < meta_data.seq_number {
if *known_meta_data.seq_number() < *meta_data.seq_number() {
debug!(self.log, "Updating peer's metadata";
"peer_id" => %peer_id, "known_seq_no" => known_meta_data.seq_number, "new_seq_no" => meta_data.seq_number);
"peer_id" => %peer_id, "known_seq_no" => known_meta_data.seq_number(), "new_seq_no" => meta_data.seq_number());
} else {
debug!(self.log, "Received old metadata";
"peer_id" => %peer_id, "known_seq_no" => known_meta_data.seq_number, "new_seq_no" => meta_data.seq_number);
"peer_id" => %peer_id, "known_seq_no" => known_meta_data.seq_number(), "new_seq_no" => meta_data.seq_number());
// Updating metadata even in this case to prevent storing
// incorrect `metadata.attnets` for a peer
// incorrect `attnets/syncnets` for a peer
}
} else {
// we have no meta-data for this peer, update
debug!(self.log, "Obtained peer's metadata";
"peer_id" => %peer_id, "new_seq_no" => meta_data.seq_number);
"peer_id" => %peer_id, "new_seq_no" => meta_data.seq_number());
}
peer_info.meta_data = Some(meta_data);
} else {
@@ -965,6 +990,46 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
Ok(())
}
/// Run discovery query for additional sync committee peers if we fall below `TARGET_PEERS`.
fn maintain_sync_committee_peers(&mut self) {
// Remove expired entries
self.sync_committee_subnets
.retain(|_, v| *v > Instant::now());
let subnets_to_discover: Vec<SubnetDiscovery> = self
.sync_committee_subnets
.iter()
.filter_map(|(k, v)| {
if self
.network_globals
.peers
.read()
.good_peers_on_subnet(Subnet::SyncCommittee(*k))
.count()
< TARGET_SUBNET_PEERS
{
Some(SubnetDiscovery {
subnet: Subnet::SyncCommittee(*k),
min_ttl: Some(*v),
})
} else {
None
}
})
.collect();
// request the subnet query from discovery
if !subnets_to_discover.is_empty() {
debug!(
self.log,
"Making subnet queries for maintaining sync committee peers";
"subnets" => ?subnets_to_discover.iter().map(|s| s.subnet).collect::<Vec<_>>()
);
self.events
.push(PeerManagerEvent::DiscoverSubnetPeers(subnets_to_discover));
}
}
/// The Peer manager's heartbeat maintains the peer count and maintains peer reputations.
///
/// It will request discovery queries if the peer count has not reached the desired number of
@@ -989,6 +1054,9 @@ impl<TSpec: EthSpec> PeerManager<TSpec> {
// Updates peer's scores.
self.update_peer_scores();
// Maintain minimum count for sync committee peers.
self.maintain_sync_committee_peers();
// Keep a list of peers we are disconnecting
let mut disconnecting_peers = Vec::new();
@@ -1115,7 +1183,7 @@ mod tests {
use super::*;
use crate::discovery::enr::build_enr;
use crate::discovery::enr_ext::CombinedKeyExt;
use crate::rpc::methods::MetaData;
use crate::rpc::methods::{MetaData, MetaDataV2};
use crate::Enr;
use discv5::enr::CombinedKey;
use slog::{o, Drain};
@@ -1156,10 +1224,11 @@ mod tests {
enr,
9000,
9000,
MetaData {
MetaData::V2(MetaDataV2 {
seq_number: 0,
attnets: Default::default(),
},
syncnets: Default::default(),
}),
vec![],
&log,
);

View File

@@ -1,8 +1,8 @@
use super::client::Client;
use super::score::{PeerAction, Score, ScoreState};
use super::PeerSyncStatus;
use crate::rpc::MetaData;
use crate::Multiaddr;
use crate::{rpc::MetaData, types::Subnet};
use discv5::Enr;
use serde::{
ser::{SerializeStruct, Serializer},
@@ -12,7 +12,7 @@ use std::collections::HashSet;
use std::net::{IpAddr, SocketAddr};
use std::time::Instant;
use strum::AsRefStr;
use types::{EthSpec, SubnetId};
use types::EthSpec;
use PeerConnectionStatus::*;
/// Information about a given connected peer.
@@ -40,7 +40,7 @@ pub struct PeerInfo<T: EthSpec> {
/// connection.
pub meta_data: Option<MetaData<T>>,
/// Subnets the peer is connected to.
pub subnets: HashSet<SubnetId>,
pub subnets: HashSet<Subnet>,
/// The time we would like to retain this peer. After this time, the peer is no longer
/// necessary.
#[serde(skip)]
@@ -84,17 +84,26 @@ impl<T: EthSpec> PeerInfo<T> {
}
}
/// Returns if the peer is subscribed to a given `SubnetId` from the metadata attnets field.
pub fn on_subnet_metadata(&self, subnet_id: SubnetId) -> bool {
/// Returns if the peer is subscribed to a given `Subnet` from the metadata attnets/syncnets field.
pub fn on_subnet_metadata(&self, subnet: &Subnet) -> bool {
if let Some(meta_data) = &self.meta_data {
return meta_data.attnets.get(*subnet_id as usize).unwrap_or(false);
match subnet {
Subnet::Attestation(id) => {
return meta_data.attnets().get(**id as usize).unwrap_or(false)
}
Subnet::SyncCommittee(id) => {
return meta_data
.syncnets()
.map_or(false, |s| s.get(**id as usize).unwrap_or(false))
}
}
}
false
}
/// Returns if the peer is subscribed to a given `SubnetId` from the gossipsub subscriptions.
pub fn on_subnet_gossipsub(&self, subnet_id: SubnetId) -> bool {
self.subnets.contains(&subnet_id)
/// Returns if the peer is subscribed to a given `Subnet` from the gossipsub subscriptions.
pub fn on_subnet_gossipsub(&self, subnet: &Subnet) -> bool {
self.subnets.contains(subnet)
}
/// Returns the seen IP addresses of the peer.

View File

@@ -1,16 +1,19 @@
use super::peer_info::{ConnectionDirection, PeerConnectionStatus, PeerInfo};
use super::peer_sync_status::PeerSyncStatus;
use super::score::{Score, ScoreState};
use crate::multiaddr::{Multiaddr, Protocol};
use crate::rpc::methods::MetaData;
use crate::Enr;
use crate::PeerId;
use crate::{
multiaddr::{Multiaddr, Protocol},
types::Subnet,
};
use rand::seq::SliceRandom;
use slog::{crit, debug, error, trace, warn};
use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::time::Instant;
use types::{EthSpec, SubnetId};
use types::EthSpec;
/// Max number of disconnected nodes to remember.
const MAX_DC_PEERS: usize = 500;
@@ -267,14 +270,14 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
}
/// Gives an iterator of all peers on a given subnet.
pub fn good_peers_on_subnet(&self, subnet_id: SubnetId) -> impl Iterator<Item = &PeerId> {
pub fn good_peers_on_subnet(&self, subnet: Subnet) -> impl Iterator<Item = &PeerId> {
self.peers
.iter()
.filter(move |(_, info)| {
// We check both the metadata and gossipsub data as we only want to count long-lived subscribed peers
info.is_connected()
&& info.on_subnet_metadata(subnet_id)
&& info.on_subnet_gossipsub(subnet_id)
&& info.on_subnet_metadata(&subnet)
&& info.on_subnet_gossipsub(&subnet)
&& info.is_good_gossipsub_peer()
})
.map(|(peer_id, _)| peer_id)
@@ -382,11 +385,11 @@ impl<TSpec: EthSpec> PeerDB<TSpec> {
/// Extends the ttl of all peers on the given subnet that have a shorter
/// min_ttl than what's given.
pub fn extend_peers_on_subnet(&mut self, subnet_id: SubnetId, min_ttl: Instant) {
pub fn extend_peers_on_subnet(&mut self, subnet: &Subnet, min_ttl: Instant) {
let log = &self.log;
self.peers.iter_mut()
.filter(move |(_, info)| {
info.is_connected() && info.on_subnet_metadata(subnet_id) && info.on_subnet_gossipsub(subnet_id)
info.is_connected() && info.on_subnet_metadata(subnet) && info.on_subnet_gossipsub(subnet)
})
.for_each(|(peer_id,info)| {
if info.min_ttl.is_none() || Some(min_ttl) > info.min_ttl {

View File

@@ -181,16 +181,18 @@ where
mod tests {
use super::super::ssz_snappy::*;
use super::*;
use crate::rpc::methods::StatusMessage;
use crate::rpc::protocol::*;
use snap::write::FrameEncoder;
use ssz::Encode;
use std::io::Write;
use types::{Epoch, Hash256, Slot};
use std::sync::Arc;
use types::{ForkContext, Hash256};
use unsigned_varint::codec::Uvi;
type Spec = types::MainnetEthSpec;
fn fork_context() -> ForkContext {
ForkContext::new::<Spec>(types::Slot::new(0), Hash256::zero(), &Spec::default_spec())
}
#[test]
fn test_decode_status_message() {
let message = hex::decode("0054ff060000734e615070590032000006e71e7b54989925efd6c9cbcb8ceb9b5f71216f5137282bf6a1e3b50f64e42d6c7fb347abe07eb0db8200000005029e2800").unwrap();
@@ -200,8 +202,9 @@ mod tests {
let snappy_protocol_id =
ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy);
let fork_context = Arc::new(fork_context());
let mut snappy_outbound_codec =
SSZSnappyOutboundCodec::<Spec>::new(snappy_protocol_id, 1_048_576);
SSZSnappyOutboundCodec::<Spec>::new(snappy_protocol_id, 1_048_576, fork_context);
// remove response code
let mut snappy_buf = buf.clone();
@@ -233,8 +236,10 @@ mod tests {
let snappy_protocol_id =
ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy);
let fork_context = Arc::new(fork_context());
let mut snappy_outbound_codec =
SSZSnappyOutboundCodec::<Spec>::new(snappy_protocol_id, 1_048_576);
SSZSnappyOutboundCodec::<Spec>::new(snappy_protocol_id, 1_048_576, fork_context);
let snappy_decoded_message = snappy_outbound_codec.decode(&mut dst).unwrap_err();
@@ -260,80 +265,34 @@ mod tests {
// Response limits
let limit = protocol_id.rpc_response_limits::<Spec>();
let mut max = encode_len(limit.max + 1);
let mut codec = SSZSnappyOutboundCodec::<Spec>::new(protocol_id.clone(), 1_048_576);
let fork_context = Arc::new(fork_context());
let mut codec = SSZSnappyOutboundCodec::<Spec>::new(
protocol_id.clone(),
1_048_576,
fork_context.clone(),
);
assert_eq!(codec.decode(&mut max).unwrap_err(), RPCError::InvalidData);
let mut min = encode_len(limit.min - 1);
let mut codec = SSZSnappyOutboundCodec::<Spec>::new(protocol_id.clone(), 1_048_576);
let mut codec = SSZSnappyOutboundCodec::<Spec>::new(
protocol_id.clone(),
1_048_576,
fork_context.clone(),
);
assert_eq!(codec.decode(&mut min).unwrap_err(), RPCError::InvalidData);
// Request limits
let limit = protocol_id.rpc_request_limits();
let mut max = encode_len(limit.max + 1);
let mut codec = SSZSnappyOutboundCodec::<Spec>::new(protocol_id.clone(), 1_048_576);
let mut codec = SSZSnappyOutboundCodec::<Spec>::new(
protocol_id.clone(),
1_048_576,
fork_context.clone(),
);
assert_eq!(codec.decode(&mut max).unwrap_err(), RPCError::InvalidData);
let mut min = encode_len(limit.min - 1);
let mut codec = SSZSnappyOutboundCodec::<Spec>::new(protocol_id, 1_048_576);
let mut codec = SSZSnappyOutboundCodec::<Spec>::new(protocol_id, 1_048_576, fork_context);
assert_eq!(codec.decode(&mut min).unwrap_err(), RPCError::InvalidData);
}
#[test]
fn test_decode_malicious_status_message() {
// 10 byte snappy stream identifier
let stream_identifier: &'static [u8] = b"\xFF\x06\x00\x00sNaPpY";
assert_eq!(stream_identifier.len(), 10);
// byte 0(0xFE) is padding chunk type identifier for snappy messages
// byte 1,2,3 are chunk length (little endian)
let malicious_padding: &'static [u8] = b"\xFE\x00\x00\x00";
// Status message is 84 bytes uncompressed. `max_compressed_len` is 32 + 84 + 84/6 = 130.
let status_message_bytes = StatusMessage {
fork_digest: [0; 4],
finalized_root: Hash256::from_low_u64_be(0),
finalized_epoch: Epoch::new(1),
head_root: Hash256::from_low_u64_be(0),
head_slot: Slot::new(1),
}
.as_ssz_bytes();
assert_eq!(status_message_bytes.len(), 84);
assert_eq!(snap::raw::max_compress_len(status_message_bytes.len()), 130);
let mut uvi_codec: Uvi<usize> = Uvi::default();
let mut dst = BytesMut::with_capacity(1024);
// Insert length-prefix
uvi_codec
.encode(status_message_bytes.len(), &mut dst)
.unwrap();
// Insert snappy stream identifier
dst.extend_from_slice(stream_identifier);
// Insert malicious padding of 80 bytes.
for _ in 0..20 {
dst.extend_from_slice(malicious_padding);
}
// Insert payload (42 bytes compressed)
let mut writer = FrameEncoder::new(Vec::new());
writer.write_all(&status_message_bytes).unwrap();
writer.flush().unwrap();
assert_eq!(writer.get_ref().len(), 42);
dst.extend_from_slice(writer.get_ref());
// 10 (for stream identifier) + 80 + 42 = 132 > `max_compressed_len`. Hence, decoding should fail with `InvalidData`.
let snappy_protocol_id =
ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy);
let mut snappy_outbound_codec =
SSZSnappyOutboundCodec::<Spec>::new(snappy_protocol_id, 1_048_576);
let snappy_decoded_message = snappy_outbound_codec.decode(&mut dst).unwrap_err();
assert_eq!(snappy_decoded_message, RPCError::InvalidData);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@
use super::methods::{
GoodbyeReason, RPCCodedResponse, RPCResponseErrorCode, RequestId, ResponseTermination,
};
use super::outbound::OutboundRequestContainer;
use super::protocol::{InboundRequest, Protocol, RPCError, RPCProtocol};
use super::{RPCReceived, RPCSend};
use crate::rpc::outbound::{OutboundFramed, OutboundRequest};
@@ -23,12 +24,13 @@ use smallvec::SmallVec;
use std::{
collections::hash_map::Entry,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
use tokio::time::{sleep_until, Instant as TInstant, Sleep};
use tokio_util::time::{delay_queue, DelayQueue};
use types::EthSpec;
use types::{EthSpec, ForkContext};
/// The time (in seconds) before a substream that is awaiting a response from the user times out.
pub const RESPONSE_TIMEOUT: u64 = 10;
@@ -126,6 +128,9 @@ where
/// This keeps track of the number of attempts.
outbound_io_error_retries: u8,
/// Fork specific info.
fork_context: Arc<ForkContext>,
/// Logger for handling RPC streams
log: slog::Logger,
}
@@ -203,6 +208,7 @@ where
{
pub fn new(
listen_protocol: SubstreamProtocol<RPCProtocol<TSpec>, ()>,
fork_context: Arc<ForkContext>,
log: &slog::Logger,
) -> Self {
RPCHandler {
@@ -219,6 +225,7 @@ where
state: HandlerState::Active,
max_dial_negotiated: 8,
outbound_io_error_retries: 0,
fork_context,
log: log.clone(),
}
}
@@ -308,7 +315,7 @@ where
type OutEvent = HandlerEvent<TSpec>;
type Error = RPCError;
type InboundProtocol = RPCProtocol<TSpec>;
type OutboundProtocol = OutboundRequest<TSpec>;
type OutboundProtocol = OutboundRequestContainer<TSpec>;
type OutboundOpenInfo = (RequestId, OutboundRequest<TSpec>); // Keep track of the id and the request
type InboundOpenInfo = ();
@@ -874,7 +881,14 @@ where
let (id, req) = self.dial_queue.remove(0);
self.dial_queue.shrink_to_fit();
return Poll::Ready(ProtocolsHandlerEvent::OutboundSubstreamRequest {
protocol: SubstreamProtocol::new(req.clone(), ()).map_info(|()| (id, req)),
protocol: SubstreamProtocol::new(
OutboundRequestContainer {
req: req.clone(),
fork_context: self.fork_context.clone(),
},
(),
)
.map_info(|()| (id, req)),
});
}

View File

@@ -1,6 +1,6 @@
//! Available RPC methods types and ids.
use crate::types::EnrBitfield;
use crate::types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield};
use regex::bytes::Regex;
use serde::Serialize;
use ssz_derive::{Decode, Encode};
@@ -10,6 +10,7 @@ use ssz_types::{
};
use std::ops::Deref;
use strum::AsStaticStr;
use superstruct::superstruct;
use types::{Epoch, EthSpec, Hash256, SignedBeaconBlock, Slot};
/// Maximum number of blocks in a single request.
@@ -93,13 +94,23 @@ pub struct Ping {
}
/// The METADATA response structure.
#[derive(Encode, Decode, Clone, Debug, PartialEq, Serialize)]
#[superstruct(
variants(V1, V2),
variant_attributes(
derive(Encode, Decode, Clone, Debug, PartialEq, Serialize),
serde(bound = "T: EthSpec", deny_unknown_fields),
)
)]
#[derive(Clone, Debug, PartialEq, Serialize, Encode)]
#[serde(bound = "T: EthSpec")]
pub struct MetaData<T: EthSpec> {
/// A sequential counter indicating when data gets modified.
pub seq_number: u64,
/// The persistent subnet bitfield.
pub attnets: EnrBitfield<T>,
/// The persistent attestation subnet bitfield.
pub attnets: EnrAttestationBitfield<T>,
/// The persistent sync committee bitfield.
#[superstruct(only(V2))]
pub syncnets: EnrSyncCommitteeBitfield<T>,
}
/// The reason given for a `Goodbye` message.
@@ -360,7 +371,7 @@ impl<T: EthSpec> std::fmt::Display for RPCResponse<T> {
write!(f, "BlocksByRoot: Block slot: {}", block.slot())
}
RPCResponse::Pong(ping) => write!(f, "Pong: {}", ping.data),
RPCResponse::MetaData(metadata) => write!(f, "Metadata: {}", metadata.seq_number),
RPCResponse::MetaData(metadata) => write!(f, "Metadata: {}", metadata.seq_number()),
}
}
}

View File

@@ -15,12 +15,13 @@ use libp2p::{Multiaddr, PeerId};
use rate_limiter::{RPCRateLimiter as RateLimiter, RPCRateLimiterBuilder, RateLimitedErr};
use slog::{crit, debug, o};
use std::marker::PhantomData;
use std::sync::Arc;
use std::task::{Context, Poll};
use std::time::Duration;
use types::EthSpec;
use types::{EthSpec, ForkContext};
pub(crate) use handler::HandlerErr;
pub(crate) use methods::{MetaData, Ping, RPCCodedResponse, RPCResponse};
pub(crate) use methods::{MetaData, MetaDataV1, MetaDataV2, Ping, RPCCodedResponse, RPCResponse};
pub(crate) use protocol::{InboundRequest, RPCProtocol};
pub use handler::SubstreamId;
@@ -101,12 +102,13 @@ pub struct RPC<TSpec: EthSpec> {
limiter: RateLimiter,
/// Queue of events to be processed.
events: Vec<NetworkBehaviourAction<RPCSend<TSpec>, RPCMessage<TSpec>>>,
fork_context: Arc<ForkContext>,
/// Slog logger for RPC behaviour.
log: slog::Logger,
}
impl<TSpec: EthSpec> RPC<TSpec> {
pub fn new(log: slog::Logger) -> Self {
pub fn new(fork_context: Arc<ForkContext>, log: slog::Logger) -> Self {
let log = log.new(o!("service" => "libp2p_rpc"));
let limiter = RPCRateLimiterBuilder::new()
.n_every(Protocol::MetaData, 2, Duration::from_secs(5))
@@ -124,6 +126,7 @@ impl<TSpec: EthSpec> RPC<TSpec> {
RPC {
limiter,
events: Vec::new(),
fork_context,
log,
}
}
@@ -182,10 +185,12 @@ where
RPCHandler::new(
SubstreamProtocol::new(
RPCProtocol {
fork_context: self.fork_context.clone(),
phantom: PhantomData,
},
(),
),
self.fork_context.clone(),
&self.log,
)
}

View File

@@ -14,16 +14,23 @@ use futures::future::BoxFuture;
use futures::prelude::{AsyncRead, AsyncWrite};
use futures::{FutureExt, SinkExt};
use libp2p::core::{OutboundUpgrade, UpgradeInfo};
use std::sync::Arc;
use tokio_util::{
codec::Framed,
compat::{Compat, FuturesAsyncReadCompatExt},
};
use types::EthSpec;
use types::{EthSpec, ForkContext};
/* Outbound request */
// Combines all the RPC requests into a single enum to implement `UpgradeInfo` and
// `OutboundUpgrade`
#[derive(Debug, Clone)]
pub struct OutboundRequestContainer<TSpec: EthSpec> {
pub req: OutboundRequest<TSpec>,
pub fork_context: Arc<ForkContext>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum OutboundRequest<TSpec: EthSpec> {
Status(StatusMessage),
@@ -34,13 +41,13 @@ pub enum OutboundRequest<TSpec: EthSpec> {
MetaData(PhantomData<TSpec>),
}
impl<TSpec: EthSpec> UpgradeInfo for OutboundRequest<TSpec> {
impl<TSpec: EthSpec> UpgradeInfo for OutboundRequestContainer<TSpec> {
type Info = ProtocolId;
type InfoIter = Vec<Self::Info>;
// add further protocols as we support more encodings/versions
fn protocol_info(&self) -> Self::InfoIter {
self.supported_protocols()
self.req.supported_protocols()
}
}
@@ -59,26 +66,23 @@ impl<TSpec: EthSpec> OutboundRequest<TSpec> {
Version::V1,
Encoding::SSZSnappy,
)],
OutboundRequest::BlocksByRange(_) => vec![ProtocolId::new(
Protocol::BlocksByRange,
Version::V1,
Encoding::SSZSnappy,
)],
OutboundRequest::BlocksByRoot(_) => vec![ProtocolId::new(
Protocol::BlocksByRoot,
Version::V1,
Encoding::SSZSnappy,
)],
OutboundRequest::BlocksByRange(_) => vec![
ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy),
],
OutboundRequest::BlocksByRoot(_) => vec![
ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy),
],
OutboundRequest::Ping(_) => vec![ProtocolId::new(
Protocol::Ping,
Version::V1,
Encoding::SSZSnappy,
)],
OutboundRequest::MetaData(_) => vec![ProtocolId::new(
Protocol::MetaData,
Version::V1,
Encoding::SSZSnappy,
)],
OutboundRequest::MetaData(_) => vec![
ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy),
],
}
}
@@ -130,7 +134,7 @@ impl<TSpec: EthSpec> OutboundRequest<TSpec> {
pub type OutboundFramed<TSocket, TSpec> = Framed<Compat<TSocket>, OutboundCodec<TSpec>>;
impl<TSocket, TSpec> OutboundUpgrade<TSocket> for OutboundRequest<TSpec>
impl<TSocket, TSpec> OutboundUpgrade<TSocket> for OutboundRequestContainer<TSpec>
where
TSpec: EthSpec + Send + 'static,
TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static,
@@ -147,6 +151,7 @@ where
let ssz_snappy_codec = BaseOutboundCodec::new(SSZSnappyOutboundCodec::new(
protocol,
usize::max_value(),
self.fork_context.clone(),
));
OutboundCodec::SSZSnappy(ssz_snappy_codec)
}
@@ -155,7 +160,7 @@ where
let mut socket = Framed::new(socket, codec);
async {
socket.send(self).await?;
socket.send(self.req).await?;
socket.close().await?;
Ok(socket)
}

View File

@@ -12,6 +12,7 @@ use ssz::Encode;
use ssz_types::VariableList;
use std::io;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
use strum::{AsStaticRef, AsStaticStr};
use tokio_io_timeout::TimeoutStream;
@@ -19,19 +20,35 @@ use tokio_util::{
codec::Framed,
compat::{Compat, FuturesAsyncReadCompatExt},
};
use types::{BeaconBlock, EthSpec, Hash256, MainnetEthSpec, Signature, SignedBeaconBlock};
use types::{
BeaconBlock, BeaconBlockAltair, BeaconBlockBase, EthSpec, ForkContext, Hash256, MainnetEthSpec,
Signature, SignedBeaconBlock,
};
lazy_static! {
// Note: Hardcoding the `EthSpec` type for `SignedBeaconBlock` as min/max values is
// same across different `EthSpec` implementations.
pub static ref SIGNED_BEACON_BLOCK_MIN: usize = SignedBeaconBlock::<MainnetEthSpec>::from_block(
BeaconBlock::empty(&MainnetEthSpec::default_spec()),
pub static ref SIGNED_BEACON_BLOCK_BASE_MIN: usize = SignedBeaconBlock::<MainnetEthSpec>::from_block(
BeaconBlock::Base(BeaconBlockBase::<MainnetEthSpec>::empty(&MainnetEthSpec::default_spec())),
Signature::empty(),
)
.as_ssz_bytes()
.len();
pub static ref SIGNED_BEACON_BLOCK_MAX: usize = SignedBeaconBlock::<MainnetEthSpec>::from_block(
BeaconBlock::full(&MainnetEthSpec::default_spec()),
pub static ref SIGNED_BEACON_BLOCK_BASE_MAX: usize = SignedBeaconBlock::<MainnetEthSpec>::from_block(
BeaconBlock::Base(BeaconBlockBase::full(&MainnetEthSpec::default_spec())),
Signature::empty(),
)
.as_ssz_bytes()
.len();
pub static ref SIGNED_BEACON_BLOCK_ALTAIR_MIN: usize = SignedBeaconBlock::<MainnetEthSpec>::from_block(
BeaconBlock::Altair(BeaconBlockAltair::<MainnetEthSpec>::empty(&MainnetEthSpec::default_spec())),
Signature::empty(),
)
.as_ssz_bytes()
.len();
pub static ref SIGNED_BEACON_BLOCK_ALTAIR_MAX: usize = SignedBeaconBlock::<MainnetEthSpec>::from_block(
BeaconBlock::Altair(BeaconBlockAltair::full(&MainnetEthSpec::default_spec())),
Signature::empty(),
)
.as_ssz_bytes()
@@ -95,6 +112,8 @@ pub enum Protocol {
pub enum Version {
/// Version 1 of RPC
V1,
/// Version 2 of RPC
V2,
}
/// RPC Encondings supported.
@@ -130,6 +149,7 @@ impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let repr = match self {
Version::V1 => "1",
Version::V2 => "2",
};
f.write_str(repr)
}
@@ -137,6 +157,7 @@ impl std::fmt::Display for Version {
#[derive(Debug, Clone)]
pub struct RPCProtocol<TSpec: EthSpec> {
pub fork_context: Arc<ForkContext>,
pub phantom: PhantomData<TSpec>,
}
@@ -149,9 +170,13 @@ impl<TSpec: EthSpec> UpgradeInfo for RPCProtocol<TSpec> {
vec![
ProtocolId::new(Protocol::Status, Version::V1, Encoding::SSZSnappy),
ProtocolId::new(Protocol::Goodbye, Version::V1, Encoding::SSZSnappy),
// V2 variants have higher preference then V1
ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy),
ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy),
ProtocolId::new(Protocol::Ping, Version::V1, Encoding::SSZSnappy),
ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy),
]
}
@@ -226,22 +251,49 @@ impl ProtocolId {
<StatusMessage as Encode>::ssz_fixed_len(),
),
Protocol::Goodbye => RpcLimits::new(0, 0), // Goodbye request has no response
Protocol::BlocksByRange => {
RpcLimits::new(*SIGNED_BEACON_BLOCK_MIN, *SIGNED_BEACON_BLOCK_MAX)
}
Protocol::BlocksByRoot => {
RpcLimits::new(*SIGNED_BEACON_BLOCK_MIN, *SIGNED_BEACON_BLOCK_MAX)
}
Protocol::BlocksByRange => RpcLimits::new(
std::cmp::min(
*SIGNED_BEACON_BLOCK_ALTAIR_MIN,
*SIGNED_BEACON_BLOCK_BASE_MIN,
),
std::cmp::max(
*SIGNED_BEACON_BLOCK_ALTAIR_MAX,
*SIGNED_BEACON_BLOCK_BASE_MAX,
),
),
Protocol::BlocksByRoot => RpcLimits::new(
std::cmp::min(
*SIGNED_BEACON_BLOCK_ALTAIR_MIN,
*SIGNED_BEACON_BLOCK_BASE_MIN,
),
std::cmp::max(
*SIGNED_BEACON_BLOCK_ALTAIR_MAX,
*SIGNED_BEACON_BLOCK_BASE_MAX,
),
),
Protocol::Ping => RpcLimits::new(
<Ping as Encode>::ssz_fixed_len(),
<Ping as Encode>::ssz_fixed_len(),
),
Protocol::MetaData => RpcLimits::new(
<MetaData<T> as Encode>::ssz_fixed_len(),
<MetaData<T> as Encode>::ssz_fixed_len(),
<MetaDataV1<T> as Encode>::ssz_fixed_len(),
<MetaDataV2<T> as Encode>::ssz_fixed_len(),
),
}
}
/// Returns `true` if the given `ProtocolId` should expect `context_bytes` in the
/// beginning of the stream, else returns `false`.
pub fn has_context_bytes(&self) -> bool {
if self.version == Version::V2 {
match self.message_name {
Protocol::BlocksByRange | Protocol::BlocksByRoot => return true,
_ => return false,
}
}
false
}
}
/// An RPC protocol ID.
@@ -292,8 +344,11 @@ where
let socket = socket.compat();
let codec = match protocol.encoding {
Encoding::SSZSnappy => {
let ssz_snappy_codec =
BaseInboundCodec::new(SSZSnappyInboundCodec::new(protocol, MAX_RPC_SIZE));
let ssz_snappy_codec = BaseInboundCodec::new(SSZSnappyInboundCodec::new(
protocol,
MAX_RPC_SIZE,
self.fork_context.clone(),
));
InboundCodec::SSZSnappy(ssz_snappy_codec)
}
};
@@ -359,26 +414,25 @@ impl<TSpec: EthSpec> InboundRequest<TSpec> {
Version::V1,
Encoding::SSZSnappy,
)],
InboundRequest::BlocksByRange(_) => vec![ProtocolId::new(
Protocol::BlocksByRange,
Version::V1,
Encoding::SSZSnappy,
)],
InboundRequest::BlocksByRoot(_) => vec![ProtocolId::new(
Protocol::BlocksByRoot,
Version::V1,
Encoding::SSZSnappy,
)],
InboundRequest::BlocksByRange(_) => vec![
// V2 has higher preference when negotiating a stream
ProtocolId::new(Protocol::BlocksByRange, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::BlocksByRange, Version::V1, Encoding::SSZSnappy),
],
InboundRequest::BlocksByRoot(_) => vec![
// V2 has higher preference when negotiating a stream
ProtocolId::new(Protocol::BlocksByRoot, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::BlocksByRoot, Version::V1, Encoding::SSZSnappy),
],
InboundRequest::Ping(_) => vec![ProtocolId::new(
Protocol::Ping,
Version::V1,
Encoding::SSZSnappy,
)],
InboundRequest::MetaData(_) => vec![ProtocolId::new(
Protocol::MetaData,
Version::V1,
Encoding::SSZSnappy,
)],
InboundRequest::MetaData(_) => vec![
ProtocolId::new(Protocol::MetaData, Version::V2, Encoding::SSZSnappy),
ProtocolId::new(Protocol::MetaData, Version::V1, Encoding::SSZSnappy),
],
}
}
@@ -424,8 +478,6 @@ impl<TSpec: EthSpec> InboundRequest<TSpec> {
}
}
/* RPC Response type - used for outbound upgrades */
/// Error in RPC Encoding/Decoding.
#[derive(Debug, Clone, PartialEq, AsStaticStr)]
#[strum(serialize_all = "snake_case")]

View File

@@ -3,8 +3,10 @@ use crate::behaviour::{
};
use crate::discovery::enr;
use crate::multiaddr::Protocol;
use crate::rpc::{GoodbyeReason, MetaData, RPCResponseErrorCode, RequestId};
use crate::types::{error, EnrBitfield, GossipKind};
use crate::rpc::{
GoodbyeReason, MetaData, MetaDataV1, MetaDataV2, RPCResponseErrorCode, RequestId,
};
use crate::types::{error, EnrAttestationBitfield, EnrSyncCommitteeBitfield, GossipKind};
use crate::EnrExt;
use crate::{NetworkConfig, NetworkGlobals, PeerAction, ReportSource};
use futures::prelude::*;
@@ -25,7 +27,7 @@ use std::io::prelude::*;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use types::{ChainSpec, EnrForkId, EthSpec};
use types::{ChainSpec, EnrForkId, EthSpec, ForkContext};
use crate::peer_manager::{MIN_OUTBOUND_ONLY_FACTOR, PEER_EXCESS_FACTOR};
@@ -66,6 +68,7 @@ impl<TSpec: EthSpec> Service<TSpec> {
config: &NetworkConfig,
enr_fork_id: EnrForkId,
log: &Logger,
fork_context: Arc<ForkContext>,
chain_spec: &ChainSpec,
) -> error::Result<(Arc<NetworkGlobals<TSpec>>, Self)> {
let log = log.new(o!("service"=> "libp2p"));
@@ -112,9 +115,10 @@ impl<TSpec: EthSpec> Service<TSpec> {
// Lighthouse network behaviour
let behaviour = Behaviour::new(
&local_keypair,
config,
config.clone(),
network_globals.clone(),
&log,
fork_context,
chain_spec,
)
.await?;
@@ -547,37 +551,57 @@ fn load_or_build_metadata<E: EthSpec>(
network_dir: &std::path::Path,
log: &slog::Logger,
) -> MetaData<E> {
// Default metadata
let mut meta_data = MetaData {
// We load a V2 metadata version by default (regardless of current fork)
// since a V2 metadata can be converted to V1. The RPC encoder is responsible
// for sending the correct metadata version based on the negotiated protocol version.
let mut meta_data = MetaDataV2 {
seq_number: 0,
attnets: EnrBitfield::<E>::default(),
attnets: EnrAttestationBitfield::<E>::default(),
syncnets: EnrSyncCommitteeBitfield::<E>::default(),
};
// Read metadata from persisted file if available
let metadata_path = network_dir.join(METADATA_FILENAME);
if let Ok(mut metadata_file) = File::open(metadata_path) {
let mut metadata_ssz = Vec::new();
if metadata_file.read_to_end(&mut metadata_ssz).is_ok() {
match MetaData::<E>::from_ssz_bytes(&metadata_ssz) {
// Attempt to read a MetaDataV2 version from the persisted file,
// if that fails, read MetaDataV1
match MetaDataV2::<E>::from_ssz_bytes(&metadata_ssz) {
Ok(persisted_metadata) => {
meta_data.seq_number = persisted_metadata.seq_number;
// Increment seq number if persisted attnet is not default
if persisted_metadata.attnets != meta_data.attnets {
if persisted_metadata.attnets != meta_data.attnets
|| persisted_metadata.syncnets != meta_data.syncnets
{
meta_data.seq_number += 1;
}
debug!(log, "Loaded metadata from disk");
}
Err(e) => {
debug!(
log,
"Metadata from file could not be decoded";
"error" => ?e,
);
Err(_) => {
match MetaDataV1::<E>::from_ssz_bytes(&metadata_ssz) {
Ok(persisted_metadata) => {
let persisted_metadata = MetaData::V1(persisted_metadata);
// Increment seq number as the persisted metadata version is updated
meta_data.seq_number = *persisted_metadata.seq_number() + 1;
debug!(log, "Loaded metadata from disk");
}
Err(e) => {
debug!(
log,
"Metadata from file could not be decoded";
"error" => ?e,
);
}
}
}
}
}
};
debug!(log, "Metadata sequence number"; "seq_num" => meta_data.seq_number);
// Wrap the MetaData
let meta_data = MetaData::V2(meta_data);
debug!(log, "Metadata sequence number"; "seq_num" => meta_data.seq_number());
save_metadata_to_disk(network_dir, meta_data.clone(), log);
meta_data
}

View File

@@ -7,13 +7,13 @@ mod topics;
use types::{BitVector, EthSpec};
#[allow(type_alias_bounds)]
pub type EnrBitfield<T: EthSpec> = BitVector<T::SubnetBitfieldLength>;
pub type EnrAttestationBitfield<T> = BitVector<<T as EthSpec>::SubnetBitfieldLength>;
pub type EnrSyncCommitteeBitfield<T> = BitVector<<T as EthSpec>::SyncCommitteeSubnetCount>;
pub type Enr = discv5::enr::Enr<discv5::enr::CombinedKey>;
pub use globals::NetworkGlobals;
pub use pubsub::{PubsubMessage, SnappyTransform};
pub use subnet::SubnetDiscovery;
pub use subnet::{Subnet, SubnetDiscovery};
pub use sync_state::SyncState;
pub use topics::{subnet_id_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, CORE_TOPICS};
pub use topics::{subnet_from_topic_hash, GossipEncoding, GossipKind, GossipTopic, CORE_TOPICS};

View File

@@ -7,10 +7,10 @@ use snap::raw::{decompress_len, Decoder, Encoder};
use ssz::{Decode, Encode};
use std::boxed::Box;
use std::io::{Error, ErrorKind};
use types::SubnetId;
use types::{
Attestation, AttesterSlashing, EthSpec, ProposerSlashing, SignedAggregateAndProof,
SignedBeaconBlock, SignedBeaconBlockBase, SignedVoluntaryExit,
Attestation, AttesterSlashing, EthSpec, ForkContext, ForkName, ProposerSlashing,
SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase,
SignedContributionAndProof, SignedVoluntaryExit, SubnetId, SyncCommitteeMessage, SyncSubnetId,
};
#[derive(Debug, Clone, PartialEq)]
@@ -27,6 +27,10 @@ pub enum PubsubMessage<T: EthSpec> {
ProposerSlashing(Box<ProposerSlashing>),
/// Gossipsub message providing notification of a new attester slashing.
AttesterSlashing(Box<AttesterSlashing<T>>),
/// Gossipsub message providing notification of partially aggregated sync committee signatures.
SignedContributionAndProof(Box<SignedContributionAndProof<T>>),
/// Gossipsub message providing notification of unaggregated sync committee signatures with its subnet id.
SyncCommitteeMessage(Box<(SyncSubnetId, SyncCommitteeMessage)>),
}
// Implements the `DataTransform` trait of gossipsub to employ snappy compression
@@ -107,6 +111,8 @@ impl<T: EthSpec> PubsubMessage<T> {
PubsubMessage::VoluntaryExit(_) => GossipKind::VoluntaryExit,
PubsubMessage::ProposerSlashing(_) => GossipKind::ProposerSlashing,
PubsubMessage::AttesterSlashing(_) => GossipKind::AttesterSlashing,
PubsubMessage::SignedContributionAndProof(_) => GossipKind::SignedContributionAndProof,
PubsubMessage::SyncCommitteeMessage(data) => GossipKind::SyncCommitteeMessage(data.0),
}
}
@@ -114,7 +120,11 @@ impl<T: EthSpec> PubsubMessage<T> {
/* Note: This is assuming we are not hashing topics. If we choose to hash topics, these will
* need to be modified.
*/
pub fn decode(topic: &TopicHash, data: &[u8]) -> Result<Self, String> {
pub fn decode(
topic: &TopicHash,
data: &[u8],
fork_context: &ForkContext,
) -> Result<Self, String> {
match GossipTopic::decode(topic.as_str()) {
Err(_) => Err(format!("Unknown gossipsub topic: {:?}", topic)),
Ok(gossip_topic) => {
@@ -141,11 +151,23 @@ impl<T: EthSpec> PubsubMessage<T> {
))))
}
GossipKind::BeaconBlock => {
// FIXME(altair): support Altair blocks
let beacon_block = SignedBeaconBlock::Base(
SignedBeaconBlockBase::from_ssz_bytes(data)
.map_err(|e| format!("{:?}", e))?,
);
let beacon_block =
match fork_context.from_context_bytes(gossip_topic.fork_digest) {
Some(ForkName::Base) => SignedBeaconBlock::<T>::Base(
SignedBeaconBlockBase::from_ssz_bytes(data)
.map_err(|e| format!("{:?}", e))?,
),
Some(ForkName::Altair) => SignedBeaconBlock::<T>::Altair(
SignedBeaconBlockAltair::from_ssz_bytes(data)
.map_err(|e| format!("{:?}", e))?,
),
None => {
return Err(format!(
"Unknown gossipsub fork digest: {:?}",
gossip_topic.fork_digest
))
}
};
Ok(PubsubMessage::BeaconBlock(Box::new(beacon_block)))
}
GossipKind::VoluntaryExit => {
@@ -163,6 +185,21 @@ impl<T: EthSpec> PubsubMessage<T> {
.map_err(|e| format!("{:?}", e))?;
Ok(PubsubMessage::AttesterSlashing(Box::new(attester_slashing)))
}
GossipKind::SignedContributionAndProof => {
let sync_aggregate = SignedContributionAndProof::from_ssz_bytes(data)
.map_err(|e| format!("{:?}", e))?;
Ok(PubsubMessage::SignedContributionAndProof(Box::new(
sync_aggregate,
)))
}
GossipKind::SyncCommitteeMessage(subnet_id) => {
let sync_committee = SyncCommitteeMessage::from_ssz_bytes(data)
.map_err(|e| format!("{:?}", e))?;
Ok(PubsubMessage::SyncCommitteeMessage(Box::new((
*subnet_id,
sync_committee,
))))
}
}
}
}
@@ -182,6 +219,8 @@ impl<T: EthSpec> PubsubMessage<T> {
PubsubMessage::ProposerSlashing(data) => data.as_ssz_bytes(),
PubsubMessage::AttesterSlashing(data) => data.as_ssz_bytes(),
PubsubMessage::Attestation(data) => data.1.as_ssz_bytes(),
PubsubMessage::SignedContributionAndProof(data) => data.as_ssz_bytes(),
PubsubMessage::SyncCommitteeMessage(data) => data.1.as_ssz_bytes(),
}
}
}
@@ -210,6 +249,12 @@ impl<T: EthSpec> std::fmt::Display for PubsubMessage<T> {
PubsubMessage::VoluntaryExit(_data) => write!(f, "Voluntary Exit"),
PubsubMessage::ProposerSlashing(_data) => write!(f, "Proposer Slashing"),
PubsubMessage::AttesterSlashing(_data) => write!(f, "Attester Slashing"),
PubsubMessage::SignedContributionAndProof(_) => {
write!(f, "Signed Contribution and Proof")
}
PubsubMessage::SyncCommitteeMessage(data) => {
write!(f, "Sync committee message: subnet_id: {}", *data.0)
}
}
}
}

View File

@@ -1,9 +1,28 @@
use serde::Serialize;
use std::time::Instant;
use types::SubnetId;
use types::{SubnetId, SyncSubnetId};
/// Represents a subnet on an attestation or sync committee `SubnetId`.
///
/// Used for subscribing to the appropriate gossipsub subnets and mark
/// appropriate metadata bitfields.
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)]
pub enum Subnet {
/// Represents a gossipsub attestation subnet and the metadata `attnets` field.
Attestation(SubnetId),
/// Represents a gossipsub sync committee subnet and the metadata `syncnets` field.
SyncCommittee(SyncSubnetId),
}
/// A subnet to discover peers on along with the instant after which it's no longer useful.
#[derive(Debug, Clone)]
pub struct SubnetDiscovery {
pub subnet_id: SubnetId,
pub subnet: Subnet,
pub min_ttl: Option<Instant>,
}
impl PartialEq for SubnetDiscovery {
fn eq(&self, other: &SubnetDiscovery) -> bool {
self.subnet.eq(&other.subnet)
}
}

View File

@@ -1,7 +1,9 @@
use libp2p::gossipsub::{IdentTopic as Topic, TopicHash};
use serde_derive::{Deserialize, Serialize};
use strum::AsRefStr;
use types::SubnetId;
use types::{SubnetId, SyncSubnetId};
use crate::Subnet;
/// The gossipsub topic names.
// These constants form a topic name of the form /TOPIC_PREFIX/TOPIC/ENCODING_POSTFIX
@@ -14,13 +16,16 @@ pub const BEACON_ATTESTATION_PREFIX: &str = "beacon_attestation_";
pub const VOLUNTARY_EXIT_TOPIC: &str = "voluntary_exit";
pub const PROPOSER_SLASHING_TOPIC: &str = "proposer_slashing";
pub const ATTESTER_SLASHING_TOPIC: &str = "attester_slashing";
pub const SIGNED_CONTRIBUTION_AND_PROOF_TOPIC: &str = "sync_committee_contribution_and_proof";
pub const SYNC_COMMITTEE_PREFIX_TOPIC: &str = "sync_committee_";
pub const CORE_TOPICS: [GossipKind; 5] = [
pub const CORE_TOPICS: [GossipKind; 6] = [
GossipKind::BeaconBlock,
GossipKind::BeaconAggregateAndProof,
GossipKind::VoluntaryExit,
GossipKind::ProposerSlashing,
GossipKind::AttesterSlashing,
GossipKind::SignedContributionAndProof,
];
/// A gossipsub topic which encapsulates the type of messages that should be sent and received over
@@ -30,7 +35,7 @@ pub struct GossipTopic {
/// The encoding of the topic.
encoding: GossipEncoding,
/// The fork digest of the topic,
fork_digest: [u8; 4],
pub fork_digest: [u8; 4],
/// The kind of topic.
kind: GossipKind,
}
@@ -53,12 +58,20 @@ pub enum GossipKind {
ProposerSlashing,
/// Topic for publishing attester slashings.
AttesterSlashing,
/// Topic for publishing partially aggregated sync committee signatures.
SignedContributionAndProof,
/// Topic for publishing unaggregated sync committee signatures on a particular subnet.
#[strum(serialize = "sync_committee")]
SyncCommitteeMessage(SyncSubnetId),
}
impl std::fmt::Display for GossipKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GossipKind::Attestation(subnet_id) => write!(f, "beacon_attestation_{}", **subnet_id),
GossipKind::SyncCommitteeMessage(subnet_id) => {
write!(f, "sync_committee_{}", **subnet_id)
}
x => f.write_str(x.as_ref()),
}
}
@@ -124,11 +137,15 @@ impl GossipTopic {
let kind = match topic_parts[3] {
BEACON_BLOCK_TOPIC => GossipKind::BeaconBlock,
BEACON_AGGREGATE_AND_PROOF_TOPIC => GossipKind::BeaconAggregateAndProof,
SIGNED_CONTRIBUTION_AND_PROOF_TOPIC => GossipKind::SignedContributionAndProof,
VOLUNTARY_EXIT_TOPIC => GossipKind::VoluntaryExit,
PROPOSER_SLASHING_TOPIC => GossipKind::ProposerSlashing,
ATTESTER_SLASHING_TOPIC => GossipKind::AttesterSlashing,
topic => match committee_topic_index(topic) {
Some(subnet_id) => GossipKind::Attestation(subnet_id),
Some(subnet) => match subnet {
Subnet::Attestation(s) => GossipKind::Attestation(s),
Subnet::SyncCommittee(s) => GossipKind::SyncCommitteeMessage(s),
},
None => return Err(format!("Unknown topic: {}", topic)),
},
};
@@ -163,6 +180,10 @@ impl From<GossipTopic> for String {
GossipKind::ProposerSlashing => PROPOSER_SLASHING_TOPIC.into(),
GossipKind::AttesterSlashing => ATTESTER_SLASHING_TOPIC.into(),
GossipKind::Attestation(index) => format!("{}{}", BEACON_ATTESTATION_PREFIX, *index,),
GossipKind::SignedContributionAndProof => SIGNED_CONTRIBUTION_AND_PROOF_TOPIC.into(),
GossipKind::SyncCommitteeMessage(index) => {
format!("{}{}", SYNC_COMMITTEE_PREFIX_TOPIC, *index)
}
};
format!(
"/{}/{}/{}/{}",
@@ -174,32 +195,72 @@ impl From<GossipTopic> for String {
}
}
impl From<SubnetId> for GossipKind {
fn from(subnet_id: SubnetId) -> Self {
GossipKind::Attestation(subnet_id)
impl std::fmt::Display for GossipTopic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let encoding = match self.encoding {
GossipEncoding::SSZSnappy => SSZ_SNAPPY_ENCODING_POSTFIX,
};
let kind = match self.kind {
GossipKind::BeaconBlock => BEACON_BLOCK_TOPIC.into(),
GossipKind::BeaconAggregateAndProof => BEACON_AGGREGATE_AND_PROOF_TOPIC.into(),
GossipKind::VoluntaryExit => VOLUNTARY_EXIT_TOPIC.into(),
GossipKind::ProposerSlashing => PROPOSER_SLASHING_TOPIC.into(),
GossipKind::AttesterSlashing => ATTESTER_SLASHING_TOPIC.into(),
GossipKind::Attestation(index) => format!("{}{}", BEACON_ATTESTATION_PREFIX, *index,),
GossipKind::SignedContributionAndProof => SIGNED_CONTRIBUTION_AND_PROOF_TOPIC.into(),
GossipKind::SyncCommitteeMessage(index) => {
format!("{}{}", SYNC_COMMITTEE_PREFIX_TOPIC, *index)
}
};
write!(
f,
"/{}/{}/{}/{}",
TOPIC_PREFIX,
hex::encode(self.fork_digest),
kind,
encoding
)
}
}
impl From<Subnet> for GossipKind {
fn from(subnet_id: Subnet) -> Self {
match subnet_id {
Subnet::Attestation(s) => GossipKind::Attestation(s),
Subnet::SyncCommittee(s) => GossipKind::SyncCommitteeMessage(s),
}
}
}
// helper functions
/// Get subnet id from an attestation subnet topic hash.
pub fn subnet_id_from_topic_hash(topic_hash: &TopicHash) -> Option<SubnetId> {
pub fn subnet_from_topic_hash(topic_hash: &TopicHash) -> Option<Subnet> {
let gossip_topic = GossipTopic::decode(topic_hash.as_str()).ok()?;
if let GossipKind::Attestation(subnet_id) = gossip_topic.kind() {
return Some(*subnet_id);
match gossip_topic.kind() {
GossipKind::Attestation(subnet_id) => Some(Subnet::Attestation(*subnet_id)),
GossipKind::SyncCommitteeMessage(subnet_id) => Some(Subnet::SyncCommittee(*subnet_id)),
_ => None,
}
None
}
// Determines if a string is a committee topic.
fn committee_topic_index(topic: &str) -> Option<SubnetId> {
// Determines if a string is an attestation or sync committee topic.
fn committee_topic_index(topic: &str) -> Option<Subnet> {
if topic.starts_with(BEACON_ATTESTATION_PREFIX) {
return Some(SubnetId::new(
return Some(Subnet::Attestation(SubnetId::new(
topic
.trim_start_matches(BEACON_ATTESTATION_PREFIX)
.parse::<u64>()
.ok()?,
));
)));
} else if topic.starts_with(SYNC_COMMITTEE_PREFIX_TOPIC) {
return Some(Subnet::SyncCommittee(SyncSubnetId::new(
topic
.trim_start_matches(SYNC_COMMITTEE_PREFIX_TOPIC)
.parse::<u64>()
.ok()?,
)));
}
None
}
@@ -222,7 +283,9 @@ mod tests {
for kind in [
BeaconBlock,
BeaconAggregateAndProof,
SignedContributionAndProof,
Attestation(SubnetId::new(42)),
SyncCommitteeMessage(SyncSubnetId::new(42)),
VoluntaryExit,
ProposerSlashing,
AttesterSlashing,
@@ -292,14 +355,20 @@ mod tests {
}
#[test]
fn test_subnet_id_from_topic_hash() {
fn test_subnet_from_topic_hash() {
let topic_hash = TopicHash::from_raw("/eth2/e1925f3b/beacon_block/ssz_snappy");
assert!(subnet_id_from_topic_hash(&topic_hash).is_none());
assert!(subnet_from_topic_hash(&topic_hash).is_none());
let topic_hash = TopicHash::from_raw("/eth2/e1925f3b/beacon_attestation_42/ssz_snappy");
assert_eq!(
subnet_id_from_topic_hash(&topic_hash),
Some(SubnetId::new(42))
subnet_from_topic_hash(&topic_hash),
Some(Subnet::Attestation(SubnetId::new(42)))
);
let topic_hash = TopicHash::from_raw("/eth2/e1925f3b/sync_committee_42/ssz_snappy");
assert_eq!(
subnet_from_topic_hash(&topic_hash),
Some(Subnet::SyncCommittee(SyncSubnetId::new(42)))
);
}
@@ -314,6 +383,11 @@ mod tests {
"beacon_attestation",
Attestation(SubnetId::new(42)).as_ref()
);
assert_eq!(
"sync_committee",
SyncCommitteeMessage(SyncSubnetId::new(42)).as_ref()
);
assert_eq!("voluntary_exit", VoluntaryExit.as_ref());
assert_eq!("proposer_slashing", ProposerSlashing.as_ref());
assert_eq!("attester_slashing", AttesterSlashing.as_ref());