//! The domain protocol carried over `federation-net`. use std::fmt; use federation_net::EndpointId; use serde::{Deserialize, Serialize}; use crate::record::{DhtKey, StoredRecord}; use crate::routing::{NodeContact, NodeId}; /// Version of the music-dht protocol. Batch limits are enforced by the /// receiver, so raising them is a version bump too. pub const DHT_PROTOCOL_VERSION: u16 = 5; /// Maximum number of contacts in a single [`PeerExchange`]. pub const MAX_PEER_EXCHANGE_CONTACTS: usize = 32; /// Maximum number of records in a single [`FindValueResponse`]. pub const MAX_RECORDS_PER_RESPONSE: usize = 256; /// Maximum number of entries in a single [`StoreBatchRequest`]. Sized so a /// batch of typical records nearly fills the byte budget: replicating a /// 20k-track library takes hundreds of requests, not tens of thousands. pub const MAX_RECORDS_PER_BATCH: usize = 512; /// Correlates a response with its request. #[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct RequestId([u8; 16]); impl RequestId { /// Generates a random request id. pub fn random() -> Self { Self(rand::random()) } /// Returns the raw bytes. pub fn as_bytes(&self) -> &[u8; 16] { &self.0 } } impl fmt::Debug for RequestId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "RequestId(")?; for byte in &self.0 { write!(f, "{byte:02x}")?; } write!(f, ")") } } /// Wraps a request payload with its correlation id. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RequestEnvelope { /// Correlation id; echoed back in the response. pub request_id: RequestId, /// The request itself. pub payload: T, } /// Wraps a response payload with the correlation id of its request. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponseEnvelope { /// Correlation id of the request being answered. pub request_id: RequestId, /// The response itself. pub payload: T, } /// Introduction sent right after a connection is established. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Hello { /// DHT identifier of the sender. pub node_id: NodeId, /// Transport identifier of the sender (informational; the authenticated /// id always comes from the connection itself). pub peer_id: EndpointId, /// Ticket other peers can use to reach the sender. pub ticket: String, /// Protocol version of the sender. pub protocol_version: u16, } /// A batch of known contacts, shared after [`Hello`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PeerExchange { /// Up to [`MAX_PEER_EXCHANGE_CONTACTS`] contacts. pub peers: Vec, } /// Liveness probe. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PingRequest {} /// Reply to [`PingRequest`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PongResponse { /// DHT identifier of the responder. pub node_id: NodeId, } /// Asks for the closest known nodes to `target`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FindNodeRequest { /// Point of the key space to search around. pub target: NodeId, } /// Reply to [`FindNodeRequest`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FindNodeResponse { /// Up to `K` known nodes closest to the target. pub nodes: Vec, } /// Asks for records stored under `key`, or the closest nodes to it. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FindValueRequest { /// The DHT key to look up. pub key: DhtKey, } /// Reply to [`FindValueRequest`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum FindValueResponse { /// The responder stores records under the key. Records { /// Up to [`MAX_RECORDS_PER_RESPONSE`] non-expired records. records: Vec, }, /// The responder has nothing stored; here are closer nodes instead. CloserNodes { /// Up to `K` known nodes closest to the key. nodes: Vec, }, } /// One record replica to store, always carried inside a /// [`StoreBatchRequest`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StoreRecordRequest { /// The key the record is published under. pub key: DhtKey, /// The record to store. pub record: StoredRecord, } /// Asks the receiver to store replicas of several records at once. /// /// Batching keeps a full-library republish at roughly O(peers) requests /// instead of O(records × keys): each receiver validates every entry /// individually, so one bad entry never poisons the rest of the batch. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StoreBatchRequest { /// At most [`MAX_RECORDS_PER_BATCH`] entries. pub entries: Vec, } /// Reply to [`StoreBatchRequest`]. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StoreBatchResponse { /// How many entries were accepted and stored (or refreshed). pub stored: u32, } /// Every message exchanged between music-dht peers. #[derive(Debug, Clone, Serialize, Deserialize)] #[allow(clippy::large_enum_variant)] pub enum MusicDhtMessage { /// Introduction after connect. Hello(Hello), /// Contact gossip after `Hello`. PeerExchange(PeerExchange), /// Liveness probe. Ping(RequestEnvelope), /// Reply to `Ping`. Pong(ResponseEnvelope), /// Node lookup request. FindNode(RequestEnvelope), /// Reply to `FindNode`. FindNodeResult(ResponseEnvelope), /// Value lookup request. FindValue(RequestEnvelope), /// Reply to `FindValue`. FindValueResult(ResponseEnvelope), /// Batched replication request. StoreBatch(RequestEnvelope), /// Reply to `StoreBatch`. StoreBatchResult(ResponseEnvelope), }