Peers
attacca_api peer rendezvous — publish a node's iroh address, list and look up the account's nodes by path, get Attacca's relay and request chat files over the peer link.
Two nodes on the same account can move files directly, without Attacca carrying the bytes. Attacca's
part is the address book: each node publishes where it can be reached, and looks the others up.
zyris-transfer builds on these tools; this page is what they do on
their own.
Every tool here needs peers:write, except request_file, which needs files:read.
The tools
peer_publish(endpoint_id, addrs: Vec<String>) -> ()
Publish this node's own iroh address: its EndpointId (an ed25519 public key) and hole-punching
candidate addresses. Call again whenever the addresses change.
endpoint_id keeps whatever value was published first. A later request with a different one is
rejected, so the server never becomes a place that quietly swaps the key. Persist the endpoint's
secret key; generating a new one is a new identity this node cannot publish.
peer_list() -> Vec<ZPeerEntry>
The nodes on the same account — what a listener uses to decide whether an incoming connection may be accepted.
ZPeerEntry field | Type | Notes |
|---|---|---|
node_id | String | |
path | String | The node path, system/program/node. A label, not a trust anchor — see below. |
endpoint_id | String | The published iroh EndpointId. |
online | bool | |
kind | ZPeerKind | node (default) or server. |
A server entry is Attacca's own endpoint, which offers chat files — request_file, or an agent
sending one. Accept offers from either kind, but do not show a server entry as one of the person's
machines, and never pin anything under its path. A server that predates the field omits it and only
ever listed nodes, so it decodes as node.
peer_lookup(path) -> ZPeerAddr
Ask for one node's address by its node path — laptop/zyris-code/myrepo, the value of
zyris::NodeAddress::path(). The request is {"path": "…"}. A path is unique among live nodes, so
there is at most one answer; a path no live node holds is refused, never answered with a near match.
ZPeerAddr field | Type | Notes |
|---|---|---|
node_id | String | |
path | String | The path it was looked up by. |
endpoint_id | String | The key the peer proves its identity with. |
addrs | Vec<String> | Hole-punching candidates. Defaults to empty when absent. |
relay_url | Option<String> | The relay the deployment operates. Omitted, not null, when unset. |
online | bool |
peer_relay() -> Option<String>
Attacca's own iroh relay — the one relay a Zyris endpoint should use. None when the deployment runs
none, in which case peers connect directly or not at all.
request_file(file_id) -> ZFileOffered
Have Attacca offer a chat file to the calling node over the peer link. Attacca dials this node's published address and offers the file; the node pulls it into its inbox the way it receives from any peer, and the call returns once the offer was taken. It is refused for a connection that is not a node, or a node that has not published a peer address.
ZFileOffered { transfer_id, name, size: u64, sha256 } describes the offer.
Paths are labels, not trust anchors
Do not key a peer-key pin on path, and not on node_id either. Both are issued by the server, and
the server is exactly the party a pin exists to constrain: it could present a substituted peer as a
new node, which arrives as "never seen before", passes, and gets pinned.
A path looks user-chosen but is not. Every segment is a slug the server derived and deduplicated — the system from what the approving person picked (pre-filled from the enrolling machine's unverified hostname), the program from what the program called itself, the node from what the connection asked for. A path is freed once its connection is gone past the resume grace, so a different connection can take it; "same path, different key" is ordinary.
The anchor comes from a person: the peer's key fingerprint is confirmed once, out of band, and the pin
binds to that key. The path only says which peer the user meant. zyris-transfer keys its pins on
the name the caller asked for and asks a
PeerConfirmer before trusting a new key.
Examples
Publishing, listing and looking up:
use zyris_attacca::{AttaccaApi, AttaccaApiClient, ZPeerAddr, ZPeerKind};
async fn announce(api: &AttaccaApiClient, endpoint_id: String, addrs: Vec<String>) -> zyris::Result<()> {
// The first endpoint_id published sticks; a different one later is rejected.
api.peer_publish(endpoint_id, addrs).await
}
async fn my_machines(api: &AttaccaApiClient) -> zyris::Result<Vec<String>> {
Ok(api
.peer_list()
.await?
.into_iter()
.filter(|peer| peer.kind == ZPeerKind::Node) // not Attacca's own endpoint
.map(|peer| format!("{} online={}", peer.path, peer.online))
.collect())
}
async fn find(api: &AttaccaApiClient, path: &str) -> zyris::Result<ZPeerAddr> {
let addr = api.peer_lookup(path.to_string()).await?;
if !addr.path.eq_ignore_ascii_case(path) {
return Err(zyris::Error::invalid_params(format!("asked for {path}, answered {}", addr.path)));
}
if !addr.online {
return Err(zyris::Error::invalid_params(format!("{path} is not connected")));
}
Ok(addr)
}find repeats the checks zyris-transfer makes before it sends: refuse an answer about a node that
was not named, and do not dial one that is offline.
Building the endpoint with Attacca's relay, and pulling a chat file. This needs the p2p feature of
zyris (features = ["tls-ring", "p2p"]):
use zyris::p2p::{endpoint::private_endpoint, iroh};
use zyris_attacca::{AttaccaApi, AttaccaApiClient};
async fn endpoint(api: &AttaccaApiClient, secret: iroh::SecretKey) -> Result<iroh::Endpoint, Box<dyn std::error::Error>> {
let relay = match api.peer_relay().await? {
Some(url) => Some(url.parse::<iroh::RelayUrl>()?),
None => None, // this deployment runs no relay: direct connections only
};
Ok(private_endpoint(secret, relay).await?)
}
async fn fetch_chat_file(api: &AttaccaApiClient, file_id: String) -> zyris::Result<()> {
// Requires a published peer address and a running peer listener on this node.
let offered = api.request_file(file_id).await?;
println!("{} ({} bytes, sha256 {}) as transfer {}", offered.name, offered.size, offered.sha256, offered.transfer_id);
Ok(())
}private_endpoint builds an iroh endpoint that touches no public iroh infrastructure — no n0
discovery, and a relay only if it is Attacca's. The listener that receives the offered file is
serve_peers.