Nostr events used by the transport

This describes the current local implementation, not a claim of verified Rust interoperability.

Kind 37195: overlay advert

Illustrative event matching the shape emitted by publishOverlayAdvert(). The ID, key, signature, and relay URL below are placeholders, not a valid signed event or a recommended public relay.

{
  "id": "<64-hex-event-hash>",
  "pubkey": "<64-hex-x-only-FIPS-public-key>",
  "created_at": 1804377600,
  "kind": 37195,
  "tags": [
    ["d", "fips-overlay-v1"],
    ["protocol", "fips-overlay-v1"],
    ["version", "1"],
    ["expiration", "1804381200"]
  ],
  "content": "{\"identifier\":\"fips-overlay-v1\",\"version\":1,\"endpoints\":[{\"transport\":\"udp\",\"addr\":\"nat\"}],\"signalRelays\":[\"wss://relay.example.com\"],\"stunServers\":[\"stun.l.google.com:19302\",\"stun.cloudflare.com:3478\"]}",
  "sig": "<128-hex-schnorr-signature>"
}

Decoded content, for readability:

{
  "identifier": "fips-overlay-v1",
  "version": 1,
  "endpoints": [{ "transport": "udp", "addr": "nat" }],
  "signalRelays": ["wss://relay.example.com"],
  "stunServers": ["stun.l.google.com:19302", "stun.cloudflare.com:3478"]
}

Publication versus invite generation

The runtime publishes on startup and periodically. Signed invite/acceptance completion additionally persists the relay, starts its incoming subscription, and requires advert acceptance on that relay before returning the code. Subscription health is not yet verified. See pairing.

Remote advert lookup remains pending: resolve() uses the local roster key. Existing dialing does not yet depend on fetching the advert.

STUN and private candidate exchange

For each traversal attempt, both peers independently open a UDP socket and query STUN. They may use different STUN servers; they do not need accounts or a shared STUN session. The server reports each socket's externally observed endpoint.

Those endpoints are sent in encrypted kind 21059 offer/answer payloads as reflexiveAddress, alongside localAddresses. They are not published in our kind 37195 advert. Each peer then probes the other's candidates using the same socket. A mapping learned against STUN can still be unsuitable for the peer destination, so traversal is best effort.

Current diagnostics

Collecting a failed attempt

  1. Update and deliberately restart both paired instances, preserving their databases.
  2. Run /fips ping <id> on one side only.
  3. Capture its error and [fips] logs from both terminals. Search for the same attempt prefix; if the receiver logs no event, compare subscription URLs and the initiator's relay acknowledgement first.
  4. If the receiver logs a rejected offer, inspect that reason. If it publishes an accepted answer but the initiator sees none, inspect the initiator subscription.
  5. Only move to NAT/firewall investigation once answer matching and UDP punching are reached. Successful punching still requires the FMP/FSP handshakes.

EOSE/closure callbacks are aggregate pool notifications, not continuous per-relay health checks. There is no persistent per-peer diagnostic history or dedicated diagnostics UI yet. An unknown peer is logged/rejected locally without sending it a denial message. Full signaling schemas, freshness/replay limits, rate limits, and transport cancellation remain in the implementation plan.