summaryrefslogtreecommitdiffstats
path: root/src/transport/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/transport/mod.rs')
-rw-r--r--src/transport/mod.rs647
1 files changed, 534 insertions, 113 deletions
diff --git a/src/transport/mod.rs b/src/transport/mod.rs
index 35e3499..25ad56d 100644
--- a/src/transport/mod.rs
+++ b/src/transport/mod.rs
@@ -2,62 +2,102 @@ use anyhow::Result;
use async_trait::async_trait;
use automerge::ChangeHash;
use automerge::sync::Message;
+use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::boxed::Box;
use std::future::Future;
use std::pin::Pin;
+use std::sync::atomic::{AtomicUsize, Ordering};
-pub use crate::document::replica::Epoch;
+pub(crate) use crate::document::replica::Epoch;
-pub mod firewall;
-pub mod mycelium;
+// Used by the transports that don't authenticate their peers themselves (`tor`).
+#[cfg_attr(not(feature = "tor"), allow(dead_code))]
+pub(crate) mod challenge;
+pub(crate) mod firewall;
+pub(crate) mod identity;
+#[cfg(feature = "libp2p")]
+pub(crate) mod libp2p;
+pub(crate) mod mycelium;
+pub(crate) mod paths;
#[cfg(test)]
pub(crate) mod sim;
-pub mod traffic;
+pub(crate) mod stats;
+#[cfg(feature = "tor")]
+pub(crate) mod tor;
+pub(crate) mod traffic;
+pub(crate) mod wire;
/// Random identifier of one run of a node (from start to exit).
-pub type RunId = [u8; 16];
+pub(crate) type RunId = [u8; 16];
-/// Most heads listed in a keepalive; more are sent as a digest. A head takes 34 bytes on
-/// the wire, so 32 of them keep a keepalive within one Mycelium chunk (1300 bytes; see
-/// the size test in `mycelium`). There are rarely more heads than nodes that wrote
-/// concurrently since the last merging write.
-pub const MAX_LISTED_HEADS: usize = 32;
+/// Most heads listed in a keepalive; more are sent as a digest. A head takes 34 bytes
+/// on the wire, so 32 of them keep a keepalive within one small packet (a Mycelium
+/// chunk: 1300 bytes; see the size test in `mycelium`). There are rarely more heads
+/// than nodes that wrote concurrently since the last merging write.
+pub(crate) const MAX_LISTED_HEADS: usize = 32;
/// A document's heads, compactly: the heads themselves while there are few, otherwise a
-/// digest of them. Two summaries are equal exactly when the heads are, whatever form
-/// each took (nodes with different limits still agree).
+/// digest of them with some of them. Two summaries are equal exactly when the heads are,
+/// whatever form each took (nodes with different limits still agree).
#[derive(Debug, Clone)]
-pub enum HeadsSummary {
+pub(crate) enum HeadsSummary {
/// Sorted.
Heads(Vec<ChangeHash>),
- /// SHA-256 over the sorted heads.
- Digest([u8; 32]),
+ Digest {
+ /// SHA-256 over the sorted heads.
+ digest: [u8; 32],
+ /// `MAX_LISTED_HEADS` of the heads, another window of them each time: a receiver
+ /// pulls those it lacks, and over a few keepalives every head is listed, instead
+ /// of starting the sync over to find out what it lacks.
+ sample: Vec<ChangeHash>,
+ },
}
+/// Where the next sample starts (`HeadsSummary::of`).
+static SAMPLE_OFFSET: AtomicUsize = AtomicUsize::new(0);
+
impl HeadsSummary {
- pub fn of(mut heads: Vec<ChangeHash>) -> Self {
+ pub(crate) fn of(mut heads: Vec<ChangeHash>) -> Self {
heads.sort_unstable();
if heads.len() <= MAX_LISTED_HEADS {
- Self::Heads(heads)
- } else {
- Self::Digest(digest_of(&heads))
+ return Self::Heads(heads);
+ }
+ let start = SAMPLE_OFFSET.fetch_add(MAX_LISTED_HEADS, Ordering::Relaxed) % heads.len();
+ let sample = heads
+ .iter()
+ .cycle()
+ .skip(start)
+ .take(MAX_LISTED_HEADS)
+ .copied()
+ .collect();
+ Self::Digest {
+ digest: digest_of(&heads),
+ sample,
}
}
/// The digest of the heads, computed if they are listed.
- pub fn digest(&self) -> [u8; 32] {
+ pub(crate) fn digest(&self) -> [u8; 32] {
match self {
Self::Heads(heads) => digest_of(heads),
- Self::Digest(digest) => *digest,
+ Self::Digest { digest, .. } => *digest,
}
}
- /// The heads, if listed.
- pub fn heads(&self) -> Option<&[ChangeHash]> {
+ /// The heads, if all are listed.
+ pub(crate) fn heads(&self) -> Option<&[ChangeHash]> {
match self {
Self::Heads(heads) => Some(heads),
- Self::Digest(_) => None,
+ Self::Digest { .. } => None,
+ }
+ }
+
+ /// The heads listed: all of them, or a sample.
+ pub(crate) fn listed(&self) -> &[ChangeHash] {
+ match self {
+ Self::Heads(heads) => heads,
+ Self::Digest { sample, .. } => sample,
}
}
}
@@ -82,79 +122,117 @@ fn digest_of(sorted_heads: &[ChangeHash]) -> [u8; 32] {
hasher.finalize().into()
}
-/// PING and PONG. Addressed to one peer: `syncs_with_you` is about the recipient.
-#[derive(Debug, Clone)]
-pub struct Keepalive {
+/// STATUS and STATUS_REPLY. Addressed to one peer: `syncs_with_you` is about the recipient.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct Status {
/// Only comparable between nodes on the same epoch.
- pub heads: HeadsSummary,
- pub epoch: Epoch,
+ #[serde(with = "crate::transport::wire::codec::heads")]
+ pub(crate) heads: HeadsSummary,
+ #[serde(with = "crate::transport::wire::codec::epoch")]
+ pub(crate) epoch: Epoch,
/// See `SyncEnvelope::epoch_seq`.
- pub epoch_seq: u32,
+ pub(crate) epoch_seq: u32,
/// The sender is ready (same meaning as `SyncEnvelope::ready`).
- pub ready: bool,
+ pub(crate) ready: bool,
/// The sender would sync with the recipient: counts it as an active member on its
/// own epoch.
- pub syncs_with_you: bool,
+ pub(crate) syncs_with_you: bool,
}
-#[derive(Debug)]
-pub enum ParsedMessage {
+/// Cheap to clone: large parts are shared (`bytes::Bytes`).
+#[derive(Debug, Clone)]
+pub(crate) enum ParsedMessage {
Hello(HelloRequest),
HelloReply(HelloReply),
Sync(SyncEnvelope),
- Ping(Keepalive),
- Pong(Keepalive),
+ Status(Status),
+ StatusReply(Status),
Blob(BlobMessage),
+ /// Asks the recipient who it is, saying who the sender is (`transport::identity`).
+ Who(IdentityProof),
+ /// Answers a WHO.
+ Me(IdentityProof),
+ /// Asks for a PONG with this nonce, over the same path (`transport::paths`).
+ Ping(u64),
+ /// Answers a PING.
+ Pong(u64),
+}
+
+/// A node's proof that it holds the key of its node ID (`identity`), sending from its
+/// transport address to the recipient's: a signature over both addresses
+/// (`transport::identity::signed`). WHO and ME end at the identity layer; the rest of
+/// the node never sees them.
+#[derive(Clone, Serialize, Deserialize)]
+pub(crate) struct IdentityProof {
+ /// The sender's public node key.
+ #[serde(with = "crate::transport::wire::codec::bytes32")]
+ pub(crate) key: [u8; 32],
+ #[serde(with = "crate::transport::wire::codec::bytes64")]
+ pub(crate) signature: [u8; 64],
+}
+
+impl std::fmt::Debug for IdentityProof {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("IdentityProof")
+ .field("key", &Hex(&self.key))
+ .finish_non_exhaustive()
+ }
}
/// Content-addressed bytes that any node holding them can serve, in chunks
/// (`swarm::blob`).
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum BlobId {
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub(crate) enum BlobId {
/// An Automerge change, by its hash: e.g. an epoch's snapshot change.
- Change(ChangeHash),
+ Change(#[serde(with = "crate::transport::wire::codec::change_hash")] ChangeHash),
/// Bytes by their BLAKE3 hash: e.g. the content of a remake.
- Content([u8; 32]),
+ Content(#[serde(with = "crate::transport::wire::codec::bytes32")] [u8; 32]),
+ /// The changes of a `DeltaSpec`, by its ID (`DeltaSpec::id`). The requests carry
+ /// the spec, so any node holding its `to` can serve it.
+ Delta(#[serde(with = "crate::transport::wire::codec::bytes32")] [u8; 32]),
}
impl std::fmt::Display for BlobId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BlobId::Change(hash) => write!(f, "change:{hash}"),
- BlobId::Content(hash) => {
- write!(f, "content:")?;
- hash.iter().try_for_each(|byte| write!(f, "{byte:02x}"))
- }
+ BlobId::Content(hash) => write!(f, "content:{}", Hex(hash)),
+ BlobId::Delta(id) => write!(f, "delta:{}", Hex(id)),
}
}
}
/// A blob request or one of its answers. Each is a message of its own, so no message
/// is larger than one chunk.
-#[derive(Debug, Clone)]
-pub struct BlobMessage {
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct BlobMessage {
/// The sender's epoch (every message carries it).
- pub epoch: Epoch,
+ #[serde(with = "crate::transport::wire::codec::epoch")]
+ pub(crate) epoch: Epoch,
/// See `SyncEnvelope::epoch_seq`.
- pub epoch_seq: u32,
- pub id: BlobId,
- pub body: BlobBody,
+ pub(crate) epoch_seq: u32,
+ pub(crate) id: BlobId,
+ pub(crate) body: BlobBody,
}
-#[derive(Clone)]
-pub enum BlobBody {
- /// Asks for these chunks; for the manifest if empty.
+#[derive(Clone, Serialize, Deserialize)]
+pub(crate) enum BlobBody {
+ /// Asks for these chunks; for the manifest if empty. `delta`: the spec of a
+ /// `BlobId::Delta`, which the ID alone doesn't say.
Request {
chunks: Vec<u32>,
+ delta: Option<DeltaSpec>,
},
/// Size and per-chunk BLAKE3 hashes; the last chunk may be shorter.
Manifest {
size: u64,
chunk_size: u32,
+ #[serde(with = "crate::transport::wire::codec::hashes32")]
hashes: Vec<[u8; 32]>,
},
Chunk {
index: u32,
+ #[serde(with = "crate::transport::wire::codec::shared_bytes")]
data: bytes::Bytes,
},
/// The sender doesn't have the blob.
@@ -165,9 +243,11 @@ pub enum BlobBody {
impl std::fmt::Debug for BlobBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- BlobBody::Request { chunks } => {
- f.debug_struct("Request").field("chunks", chunks).finish()
- }
+ BlobBody::Request { chunks, delta } => f
+ .debug_struct("Request")
+ .field("chunks", chunks)
+ .field("delta", &delta.is_some())
+ .finish(),
BlobBody::Manifest {
size,
chunk_size,
@@ -190,125 +270,439 @@ impl std::fmt::Debug for BlobBody {
/// What a message is, without its contents.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum MessageKind {
+pub(crate) enum MessageKind {
Hello,
HelloReply,
Sync,
+ Status,
+ StatusReply,
+ Blob,
+ Who,
+ Me,
Ping,
Pong,
- Blob,
}
impl ParsedMessage {
- pub fn kind(&self) -> MessageKind {
+ pub(crate) fn kind(&self) -> MessageKind {
match self {
ParsedMessage::Hello(_) => MessageKind::Hello,
ParsedMessage::HelloReply(_) => MessageKind::HelloReply,
ParsedMessage::Sync(_) => MessageKind::Sync,
+ ParsedMessage::Status(_) => MessageKind::Status,
+ ParsedMessage::StatusReply(_) => MessageKind::StatusReply,
+ ParsedMessage::Blob(_) => MessageKind::Blob,
+ ParsedMessage::Who(_) => MessageKind::Who,
+ ParsedMessage::Me(_) => MessageKind::Me,
ParsedMessage::Ping(_) => MessageKind::Ping,
ParsedMessage::Pong(_) => MessageKind::Pong,
- ParsedMessage::Blob(_) => MessageKind::Blob,
- }
- }
-
- /// Every message carries the sender's epoch.
- pub fn epoch(&self) -> Epoch {
- match self {
- ParsedMessage::Hello(hello) => hello.epoch,
- ParsedMessage::HelloReply(reply) => reply.epoch,
- ParsedMessage::Sync(envelope) => envelope.epoch,
- ParsedMessage::Ping(keepalive) | ParsedMessage::Pong(keepalive) => keepalive.epoch,
- ParsedMessage::Blob(blob) => blob.epoch,
}
}
- /// `SyncEnvelope::epoch_seq`, as every message carries it.
- pub fn epoch_seq(&self) -> u32 {
+ /// The sender's epoch and `SyncEnvelope::epoch_seq`, as every message carries them
+ /// but WHO, ME, PING and PONG (which the node never sees).
+ pub(crate) fn epoch_stamp(&self) -> Option<(Epoch, u32)> {
match self {
- ParsedMessage::Hello(hello) => hello.epoch_seq,
- ParsedMessage::HelloReply(reply) => reply.epoch_seq,
- ParsedMessage::Sync(envelope) => envelope.epoch_seq,
- ParsedMessage::Ping(keepalive) | ParsedMessage::Pong(keepalive) => keepalive.epoch_seq,
- ParsedMessage::Blob(blob) => blob.epoch_seq,
+ ParsedMessage::Hello(hello) => Some((hello.epoch, hello.epoch_seq)),
+ ParsedMessage::HelloReply(reply) => Some((reply.epoch, reply.epoch_seq)),
+ ParsedMessage::Sync(envelope) => Some((envelope.epoch, envelope.epoch_seq)),
+ ParsedMessage::Status(status) | ParsedMessage::StatusReply(status) => {
+ Some((status.epoch, status.epoch_seq))
+ }
+ ParsedMessage::Blob(blob) => Some((blob.epoch, blob.epoch_seq)),
+ ParsedMessage::Who(_)
+ | ParsedMessage::Me(_)
+ | ParsedMessage::Ping(_)
+ | ParsedMessage::Pong(_) => None,
}
}
}
-#[derive(Debug, Clone)]
-pub struct HelloRequest {
+#[derive(Clone, Serialize, Deserialize)]
+pub(crate) struct HelloRequest {
/// The sender's swarm, if it knows it (a new node doesn't yet). A receiver in
/// another swarm doesn't answer.
- pub swarm_id: Option<String>,
- pub epoch: Epoch,
+ pub(crate) swarm_id: Option<String>,
+ #[serde(with = "crate::transport::wire::codec::epoch")]
+ pub(crate) epoch: Epoch,
/// See `SyncEnvelope::epoch_seq`.
- pub epoch_seq: u32,
+ pub(crate) epoch_seq: u32,
/// Proof of the swarm key (`swarm::auth`); empty without one.
- pub auth: Vec<u8>,
+ #[serde(with = "serde_bytes")]
+ pub(crate) auth: Vec<u8>,
/// The sender is ready (same meaning as `SyncEnvelope::ready`).
- pub ready: bool,
+ pub(crate) ready: bool,
/// Random per run of the sender; the reply echoes it.
- pub run_id: RunId,
+ #[serde(with = "crate::transport::wire::codec::run_id")]
+ pub(crate) run_id: RunId,
+ /// The handshake's number: the sender numbers the handshakes it starts in a run,
+ /// with all peers, from 1. A retry carries the same number, so the recipient resets
+ /// its sync session only for a new handshake, not for every resend or late copy.
+ pub(crate) handshake: u32,
}
-#[derive(Debug, Clone)]
-pub struct HelloReply {
- pub swarm_id: String,
- pub peers: Vec<String>,
- pub epoch: Epoch,
+/// A peer a HELLO_REPLY lists: who it is, and where the sender reaches it.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub(crate) struct ListedPeer {
+ pub(crate) id: String,
+ /// As `Transport::addresses_of` gives them; hints for the recipient.
+ pub(crate) addresses: Vec<String>,
+}
+
+/// Where messages to a peer go now (`Transport::destination`).
+#[derive(Debug, Clone, PartialEq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct Destination {
+ /// As the identity layer keeps it: `<scheme>:<transport identity>`.
+ pub(crate) address: String,
+ /// The peer proved itself there; else it is only said to be there, and sends wait
+ /// for its proof.
+ pub(crate) proven: bool,
+ /// The transport's connection to it, if the transport has connections.
+ pub(crate) connection: Option<Connection>,
+ /// Every address it proved itself at, best first, as the paths layer measures them
+ /// (`transport::paths`).
+ pub(crate) paths: Vec<stats::PathInfo>,
+}
+
+/// How a transport with connections (libp2p) is connected to a peer.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) enum Connection {
+ Direct,
+ /// Only through a relay.
+ Relayed,
+}
+
+impl Connection {
+ pub(crate) fn as_str(self) -> &'static str {
+ match self {
+ Self::Direct => "direct",
+ Self::Relayed => "relayed",
+ }
+ }
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+pub(crate) struct HelloReply {
+ pub(crate) swarm_id: String,
+ pub(crate) peers: Vec<ListedPeer>,
+ #[serde(with = "crate::transport::wire::codec::epoch")]
+ pub(crate) epoch: Epoch,
/// See `SyncEnvelope::epoch_seq`.
- pub epoch_seq: u32,
+ pub(crate) epoch_seq: u32,
/// Proof of the swarm key (`swarm::auth`); empty without one.
- pub auth: Vec<u8>,
+ #[serde(with = "serde_bytes")]
+ pub(crate) auth: Vec<u8>,
/// The sender is ready.
- pub ready: bool,
+ pub(crate) ready: bool,
/// The `run_id` of the HELLO answered. A reply to a HELLO from an earlier run of the
/// recipient (delivered late, or queued while it was down) is ignored.
- pub run_id: RunId,
+ #[serde(with = "crate::transport::wire::codec::run_id")]
+ pub(crate) run_id: RunId,
/// How long before this HELLO the sender had last heard from the recipient, if ever
/// (in milliseconds): tells a restarted node how long it was away.
- pub away_ms: Option<u64>,
+ pub(crate) away_ms: Option<u64>,
/// The sender's own run (as its HELLOs carry it): a new one means it restarted.
- pub sender_run_id: RunId,
+ #[serde(with = "crate::transport::wire::codec::run_id")]
+ pub(crate) sender_run_id: RunId,
}
-#[derive(Debug, Clone)]
-pub struct SyncEnvelope {
- pub swarm_id: String,
+/// Bytes shown as lowercase hex, e.g. run IDs in logs.
+pub(crate) struct Hex<'a>(pub(crate) &'a [u8]);
+
+impl std::fmt::Display for Hex<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.iter().try_for_each(|byte| write!(f, "{byte:02x}"))
+ }
+}
+
+impl std::fmt::Debug for Hex<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ std::fmt::Display::fmt(self, f)
+ }
+}
+
+/// Whether a handshake carries a swarm-key proof, not the proof: logged messages don't
+/// hand out tags, and stay short.
+fn auth_shown(auth: &[u8]) -> &'static str {
+ if auth.is_empty() { "none" } else { "present" }
+}
+
+impl std::fmt::Debug for HelloRequest {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("HelloRequest")
+ .field("swarm_id", &self.swarm_id)
+ .field("epoch", &self.epoch)
+ .field("epoch_seq", &self.epoch_seq)
+ .field("auth", &auth_shown(&self.auth))
+ .field("ready", &self.ready)
+ .field("run_id", &Hex(&self.run_id))
+ .field("handshake", &self.handshake)
+ .finish()
+ }
+}
+
+impl std::fmt::Debug for HelloReply {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("HelloReply")
+ .field("swarm_id", &self.swarm_id)
+ .field("peers", &self.peers.len())
+ .field("epoch", &self.epoch)
+ .field("epoch_seq", &self.epoch_seq)
+ .field("auth", &auth_shown(&self.auth))
+ .field("ready", &self.ready)
+ .field("run_id", &Hex(&self.run_id))
+ .field("away_ms", &self.away_ms)
+ .field("sender_run_id", &Hex(&self.sender_run_id))
+ .finish()
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) struct SyncEnvelope {
+ pub(crate) swarm_id: String,
/// Epoch of the document the message was generated from.
- pub epoch: Epoch,
+ #[serde(with = "crate::transport::wire::codec::epoch")]
+ pub(crate) epoch: Epoch,
/// How many times the sender changed epoch in its current run, when it sent this
/// (`Replica::epoch_seq`). Messages may arrive late and out of order: the receiver
/// ignores an epoch older than the newest it has seen in the same session.
- pub epoch_seq: u32,
- pub message: Message,
+ pub(crate) epoch_seq: u32,
+ pub(crate) body: SyncBody,
/// The sender was ready when it generated the message: its document is the swarm's,
/// not one it is still filling (joining, rejoining). Only such a peer can tell a
/// catching-up node that it has everything; an empty document says nothing else.
- pub ready: bool,
+ pub(crate) ready: bool,
+}
+
+/// What a sync envelope carries.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub(crate) enum SyncBody {
+ /// An Automerge sync message.
+ Message(#[serde(with = "crate::transport::wire::codec::sync_message")] SyncPayload),
+ /// Instead of a message too large to send in one piece: fetch these changes as a
+ /// blob (`BlobId::Delta`), then sync on.
+ Delta(DeltaSpec),
+}
+
+/// The changes in the history of `to` but not of `from`: what a peer at `from` lacks of
+/// a document at `to`. Every node holding `to` produces the same bytes for it
+/// (`swarm::blob::delta_bytes`).
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub(crate) struct DeltaSpec {
+ /// Sorted.
+ #[serde(with = "crate::transport::wire::codec::change_hashes")]
+ pub(crate) from: Vec<ChangeHash>,
+ /// Sorted.
+ #[serde(with = "crate::transport::wire::codec::change_hashes")]
+ pub(crate) to: Vec<ChangeHash>,
+}
+
+impl DeltaSpec {
+ pub(crate) fn new(mut from: Vec<ChangeHash>, mut to: Vec<ChangeHash>) -> Self {
+ from.sort_unstable();
+ from.dedup();
+ to.sort_unstable();
+ to.dedup();
+ Self { from, to }
+ }
+
+ /// Names the spec as a blob: SHA-256 over both lists.
+ pub(crate) fn id(&self) -> [u8; 32] {
+ let mut hasher = Sha256::new();
+ hasher.update(b"continuum/delta/1");
+ for heads in [&self.from, &self.to] {
+ hasher.update((heads.len() as u32).to_be_bytes());
+ for head in heads {
+ hasher.update(head.0);
+ }
+ }
+ hasher.finalize().into()
+ }
+}
+
+/// An Automerge sync message in Automerge's own encoding. Decoded only where it is
+/// applied (`decode`), after the sender proved to be a member and off the consumer: a
+/// large message from anyone else costs nothing but its bytes.
+#[derive(Clone)]
+pub(crate) struct SyncPayload(pub(crate) bytes::Bytes);
+
+impl SyncPayload {
+ pub(crate) fn encode(message: Message) -> Self {
+ Self(message.encode().into())
+ }
+
+ pub(crate) fn decode(&self) -> Result<Message> {
+ Message::decode(&self.0)
+ .map_err(|error| anyhow::anyhow!("Automerge decode failed: {error:?}"))
+ }
+}
+
+/// Sizes instead of contents: logged messages stay short.
+impl std::fmt::Debug for SyncPayload {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "SyncPayload({} bytes)", self.0.len())
+ }
}
/// A message serialized for the wire. Cloning is cheap (shared buffer).
#[derive(Clone, Debug)]
-pub struct EncodedMessage {
- pub peer: String,
- pub body: bytes::Bytes,
- pub kind: MessageKind,
+pub(crate) struct EncodedMessage {
+ pub(crate) peer: String,
+ /// Where the transport sends it: `peer` itself for a transport whose identities are
+ /// addresses, the peer's address under the identity layer (`transport::identity`).
+ pub(crate) address: String,
+ pub(crate) body: bytes::Bytes,
+ pub(crate) kind: MessageKind,
/// Bytes the message takes on the wire (without the transport's own framing).
- pub payload_len: usize,
+ pub(crate) payload_len: usize,
+ /// The message itself, under the paths layer (`transport::paths`): to encode it
+ /// again for another transport when a send there fails. None from a transport.
+ pub(crate) message: Option<std::sync::Arc<ParsedMessage>>,
}
/// Called with the sender, the message and its `payload_len` (as in `EncodedMessage`).
-pub type HandlerFn = dyn Fn(String, ParsedMessage, usize) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>
+pub(crate) type HandlerFn = dyn Fn(String, ParsedMessage, usize) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>
+ Send
+ Sync
+ 'static;
-pub type MessageHandler = Box<HandlerFn>;
+pub(crate) type MessageHandler = Box<HandlerFn>;
+
+/// Whether `message` is a PING or PONG. The paths layer times them, so a transport's
+/// consumer hands them on as soon as they are read (`hand_on_probe`), not after the
+/// messages read before them are handled (which can take a while: sync).
+pub(crate) fn is_probe(message: &ParsedMessage) -> bool {
+ matches!(message, ParsedMessage::Ping(_) | ParsedMessage::Pong(_))
+}
+
+/// Hands a PING or PONG (`is_probe`) to `handler` in a task of its own.
+pub(crate) fn hand_on_probe(
+ handler: &std::sync::Arc<HandlerFn>,
+ sender: String,
+ message: ParsedMessage,
+ payload_len: usize,
+) {
+ let handler = std::sync::Arc::clone(handler);
+ tokio::spawn(async move {
+ if let Err(error) = handler(sender.clone(), message, payload_len).await {
+ tracing::warn!(peer = %sender, error = %format!("{error:#}"), "Failed to process inbound message");
+ }
+ });
+}
#[async_trait]
-pub trait Transport: Send + Sync {
+pub(crate) trait Transport: Send + Sync {
/// This node's identity on the transport (for Mycelium: its public key).
fn local_id(&self) -> &str;
+ /// Whether `id` can be a node's identity on this transport (for Mycelium: a public
+ /// key). Identities other peers name to us (in a HELLO_REPLY) are checked with it.
+ fn is_peer_id(&self, id: &str) -> bool;
+
+ /// How long the transport tries to deliver a message before dropping it: after that,
+ /// a message sent is delivered or lost for good.
+ fn delivery_limit(&self) -> std::time::Duration;
+
+ /// Where this node can be reached, as addresses other nodes can use: published in
+ /// the member list (`swarm::members`). By default its identity, for a transport
+ /// whose identities are addresses (Mycelium: a public key).
+ fn local_addresses(&self) -> Vec<String> {
+ vec![self.local_id().to_owned()]
+ }
+
+ /// For a transport under the identity layer: the transport identity at `target`, an
+ /// address of this transport without its scheme, noting where it is if `target` says
+ /// more than who (as a libp2p multiaddr ending in the peer ID does). None if
+ /// `target` isn't one of this transport's. By default `target` itself, for a
+ /// transport whose identities are addresses.
+ fn locate(&self, target: &str) -> Option<String> {
+ self.is_peer_id(target).then(|| target.to_owned())
+ }
+
+ /// For a transport under the identity layer: where the node with transport identity
+ /// `target` is, as addresses others can use (`locate` reads them). By default
+ /// `target` itself, as for `locate`.
+ fn locations(&self, target: &str) -> Vec<String> {
+ vec![target.to_owned()]
+ }
+
+ /// Where `peer` can be reached, as far as this node knows: passed on to others (in
+ /// a HELLO_REPLY). By default its identity, as for `local_addresses`.
+ fn addresses_of(&self, peer: &str) -> Vec<String> {
+ vec![peer.to_owned()]
+ }
+
+ /// The transports this node is on, as the schemes of their addresses
+ /// (`swarm::topology`), whether or not it has an address on each (one that only
+ /// connects out has none). By default the empty scheme, for a transport whose
+ /// identities are addresses.
+ fn transports(&self) -> Vec<String> {
+ vec![String::new()]
+ }
+
+ /// Seeds the transport found by itself since last asked (libp2p: nodes on the LAN),
+ /// for a node that is joining. By default none.
+ fn discovered_seeds(&self) -> Vec<String> {
+ Vec::new()
+ }
+
+ /// Whether this node has a way to reach `peer` at all: an address of it on one of
+ /// its transports. Members it can't reach aren't its neighbours (`swarm::topology`).
+ /// By default yes, as for `local_addresses`.
+ fn can_reach(&self, peer: &str) -> bool {
+ let _ = peer;
+ true
+ }
+
+ /// Where others say `peer` can be reached (the member list, a HELLO_REPLY): hints,
+ /// to be checked before they are relied on. By default nothing to learn, as for
+ /// `local_addresses`.
+ fn learn_addresses(&self, peer: &str, addresses: &[String]) {
+ let _ = (peer, addresses);
+ }
+
+ /// Where messages to `peer` go now. None if nowhere, and by default, for a
+ /// transport whose identities are addresses (`peer` is where they go).
+ fn destination(&self, peer: &str) -> Option<Destination> {
+ let _ = peer;
+ None
+ }
+
+ /// Where messages to `target` (a transport identity) go now, as an address that
+ /// names `target` too: the remote address of the connection in use, say. By default
+ /// nowhere but `target` itself (Mycelium).
+ fn location(&self, target: &str) -> Option<String> {
+ let _ = target;
+ None
+ }
+
+ /// How this node is connected to `target` (a transport identity), if it is. By
+ /// default not at all, for a transport without connections (Mycelium).
+ fn connection(&self, target: &str) -> Option<Connection> {
+ let _ = target;
+ None
+ }
+
+ /// The smoothed round-trip time of the path messages to `peer` go over, if
+ /// measured (`transport::paths`). By default none.
+ fn rtt(&self, peer: &str) -> Option<std::time::Duration> {
+ let _ = peer;
+ None
+ }
+
+ /// Whether the transport answers PINGs with PONGs itself (the paths layer,
+ /// `transport::paths`); if not (a bare transport, in tests), the node does. By
+ /// default not.
+ fn answers_pings(&self) -> bool {
+ false
+ }
+
+ /// Figures about the transport itself (`stats`). By default none.
+ fn stats(&self) -> stats::TransportStats {
+ stats::TransportStats::default()
+ }
+
/// Resolves a seed (anything that leads to a node, e.g. an address) to that node's
/// identity, as `local_id` gives it on that node.
async fn resolve_seed(&self, seed: &str) -> Result<String>;
@@ -347,7 +741,16 @@ mod tests {
let mut shuffled = heads.clone();
rng.shuffle(&mut shuffled);
assert_eq!(summary, HeadsSummary::of(shuffled));
- assert_eq!(summary, HeadsSummary::Digest(summary.digest()));
+ assert_eq!(
+ summary,
+ HeadsSummary::Digest {
+ digest: summary.digest(),
+ sample: Vec::new(),
+ }
+ );
+ // A sample is some of the heads.
+ assert!(summary.listed().iter().all(|head| heads.contains(head)));
+ assert_eq!(summary.listed().len(), n.min(MAX_LISTED_HEADS));
let mut other = heads.clone();
match rng.below(3) {
@@ -364,4 +767,22 @@ mod tests {
assert_ne!(summary, HeadsSummary::of(other));
});
}
+
+ /// Successive samples cover every head.
+ #[test]
+ fn samples_rotate_over_all_heads() {
+ let heads: Vec<ChangeHash> = (0..100u8).map(|n| ChangeHash([n; 32])).collect();
+ let mut seen = std::collections::HashSet::new();
+ for _ in 0..heads.len().div_ceil(MAX_LISTED_HEADS) + 1 {
+ seen.extend(HeadsSummary::of(heads.clone()).listed().iter().copied());
+ }
+ // Other tests sample concurrently and move the window too, so allow a few more.
+ for _ in 0..100 {
+ if seen.len() == heads.len() {
+ break;
+ }
+ seen.extend(HeadsSummary::of(heads.clone()).listed().iter().copied());
+ }
+ assert_eq!(seen.len(), heads.len());
+ }
}