diff options
| -rw-r--r-- | docs/openapi.yaml | 36 | ||||
| -rw-r--r-- | src/api/peers.rs | 23 | ||||
| -rw-r--r-- | src/api/stats.rs | 7 | ||||
| -rw-r--r-- | src/api/stats/prometheus.rs | 8 | ||||
| -rw-r--r-- | src/config/mod.rs | 2 | ||||
| -rw-r--r-- | src/swarm/peer/handshake.rs | 9 | ||||
| -rw-r--r-- | src/transport/identity.rs | 16 |
7 files changed, 70 insertions, 31 deletions
diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 3025146..47c7596 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -210,7 +210,7 @@ paths: summary: List known peers responses: '200': - description: Peers, by public key. + description: Peers, by node ID. content: application/json: schema: @@ -230,7 +230,7 @@ paths: - name: peer in: path required: true - description: The peer's public key. + description: The peer's node ID (`continuum:<hex>`). schema: type: string - name: force @@ -340,7 +340,7 @@ paths: description: | JSON by default. When `Accept` names `application/openmetrics-text` or `text/plain` (as Prometheus does), the Prometheus text format (0.0.4) instead, - with per-peer series labelled `peer` (the public key). Aliases are only in + with per-peer series labelled `peer` (the node ID). Aliases are only in `continuum_info` and `continuum_peer_info`, for joining. responses: '200': @@ -1090,10 +1090,15 @@ components: PeerList: type: object - required: [localEpoch, localHeads, peers] + required: [localAddresses, localEpoch, localHeads, peers] properties: localAlias: type: [string, 'null'] + localAddresses: + type: array + items: + type: string + description: Where this node can be reached, e.g. `mycelium:<public key>`. localEpoch: $ref: '#/components/schemas/Epoch' localHeads: @@ -1101,17 +1106,24 @@ components: description: 'Heads of the local document (more than one: concurrent changes not yet merged).' peers: type: object - description: By public key. + description: By node ID. additionalProperties: $ref: '#/components/schemas/Peer' Peer: type: object - required: [status, handshakeComplete, hasPendingDelivery, neighbour, traffic, incidents] + required: [addresses, status, handshakeComplete, hasPendingDelivery, neighbour, traffic, incidents] properties: alias: type: [string, 'null'] description: The name the peer gave itself (`--alias`); not unique. + addresses: + type: array + items: + type: string + description: | + Where this node reaches the peer: the address it proved itself at, else + where others say it is. validatedSwarm: type: [string, 'null'] status: @@ -1221,10 +1233,16 @@ components: Stats: type: object - required: [publicKey, uptimeSecs, ready, epoch, document, catchUp, remakes, sync, relay, topology, stability, staleHelloReplies, traffic, incidents, propagation, disk] + required: [nodeId, addresses, uptimeSecs, ready, epoch, document, catchUp, remakes, sync, relay, topology, stability, staleHelloReplies, traffic, incidents, propagation, disk] properties: - publicKey: + nodeId: type: string + description: '`continuum:<hex>`, from the node key.' + addresses: + type: array + items: + type: string + description: Where this node can be reached, e.g. `mycelium:<public key>`. alias: type: [string, 'null'] uptimeSecs: @@ -1600,7 +1618,7 @@ components: votes: type: object description: | - Votes for this remake by member public key, from the votes register (only + Votes for this remake by member node ID, from the votes register (only electorate members count; one that also voted for another remake counts for neither). additionalProperties: diff --git a/src/api/peers.rs b/src/api/peers.rs index bc32ae1..9e907ac 100644 --- a/src/api/peers.rs +++ b/src/api/peers.rs @@ -48,6 +48,8 @@ impl From<Epoch> for EpochInfo { pub(crate) struct PeerStatus { /// The name the peer gave itself in the member list, if any (not unique). alias: Option<String>, + /// Where this node reaches it, or would (`Transport::addresses_of`). + addresses: Vec<String>, validated_swarm: Option<String>, status: PeerLivenessStatus, handshake_complete: bool, @@ -93,7 +95,9 @@ pub(crate) struct PeerListResponse { /// Number of heads of our document. Several heads are concurrent changes no later /// change has merged yet. local_heads: usize, - /// By public key. + /// Where this node can be reached (`Transport::local_addresses`). + local_addresses: Vec<String>, + /// By node ID. peers: BTreeMap<String, PeerStatus>, } @@ -114,17 +118,17 @@ pub(crate) async fn list_peers( let mut aliases = crate::swarm::members::aliases(&state.doc.read().await.doc); let mut peers = BTreeMap::new(); - for (public_key, peer_state_mutex) in snapshot { - let neighbour = crate::swarm::topology::is_neighbour(&state, &public_key); + for (peer, peer_state_mutex) in snapshot { + let neighbour = crate::swarm::topology::is_neighbour(&state, &peer); let rtt_ms = state .lock_rtt() - .effective(&public_key, now, state.timing.rtt_fresh_for()) + .effective(&peer, now, state.timing.rtt_fresh_for()) .map(|rtt| rtt.as_secs_f64() * 1000.0); - let traffic = state.traffic.peer(&public_key).unwrap_or_default(); + let traffic = state.traffic.peer(&peer).unwrap_or_default(); let sync_throughput = state .throughput .lock_or_recover() - .get(&public_key) + .get(&peer) .map(|estimate| ThroughputInfo { bytes_per_sec: estimate.bytes_per_sec.round(), samples: estimate.samples, @@ -140,11 +144,13 @@ pub(crate) async fn list_peers( ), }; - let alias = aliases.remove(&public_key); + let alias = aliases.remove(&peer); + let addresses = state.transport.addresses_of(&peer); peers.insert( - public_key, + peer, PeerStatus { alias, + addresses, validated_swarm: ps.standing.swarm_id().map(str::to_owned), status, handshake_complete: ps.hello_state == HelloState::Established, @@ -168,6 +174,7 @@ pub(crate) async fn list_peers( let local_heads = state.doc.read().await.heads().len(); Ok(Json(PeerListResponse { local_alias: state.alias.clone(), + local_addresses: state.transport.local_addresses(), local_epoch: local_epoch.into(), local_heads, peers, diff --git a/src/api/stats.rs b/src/api/stats.rs index fabbffd..fa64600 100644 --- a/src/api/stats.rs +++ b/src/api/stats.rs @@ -21,7 +21,9 @@ use crate::transport::traffic::PeerTraffic; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct StatsResponse { - public_key: String, + node_id: String, + /// Where this node can be reached (`Transport::local_addresses`). + addresses: Vec<String>, alias: Option<String>, uptime_secs: u64, ready: bool, @@ -228,7 +230,8 @@ async fn collect(state: &AppState) -> StatsResponse { } StatsResponse { - public_key: state.local_id().to_owned(), + node_id: state.local_id().to_owned(), + addresses: state.transport.local_addresses(), alias: state.alias.clone(), uptime_secs: node.uptime.as_secs(), ready: state.is_ready().await, diff --git a/src/api/stats/prometheus.rs b/src/api/stats/prometheus.rs index 8d31eb0..01cd271 100644 --- a/src/api/stats/prometheus.rs +++ b/src/api/stats/prometheus.rs @@ -190,15 +190,11 @@ pub(super) async fn render(state: &AppState, stats: &StatsResponse) -> String { // Names for joining (`* on(peer) group_left(alias) continuum_peer_info`), kept out // of the other series so a rename doesn't start new ones. - out.family( - "info", - "gauge", - "This node's public key and alias (always 1).", - ); + out.family("info", "gauge", "This node's node ID and alias (always 1)."); out.sample( "info", &[ - ("public_key", &stats.public_key), + ("node_id", &stats.node_id), ("alias", stats.alias.as_deref().unwrap_or("")), ], 1.0, diff --git a/src/config/mod.rs b/src/config/mod.rs index 65d481d..811ad57 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -121,7 +121,7 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ Kind::List, "STRING,[STRING...]", None, - "How to reach a node of the swarm to join: a Mycelium public key, a Mycelium IPv6 address, or a host name with an AAAA record for one. A node keeps trying until it has joined.", + "How to reach a node of the swarm to join: its address (`mycelium:<public key>`, as GET /admin/stats lists it), a Mycelium public key, a Mycelium IPv6 address, or a host name with an AAAA record for one. A node keeps trying until it has joined.", ), option( "genesis", diff --git a/src/swarm/peer/handshake.rs b/src/swarm/peer/handshake.rs index 4a7f451..1183db1 100644 --- a/src/swarm/peer/handshake.rs +++ b/src/swarm/peer/handshake.rs @@ -160,7 +160,14 @@ async fn transmit_hello(state: &AppState, peer: &str) -> Result<()> { } } - result.context("Sending HELLO") + match result { + // The first HELLO to a peer known only from hints: expected, and retried. + Err(error) if transport::identity::is_not_proven(&error) => { + debug!(peer = %peer, "HELLO waits until the peer proves where it is; will retry"); + Ok(()) + } + result => result.context("Sending HELLO"), + } } pub(super) async fn ensure_handshake(state: &AppState, peer: &str) { diff --git a/src/transport/identity.rs b/src/transport/identity.rs index 712e1f9..6f000d0 100644 --- a/src/transport/identity.rs +++ b/src/transport/identity.rs @@ -49,6 +49,17 @@ const MAX_HINTS: usize = 8; /// anyone can send us proofs (keys are free), so the books are bounded. const MAX_ENTRIES: usize = 4096; +/// A send refused because the peer hasn't proven where it is yet (`send_encoded`): +/// expected when a peer known only from hints is first contacted, and retried. +#[derive(Debug, thiserror::Error)] +#[error("{0} hasn't proven where it is yet; asked where it is said to be")] +pub(crate) struct NotProven(String); + +/// Whether `error` is (or was caused by) a [`NotProven`]. +pub(crate) fn is_not_proven(error: &anyhow::Error) -> bool { + error.chain().any(|cause| cause.is::<NotProven>()) +} + /// What a node under the identity layer needs (`node::NodeConfig::identity`). #[derive(Clone)] pub(crate) struct IdentityConfig { @@ -426,10 +437,7 @@ impl Transport for IdentityTransport { for target in hints { self.shared.ask(&target, false); } - bail!( - "{} hasn't proven where it is yet; asked where it is said to be", - message.peer - ); + return Err(NotProven(message.peer.clone()).into()); } let mut inner = message.clone(); inner.peer = message.address.clone(); |
