summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-27 01:34:15 +0100
committertv <tv@krebsco.de>2026-10-27 01:34:15 +0100
commit2593edca179e86883e74c7039629564f4ea6ec55 (patch)
tree64257a6f5b1cf79b0e8d39bdfbfadb3e72c78e9c
parent4cdece6d0f8e25ac154c3fd0f30bca604b08c16a (diff)
admit nodeId and addresses in the API
-rw-r--r--docs/openapi.yaml36
-rw-r--r--src/api/peers.rs23
-rw-r--r--src/api/stats.rs7
-rw-r--r--src/api/stats/prometheus.rs8
-rw-r--r--src/config/mod.rs2
-rw-r--r--src/swarm/peer/handshake.rs9
-rw-r--r--src/transport/identity.rs16
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();