From 2c0c43394042b2cf8c6d30f76b38a4c013b4deb6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 11 Nov 2024 19:41:19 -0600 Subject: [PATCH] Add ValidatorInfo to the PeerContacts. Do basic verification --- Cargo.lock | 1 + dht/Cargo.toml | 1 + dht/src/lib.rs | 89 ++++++++--- network-libp2p/src/behaviour.rs | 11 +- .../src/connection_pool/behaviour.rs | 4 +- network-libp2p/src/dht.rs | 19 ++- network-libp2p/src/discovery/behaviour.rs | 21 ++- network-libp2p/src/discovery/handler.rs | 106 ++++++++---- network-libp2p/src/discovery/peer_contacts.rs | 151 ++++++++++++++++-- network-libp2p/src/network.rs | 12 +- network-libp2p/src/swarm.rs | 22 ++- network-libp2p/tests/discovery.rs | 8 +- network-libp2p/tests/network.rs | 50 ++++-- validator-network/src/network_impl.rs | 1 + validator-network/src/validator_record.rs | 11 +- 15 files changed, 394 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ffaca6a22..328247a52a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3833,6 +3833,7 @@ dependencies = [ "nimiq-blockchain-proxy", "nimiq-keys", "nimiq-log", + "nimiq-network-interface", "nimiq-network-libp2p", "nimiq-serde", "nimiq-utils", diff --git a/dht/Cargo.toml b/dht/Cargo.toml index a65f4321a3..3b054d723d 100644 --- a/dht/Cargo.toml +++ b/dht/Cargo.toml @@ -26,6 +26,7 @@ nimiq-blockchain-interface = { workspace = true } nimiq-blockchain-proxy = { workspace = true } nimiq-keys = { workspace = true } nimiq-log = { workspace = true, optional = true } +nimiq-network-interface = { workspace = true } nimiq-network-libp2p = { workspace = true } nimiq-serde = { workspace = true } nimiq-utils = { workspace = true } diff --git a/dht/src/lib.rs b/dht/src/lib.rs index c16ff9d80f..9728141061 100644 --- a/dht/src/lib.rs +++ b/dht/src/lib.rs @@ -1,9 +1,11 @@ use nimiq_blockchain_proxy::BlockchainProxy; use nimiq_keys::{Address, KeyPair}; +use nimiq_network_interface::network::Network as NetworkInterface; use nimiq_network_libp2p::{ dht::{DhtRecord, DhtVerifierError, Verifier as DhtVerifier}, + discovery::peer_contacts::{ValidatorInfoError, ValidatorRecordVerifier}, libp2p::kad::Record, - PeerId, + Network, PeerId, }; use nimiq_serde::Deserialize; use nimiq_utils::tagged_signing::{TaggedSignable, TaggedSigned}; @@ -17,21 +19,20 @@ impl Verifier { pub fn new(blockchain: BlockchainProxy) -> Self { Self { blockchain } } +} - fn verify_validator_record(&self, record: &Record) -> Result { - // Deserialize the value of the record, which is a ValidatorRecord. If it fails return an error. - let validator_record = - TaggedSigned::, KeyPair>::deserialize_from_vec(&record.value) - .map_err(DhtVerifierError::MalformedValue)?; - - // Deserialize the key of the record which is an Address. If it fails return an error. - let validator_address = Address::deserialize_from_vec(record.key.as_ref()) - .map_err(DhtVerifierError::MalformedKey)?; - +impl ValidatorRecordVerifier for Verifier { + fn verify_validator_record( + &self, + signed_record: &TaggedSigned< + ValidatorRecord<::PeerId>, + KeyPair, + >, + ) -> Result<(), ValidatorInfoError> { // Acquire blockchain read access. For now exclude Light clients. let blockchain = match self.blockchain { BlockchainProxy::Light(ref _light_blockchain) => { - return Err(DhtVerifierError::UnknownTag) + panic!("Light Blockchains cannot verify validator records.") } BlockchainProxy::Full(ref full_blockchain) => full_blockchain, }; @@ -40,27 +41,26 @@ impl Verifier { // Get the staking contract to retrieve the public key for verification. let staking_contract = blockchain_read .get_staking_contract_if_complete(None) - .ok_or(DhtVerifierError::StateIncomplete)?; + .ok_or(ValidatorInfoError::StateIncomplete)?; // Get the public key needed for verification. let data_store = blockchain_read.get_staking_contract_store(); let txn = blockchain_read.read_transaction(); let public_key = staking_contract - .get_validator(&data_store.read(&txn), &validator_address) - .ok_or(DhtVerifierError::UnknownValidator(validator_address))? + .get_validator( + &data_store.read(&txn), + &signed_record.record.validator_address, + ) + .ok_or(ValidatorInfoError::UnknownValidator( + signed_record.record.validator_address.clone(), + ))? .signing_key; // Verify the record. - validator_record + signed_record .verify(&public_key) - .then(|| { - DhtRecord::Validator( - record.publisher.unwrap(), - validator_record.record, - record.clone(), - ) - }) - .ok_or(DhtVerifierError::InvalidSignature) + .then(|| ()) + .ok_or(ValidatorInfoError::InvalidSignature) } } @@ -75,7 +75,46 @@ impl DhtVerifier for Verifier { // Depending on tag perform the verification. match tag { - ValidatorRecord::::TAG => self.verify_validator_record(record), + ValidatorRecord::::TAG => { + // Deserialize the value of the record, which is a ValidatorRecord. If it fails return an error. + let validator_record = + TaggedSigned::, KeyPair>::deserialize_from_vec( + &record.value, + ) + .map_err(DhtVerifierError::MalformedValue)?; + + // Make sure the peer who published the record is also the one signed into the record. + if record.publisher.ok_or(DhtVerifierError::MissingPublisher)? + != validator_record.record.peer_id + { + return Err(DhtVerifierError::PublisherMismatch( + record.publisher.unwrap(), + validator_record.record.peer_id, + )); + } + + // Deserialize the key of the record which is an Address. If it fails return an error. + let validator_address = Address::deserialize_from_vec(record.key.as_ref()) + .map_err(DhtVerifierError::MalformedKey)?; + + // Make sure the address used as key is also the one signed into the record. + if validator_address != validator_record.record.validator_address { + return Err(DhtVerifierError::AddressMismatch( + validator_address, + validator_record.record.validator_address, + )); + } + + self.verify_validator_record(&validator_record) + .map_err(DhtVerifierError::ValidatorInfoError) + .and_then(|_| { + Ok(DhtRecord::Validator( + validator_record.record.peer_id, + validator_record.record, + record.clone(), + )) + }) + } _ => { log::error!(tag, "DHT invalid record tag received"); Err(DhtVerifierError::UnknownTag) diff --git a/network-libp2p/src/behaviour.rs b/network-libp2p/src/behaviour.rs index 15e01a31ac..7761729283 100644 --- a/network-libp2p/src/behaviour.rs +++ b/network-libp2p/src/behaviour.rs @@ -12,9 +12,11 @@ use parking_lot::RwLock; use rand::rngs::OsRng; use crate::{ - connection_pool, - connection_pool::behaviour::Config as PoolConfig, - discovery::{self, peer_contacts::PeerContactBook}, + connection_pool::{self, behaviour::Config as PoolConfig}, + discovery::{ + self, + peer_contacts::{PeerContactBook, ValidatorRecordVerifier}, + }, dispatch::codecs::MessageCodec, Config, }; @@ -50,6 +52,7 @@ impl Behaviour { contacts: Arc>, peer_score_params: gossipsub::PeerScoreParams, force_dht_server_mode: bool, + #[cfg(feature = "kad")] verifier: Arc, ) -> Self { let public_key = config.keypair.public(); let peer_id = public_key.to_peer_id(); @@ -68,6 +71,8 @@ impl Behaviour { config.discovery.clone(), config.keypair.clone(), Arc::clone(&contacts), + #[cfg(feature = "kad")] + verifier, ); // Gossipsub behaviour diff --git a/network-libp2p/src/connection_pool/behaviour.rs b/network-libp2p/src/connection_pool/behaviour.rs index fef1be89b7..9e898427d6 100644 --- a/network-libp2p/src/connection_pool/behaviour.rs +++ b/network-libp2p/src/connection_pool/behaviour.rs @@ -534,7 +534,7 @@ impl Behaviour { let own_peer_id = own_contact.peer_id(); contacts - .query(self.required_services) + .query(self.required_services, true) .filter_map(|contact| { let peer_id = contact.peer_id(); if peer_id != own_peer_id @@ -562,7 +562,7 @@ impl Behaviour { let own_peer_id = own_contact.peer_id(); contacts - .query(services) + .query(services, true) .filter_map(|contact| { let peer_id = contact.peer_id(); if peer_id != own_peer_id diff --git a/network-libp2p/src/dht.rs b/network-libp2p/src/dht.rs index 5ad307bc87..5e31b14fd8 100644 --- a/network-libp2p/src/dht.rs +++ b/network-libp2p/src/dht.rs @@ -1,19 +1,25 @@ use libp2p::{kad::Record, PeerId}; use nimiq_keys::Address; +use nimiq_network_interface::network::Network as NetworkInterface; use nimiq_serde::DeserializeError; use nimiq_validator_network::validator_record::ValidatorRecord; pub use crate::network_types::DhtRecord; +use crate::{discovery::peer_contacts::ValidatorInfoError, Network}; #[derive(Debug)] pub enum DhtVerifierError { MalformedTag, + UnknownTag, MalformedKey(DeserializeError), MalformedValue(DeserializeError), - UnknownTag, - UnknownValidator(Address), - StateIncomplete, - InvalidSignature, + MissingPublisher, + PublisherMismatch( + ::PeerId, + ::PeerId, + ), + AddressMismatch(Address, Address), + ValidatorInfoError(ValidatorInfoError), } pub trait Verifier: Send + Sync { @@ -23,9 +29,10 @@ pub trait Verifier: Send + Sync { /// Dummy implementation for testcases impl Verifier for () { fn verify(&self, record: &Record) -> Result { + let peer_id = PeerId::random(); Ok(DhtRecord::Validator( - PeerId::random(), - ValidatorRecord::::new(PeerId::random(), 0u64), + peer_id, + ValidatorRecord::::new(peer_id, Address::default(), 0u64), record.clone(), )) } diff --git a/network-libp2p/src/discovery/behaviour.rs b/network-libp2p/src/discovery/behaviour.rs index 4711f96389..8aaa2a3bad 100644 --- a/network-libp2p/src/discovery/behaviour.rs +++ b/network-libp2p/src/discovery/behaviour.rs @@ -11,7 +11,7 @@ use libp2p::{ identity::Keypair, swarm::{ behaviour::{ConnectionClosed, ConnectionEstablished}, - CloseConnection, ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, ToSwarm, + ConnectionDenied, ConnectionId, FromSwarm, NetworkBehaviour, ToSwarm, }, Multiaddr, PeerId, }; @@ -22,7 +22,7 @@ use parking_lot::RwLock; use super::{ handler::{Handler, HandlerOutEvent}, - peer_contacts::{PeerContact, PeerContactBook}, + peer_contacts::{PeerContact, PeerContactBook, ValidatorRecordVerifier}, }; #[derive(Clone, Debug)] @@ -115,6 +115,10 @@ pub struct Behaviour { /// Timer to do house-keeping in the peer address book. house_keeping_timer: Interval, + + /// dht verifier TODO + #[cfg(feature = "kad")] + verifier: Arc, } impl Behaviour { @@ -122,6 +126,7 @@ impl Behaviour { config: Config, keypair: Keypair, peer_contact_book: Arc>, + #[cfg(feature = "kad")] verifier: Arc, ) -> Self { let house_keeping_timer = interval(config.house_keeping_interval); peer_contact_book.write().update_own_contact(&keypair); @@ -139,6 +144,8 @@ impl Behaviour { peer_contact_book, events, house_keeping_timer, + #[cfg(feature = "kad")] + verifier, } } @@ -177,6 +184,8 @@ impl NetworkBehaviour for Behaviour { self.keypair.clone(), self.peer_contact_book(), remote_addr.clone(), + #[cfg(feature = "kad")] + Arc::clone(&self.verifier), )) } @@ -194,6 +203,8 @@ impl NetworkBehaviour for Behaviour { self.keypair.clone(), self.peer_contact_book(), addr.clone(), + #[cfg(feature = "kad")] + Arc::clone(&self.verifier), )) } @@ -288,10 +299,8 @@ impl NetworkBehaviour for Behaviour { .push_back(ToSwarm::NewExternalAddrCandidate(observed_address)); } HandlerOutEvent::Update => self.events.push_back(ToSwarm::GenerateEvent(Event::Update)), - HandlerOutEvent::Error(_) => self.events.push_back(ToSwarm::CloseConnection { - peer_id, - connection: CloseConnection::All, - }), + // Errors must not result in a closed connection as light clients are unable to verify ValidatorRecord. + HandlerOutEvent::Error(error) => log::trace!(?error, "Received invalid contact"), } } } diff --git a/network-libp2p/src/discovery/handler.rs b/network-libp2p/src/discovery/handler.rs index 7a04d03c58..5079ef6ed9 100644 --- a/network-libp2p/src/discovery/handler.rs +++ b/network-libp2p/src/discovery/handler.rs @@ -33,7 +33,10 @@ use thiserror::Error; use super::{ behaviour::Config, message_codec::{MessageReader, MessageWriter}, - peer_contacts::{PeerContactBook, SignedPeerContact}, + peer_contacts::{ + PeerContactBook, PeerContactError, SignedPeerContact, ValidatorInfoError, + ValidatorRecordVerifier, + }, protocol::{ChallengeNonce, DiscoveryMessage, DiscoveryProtocol}, }; use crate::{AUTONAT_DIAL_BACK_PROTOCOL, AUTONAT_DIAL_REQUEST_PROTOCOL}; @@ -134,6 +137,11 @@ pub struct Handler { /// The peer contact book peer_contact_book: Arc>, + /// Used to verify PeerContacts. + /// Required as contacts could contain a ValidatorInfo, for which a current verification key is required. + #[cfg(feature = "kad")] + verifier: Arc, + /// The peer address we're connected to (address that got us connected). peer_address: Multiaddr, @@ -179,6 +187,7 @@ impl Handler { keypair: Keypair, peer_contact_book: Arc>, peer_address: Multiaddr, + #[cfg(feature = "kad")] verifier: Arc, ) -> Self { if let Some(peer_contact) = peer_contact_book.write().get(&peer_id) { if let Some(outer_protocol_address) = outer_protocol_address(&peer_address) { @@ -202,6 +211,8 @@ impl Handler { inbound: None, outbound: None, waker: None, + #[cfg(feature = "kad")] + verifier, events: VecDeque::new(), } } @@ -232,7 +243,7 @@ impl Handler { let mut rng = thread_rng(); peer_contact_book - .query(self.services_filter) + .query(self.services_filter, false) .choose_multiple(&mut rng, limit) .into_iter() .map(|c| c.signed().clone()) @@ -287,6 +298,44 @@ pub(crate) fn outer_protocol_address(addr: &Multiaddr) -> Option { .unwrap_or(None) } +fn filter_contact( + timestamp: u64, + #[cfg(feature = "kad")] verifier: Arc, +) -> Box Option> { + Box::new(move |mut peer_contact: SignedPeerContact| { + match peer_contact.verify( + #[cfg(feature = "kad")] + Arc::clone(&verifier), + ) { + // Contacts with too many addresses or an invalid peer signature are rejected + // and removed from the collection + Err(PeerContactError::AdvertisedAddressesExceeded) + | Err(PeerContactError::InvalidSignature) => None, + // If there is a validator record, but the state is incomplete, + // then it cannot be verified and must be checked again at a later time. + Err(PeerContactError::ValidatorRecord(ValidatorInfoError::StateIncomplete)) => { + peer_contact.local_only = true; + Some(peer_contact) + } + // If there is a validator record but either the signature is invalid, or the validator is unknown, + // the head timestamp of the blockchain and the creation timestamp must be compared. + Err(PeerContactError::ValidatorRecord(ValidatorInfoError::InvalidSignature)) + | Err(PeerContactError::ValidatorRecord(ValidatorInfoError::UnknownValidator(_))) => { + // Set it to local only. Some contacts will be discarded by the next condition still. + peer_contact.local_only = true; + + // Retain only contacts which are in the future, as they may still verify then. + if timestamp < peer_contact.inner.timestamp() { + Some(peer_contact) + } else { + None + } + } + Ok(_) => Some(peer_contact), + } + }) +} + impl ConnectionHandler for Handler { type FromBehaviour = (); type ToBehaviour = HandlerOutEvent; @@ -526,7 +575,13 @@ impl ConnectionHandler for Handler { peer_contacts, } => { // Check the peer contact for a valid signature. - if !peer_contact.verify() { + let Some(peer_contact) = filter_contact( + 0, + #[cfg(feature = "kad")] + Arc::clone(&self.verifier), + )( + peer_contact.clone() + ) else { return Poll::Ready( ConnectionHandlerEvent::NotifyBehaviour( HandlerOutEvent::Error( @@ -536,7 +591,7 @@ impl ConnectionHandler for Handler { ), ), ); - } + }; if self.peer_id != peer_contact.peer_id() { return Poll::Ready( @@ -574,19 +629,14 @@ impl ConnectionHandler for Handler { ), ); } - for peer_contact in &peer_contacts { - if !peer_contact.verify() { - return Poll::Ready( - ConnectionHandlerEvent::NotifyBehaviour( - HandlerOutEvent::Error( - Error::InvalidPeerContactSignature { - peer_contact: peer_contact.clone(), - }, - ), - ), - ); - } - } + let peer_contacts: Vec = peer_contacts + .into_iter() + .filter_map(filter_contact( + 0, + #[cfg(feature = "kad")] + Arc::clone(&self.verifier), + )) + .collect(); let mut peer_contact_book = self.peer_contact_book.write(); @@ -685,19 +735,15 @@ impl ConnectionHandler for Handler { ), ); } - for peer_contact in &peer_contacts { - if !peer_contact.verify() { - return Poll::Ready( - ConnectionHandlerEvent::NotifyBehaviour( - HandlerOutEvent::Error( - Error::InvalidPeerContactSignature { - peer_contact: peer_contact.clone(), - }, - ), - ), - ); - } - } + + let peer_contacts: Vec = peer_contacts + .into_iter() + .filter_map(filter_contact( + 0, + #[cfg(feature = "kad")] + Arc::clone(&self.verifier), + )) + .collect(); // Insert the new peer contacts into the peer contact book. self.peer_contact_book.write().insert_all_filtered( diff --git a/network-libp2p/src/discovery/peer_contacts.rs b/network-libp2p/src/discovery/peer_contacts.rs index 9df6fb8f9e..feb1c2e052 100644 --- a/network-libp2p/src/discovery/peer_contacts.rs +++ b/network-libp2p/src/discovery/peer_contacts.rs @@ -11,24 +11,104 @@ use libp2p::{ multiaddr::Protocol, Multiaddr, PeerId, }; -use nimiq_network_interface::peer_info::{PeerInfo, Services}; -use nimiq_utils::tagged_signing::{TaggedKeyPair, TaggedSignable, TaggedSignature}; +use nimiq_keys::{Address, KeyPair}; +use nimiq_network_interface::{ + network::Network as NetworkInterface, + peer_info::{PeerInfo, Services}, +}; +use nimiq_utils::tagged_signing::{TaggedKeyPair, TaggedSignable, TaggedSignature, TaggedSigned}; +use nimiq_validator_network::validator_record::ValidatorRecord; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::utils; +use crate::{utils, Network}; #[derive(Debug, Error)] pub enum PeerContactError { #[error("Exceeded number of advertised addresses")] AdvertisedAddressesExceeded, + #[error("Validator Record failed to verify")] + ValidatorRecord(ValidatorInfoError), + #[error("Contact signature is invalid")] + InvalidSignature, +} + +#[derive(Debug)] +pub enum ValidatorInfoError { + StateIncomplete, + InvalidSignature, + UnknownValidator(Address), +} + +#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct ValidatorInfo { + validator_address: Address, + signature: TaggedSignature::PeerId>, KeyPair>, +} + +impl ValidatorInfo { + pub fn new( + validator_address: Address, + signature: TaggedSignature::PeerId>, KeyPair>, + ) -> Self { + Self { + validator_address, + signature, + } + } + + pub fn verify( + &self, + timestamp: u64, + peer_id: PeerId, + verification_key: &::PublicKey, + ) -> Result<(), ValidatorInfoError> { + // Reconstruct the record + let record = ValidatorRecord { + validator_address: self.validator_address.clone(), + timestamp, + peer_id, + }; + + // Reconstruct the signed record. + let signed_record = TaggedSigned::new(record, self.signature.clone()); + + // Verify the record and return the result. + signed_record + .verify(verification_key) + .then_some(()) + .ok_or(ValidatorInfoError::InvalidSignature) + } +} + +pub trait ValidatorRecordVerifier: Send + Sync { + fn verify_validator_record( + &self, + signed_record: &TaggedSigned< + ValidatorRecord<::PeerId>, + KeyPair, + >, + ) -> Result<(), ValidatorInfoError>; +} + +impl ValidatorRecordVerifier for () { + fn verify_validator_record( + &self, + _signed_record: &TaggedSigned< + ValidatorRecord<::PeerId>, + KeyPair, + >, + ) -> Result<(), ValidatorInfoError> { + Ok(()) + } } /// A plain peer contact. This contains: /// /// - A set of multi-addresses for the peer. /// - The peer's public key. +/// - An optional [ValidatorInfo] in case this peer is a running a validator. /// - A bitmask of the services supported by this peer. /// - A timestamp when this contact information was generated. /// @@ -42,6 +122,9 @@ pub struct PeerContact { #[serde(with = "self::serde_public_key")] pub public_key: PublicKey, + /// Optional validator info in case the node is a validator. + pub validator_info: Option, + /// Services supported by this peer. pub services: Services, @@ -69,6 +152,7 @@ impl PeerContact { Ok(Self { addresses, public_key, + validator_info: None, services, timestamp, }) @@ -76,7 +160,7 @@ impl PeerContact { /// Derives the peer ID from the public key pub fn peer_id(&self) -> PeerId { - self.public_key.clone().to_peer_id() + self.public_key.to_peer_id() } /// Returns the timestamp of the contact. It is generally set to the time the contact was created. @@ -100,6 +184,7 @@ impl PeerContact { SignedPeerContact { inner: self, signature, + local_only: false, } } @@ -137,10 +222,35 @@ impl PeerContact { /// Verifies whether the lengths of the advertised addresses are within /// the expected limits. This is helpful to verify a received peer contact. - pub fn verify(&self) -> Result<(), PeerContactError> { + pub fn verify( + &self, + #[cfg(feature = "kad")] verifier: Arc, + ) -> Result<(), PeerContactError> { if self.addresses.len() > Self::MAX_ADDRESSES { return Err(PeerContactError::AdvertisedAddressesExceeded); } + + // In case the Contact includes a ValidatorInfo it also needs to be verified. + #[cfg(feature = "kad")] + if let Some(ValidatorInfo { + validator_address, + signature, + }) = self.validator_info.clone() + { + // Reconstruct the record + let record = ValidatorRecord { + validator_address, + timestamp: self.timestamp, + peer_id: self.peer_id(), + }; + + let signed_record = TaggedSigned::new(record, signature); + + verifier + .verify_validator_record(&signed_record) + .map_err(PeerContactError::ValidatorRecord)?; + } + Ok(()) } } @@ -157,17 +267,30 @@ pub struct SignedPeerContact { /// The signature over the serialized peer contact. pub signature: TaggedSignature, + + #[serde(skip)] + pub local_only: bool, } impl SignedPeerContact { /// Verifies that the signature is valid for this peer contact and also does /// intrinsic verification on the inner PeerContact. - pub fn verify(&self) -> bool { - if self.inner.verify().is_err() { - return false; - }; - self.signature + pub fn verify( + &self, + #[cfg(feature = "kad")] verifier: Arc, + ) -> Result<(), PeerContactError> { + // The record signature must be verifid first. + if !self + .signature .tagged_verify(&self.inner, &self.inner.public_key) + { + return Err(PeerContactError::InvalidSignature); + } + + self.inner.verify( + #[cfg(feature = "kad")] + verifier, + ) } /// Gets the public key of this peer contact. @@ -486,11 +609,15 @@ impl PeerContactBook { /// Gets a set of peer contacts given a services filter. /// Every peer contact that matches such services will be returned. - pub fn query(&self, services: Services) -> impl Iterator> + '_ { + pub fn query( + &self, + services: Services, + include_local_only: bool, + ) -> impl Iterator> + '_ { // TODO: This is a naive implementation // TODO: Sort by score? self.peer_contacts.iter().filter_map(move |(_, contact)| { - if contact.matches(services) { + if !contact.contact.local_only || include_local_only && contact.matches(services) { Some(Arc::clone(contact)) } else { None diff --git a/network-libp2p/src/network.rs b/network-libp2p/src/network.rs index e60e8d471f..ec5f0b6c28 100644 --- a/network-libp2p/src/network.rs +++ b/network-libp2p/src/network.rs @@ -38,7 +38,7 @@ use tokio_stream::wrappers::{BroadcastStream, ReceiverStream}; use crate::network_metrics::NetworkMetrics; use crate::{ dht, - discovery::peer_contacts::PeerContactBook, + discovery::peer_contacts::{PeerContactBook, ValidatorRecordVerifier}, network_types::{GossipsubId, NetworkAction, ValidateMessage}, rate_limiting::RateLimitConfig, swarm::{new_swarm, swarm_task}, @@ -79,7 +79,7 @@ impl Network { /// pub async fn new( config: Config, - #[cfg(feature = "kad")] dht_verifier: impl dht::Verifier + 'static, + #[cfg(feature = "kad")] verifier: impl dht::Verifier + ValidatorRecordVerifier + 'static, ) -> Self { let required_services = config.required_services; // TODO: persist to disk @@ -100,11 +100,17 @@ impl Network { // In memory transport we don't have a mechanism that sets the DHT in server mode such as confirming an address // with Autonat. This is because Autonat v1 only works with IP addresses. let force_dht_server_mode = config.memory_transport; + + #[cfg(feature = "kad")] + let verifier = Arc::new(verifier); + let swarm = new_swarm( config, Arc::clone(&contacts), params.clone(), force_dht_server_mode, + #[cfg(feature = "kad")] + (Arc::clone(&verifier) as Arc), ); let local_peer_id = *Swarm::local_peer_id(&swarm); @@ -128,7 +134,7 @@ impl Network { update_scores, Arc::clone(&contacts), #[cfg(feature = "kad")] - dht_verifier, + (Arc::clone(&verifier) as Arc), force_dht_server_mode, dht_quorum, #[cfg(feature = "metrics")] diff --git a/network-libp2p/src/swarm.rs b/network-libp2p/src/swarm.rs index 2d58c925d7..c6479371bf 100644 --- a/network-libp2p/src/swarm.rs +++ b/network-libp2p/src/swarm.rs @@ -45,7 +45,10 @@ use crate::network_metrics::NetworkMetrics; use crate::{ autonat::NatStatus, behaviour, dht, - discovery::{self, peer_contacts::PeerContactBook}, + discovery::{ + self, + peer_contacts::{PeerContactBook, ValidatorRecordVerifier}, + }, network_types::{ DhtBootStrapState, DhtRecord, DhtResults, GossipsubTopicInfo, NetworkAction, TaskState, ValidateMessage, @@ -63,7 +66,7 @@ struct EventInfo<'a> { connected_peers: &'a RwLock>, rate_limiting: &'a mut RateLimits, #[cfg(feature = "kad")] - dht_verifier: &'a dyn dht::Verifier, + dht_verifier: Arc, #[cfg(feature = "metrics")] metrics: &'a Arc, } @@ -73,6 +76,7 @@ pub(crate) fn new_swarm( contacts: Arc>, peer_score_params: gossipsub::PeerScoreParams, force_dht_server_mode: bool, + #[cfg(feature = "kad")] verifier: Arc, ) -> Swarm { let keypair = config.keypair.clone(); let transport = new_transport( @@ -83,8 +87,14 @@ pub(crate) fn new_swarm( ) .unwrap(); - let behaviour = - behaviour::Behaviour::new(config, contacts, peer_score_params, force_dht_server_mode); + let behaviour = behaviour::Behaviour::new( + config, + contacts, + peer_score_params, + force_dht_server_mode, + #[cfg(feature = "kad")] + verifier, + ); // TODO add proper config #[cfg(not(target_family = "wasm"))] @@ -114,7 +124,7 @@ pub(crate) async fn swarm_task( connected_peers: Arc>>, mut update_scores: Interval, contacts: Arc>, - #[cfg(feature = "kad")] dht_verifier: impl dht::Verifier, + #[cfg(feature = "kad")] dht_verifier: Arc, force_dht_server_mode: bool, dht_quorum: NonZeroU8, #[cfg(feature = "metrics")] metrics: Arc, @@ -162,7 +172,7 @@ pub(crate) async fn swarm_task( connected_peers: &connected_peers, rate_limiting: &mut rate_limiting, #[cfg(feature = "kad")] - dht_verifier: &dht_verifier, + dht_verifier: Arc::clone(&dht_verifier), #[cfg( feature = "metrics")] metrics: &metrics, }, ); diff --git a/network-libp2p/tests/discovery.rs b/network-libp2p/tests/discovery.rs index 900dbdce01..8d22baa570 100644 --- a/network-libp2p/tests/discovery.rs +++ b/network-libp2p/tests/discovery.rs @@ -82,8 +82,12 @@ impl TestNode { true, ))); - let behaviour = - discovery::Behaviour::new(config, keypair.clone(), Arc::clone(&peer_contact_book)); + let behaviour = discovery::Behaviour::new( + config, + keypair.clone(), + Arc::clone(&peer_contact_book), + Arc::new(()), + ); let mut swarm = SwarmBuilder::with_existing_identity(keypair) .with_tokio() diff --git a/network-libp2p/tests/network.rs b/network-libp2p/tests/network.rs index 2b2a525304..a931a6a347 100644 --- a/network-libp2p/tests/network.rs +++ b/network-libp2p/tests/network.rs @@ -15,7 +15,10 @@ use nimiq_network_interface::{ }; use nimiq_network_libp2p::{ dht, - discovery::{self, peer_contacts::PeerContact}, + discovery::{ + self, + peer_contacts::{PeerContact, ValidatorInfoError, ValidatorRecordVerifier}, + }, Config, Network, }; use nimiq_serde::{Deserialize, Serialize}; @@ -203,6 +206,26 @@ impl Verifier { } } +impl ValidatorRecordVerifier for Verifier { + fn verify_validator_record( + &self, + signed_record: &TaggedSigned< + ValidatorRecord<::PeerId>, + KeyPair, + >, + ) -> Result<(), ValidatorInfoError> { + let keys = self.keys.read(); + let public_key = keys.get(&signed_record.record.validator_address).ok_or( + ValidatorInfoError::UnknownValidator(signed_record.record.validator_address.clone()), + )?; + + signed_record + .verify(&public_key) + .then(|| ()) + .ok_or(ValidatorInfoError::InvalidSignature) + } +} + impl dht::Verifier for Verifier { fn verify( &self, @@ -228,21 +251,15 @@ impl dht::Verifier for Verifier { let validator_address = Address::deserialize_from_vec(record.key.as_ref()) .map_err(dht::DhtVerifierError::MalformedKey)?; - let keys = self.keys.read(); - let public_key = keys - .get(&validator_address) - .ok_or(dht::DhtVerifierError::UnknownValidator(validator_address))?; - - validator_record - .verify(&public_key) - .then(|| { + self.verify_validator_record(&validator_record) + .map(|_| { dht::DhtRecord::Validator( - record.publisher.unwrap(), + validator_record.record.peer_id, validator_record.record, record.clone(), ) }) - .ok_or(dht::DhtVerifierError::InvalidSignature) + .map_err(dht::DhtVerifierError::ValidatorInfoError) } } @@ -490,17 +507,18 @@ async fn dht_put_and_get() { // FIXME: Add delay while networks share their addresses sleep(Duration::from_secs(2)).await; - let put_record = ValidatorRecord { - peer_id: net1.get_local_peer_id(), - timestamp: 0x42u64, - }; - // Generate a key let mut rng = test_rng(false); let keypair = KeyPair::generate(&mut rng); // Put it into the keys collection. let key: Address = (&keypair.public).into(); + + let put_record = ValidatorRecord { + peer_id: net1.get_local_peer_id(), + validator_address: key.clone(), + timestamp: 0x42u64, + }; assert!(keys.write().insert(key.clone(), keypair.public).is_none()); // Put the record into the dht, keyed by the address. diff --git a/validator-network/src/network_impl.rs b/validator-network/src/network_impl.rs index 6b8497755e..da775cd818 100644 --- a/validator-network/src/network_impl.rs +++ b/validator-network/src/network_impl.rs @@ -493,6 +493,7 @@ where let peer_id = self.network.get_local_peer_id(); let record = ValidatorRecord::new( peer_id, + validator_address.clone(), (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000) as u64, ); self.network diff --git a/validator-network/src/validator_record.rs b/validator-network/src/validator_record.rs index 64a0b2a4bd..5b77f7b36b 100644 --- a/validator-network/src/validator_record.rs +++ b/validator-network/src/validator_record.rs @@ -1,3 +1,4 @@ +use nimiq_keys::Address; use nimiq_serde::{Deserialize, Serialize}; use nimiq_utils::tagged_signing::TaggedSignable; @@ -17,6 +18,8 @@ where { /// Validator Peer ID pub peer_id: TPeerId, + /// Address of th validator. + pub validator_address: Address, /// Record timestamp in milliseconds since 1970-01-01 00:00:00 UTC, excluding leap seconds (Unix time) pub timestamp: u64, } @@ -25,8 +28,12 @@ impl ValidatorRecord where TPeerId: Serialize + Deserialize, { - pub fn new(peer_id: TPeerId, timestamp: u64) -> Self { - Self { peer_id, timestamp } + pub fn new(peer_id: TPeerId, validator_address: Address, timestamp: u64) -> Self { + Self { + peer_id, + validator_address, + timestamp, + } } }