Standardize naming conventions across docs and source

Rename NodeId to NodeAddr and npub to pubkey throughout documentation
and source code for consistency with the established identity model.
Add FIPS API vs IPv6 adapter overview to session protocol document.
Add transport address terminology note to wire protocol document.
This commit is contained in:
Johnathan Corgan
2026-02-06 04:03:32 +00:00
parent 7c8a5bd5ae
commit 5e7342c57c
18 changed files with 711 additions and 641 deletions

View File

@@ -35,7 +35,7 @@ Node
├── coord_cache: CoordCache // address → coordinates for routing
├── transports: HashMap<TransportId, Transport>
├── links: HashMap<LinkId, Link>
└── peers: HashMap<NodeId, Peer>
└── peers: HashMap<NodeAddr, Peer>
```
### Identity
@@ -46,13 +46,27 @@ Cryptographic identity using Nostr keys (secp256k1).
Identity
├── npub: PublicKey // public key (bech32: npub1...)
├── nsec: SecretKey // secret key (bech32: nsec1...)
├── node_id: NodeId // SHA-256(npub), 32 bytes
└── address: FipsAddress // IPv6 ULA derived from node_id (fd::/8)
├── node_addr: NodeAddr // SHA-256(pubkey), 32 bytes
└── address: FipsAddress // IPv6 ULA derived from node_addr (fd::/8)
```
`NodeId` is the routing identifier, derived deterministically from `npub`.
`NodeAddr` is the routing identifier, derived deterministically from `npub`.
Transport addresses and FIPS identity are fully decoupled.
### Protocol Layer Visibility
| Observer | Link Addr | Node Addr | FIPS Addr (pubkey) | Payload |
|-------------------------------|---------------|--------------|--------------------| --------|
| Transport (IP router, switch) | Yes | No | No | No |
| FIPS routing node | Last hop only | Yes (header) | No | No |
| Destination endpoint | Yes | Yes | Yes | Yes |
**Key insight**: Three independent encryption layers ensure:
- Passive transport observers see only encrypted blobs
- FIPS routing nodes see node_addrs but not pubkeys (FIPS addresses) or payload
- Only endpoints know each other's FIPS addresses (pubkeys) and can decrypt payload
### Transport
A physical or logical interface over which links can be established. Transports
@@ -130,14 +144,14 @@ An authenticated remote FIPS node, reachable via a link.
```
Peer
├── node_id: NodeId // routing identity
├── node_addr: NodeAddr // routing identity
├── npub: PublicKey // cryptographic identity
├── link_id: LinkId // which link reaches this peer
├── state: PeerState // lifecycle state
│ // Spanning tree
├── declaration: ParentDeclaration // their latest
├── ancestry: Vec<NodeId> // their path to root
├── ancestry: Vec<NodeAddr> // their path to root
│ // Bloom filter (inbound—what's reachable through them)
├── inbound_filter: BloomFilter
@@ -182,12 +196,12 @@ transport per peer).
```
TreeState
├── my_declaration: ParentDeclaration
│ ├── node_id: NodeId
│ ├── parent_id: NodeId // self if root candidate
│ ├── node_addr: NodeAddr
│ ├── parent_id: NodeAddr // self if root candidate
│ ├── sequence: u64 // monotonic
│ └── signature: Signature
├── my_coords: Vec<NodeId> // [self, parent, grandparent, ..., root]
└── root: NodeId // elected root (smallest reachable node_id)
├── my_coords: Vec<NodeAddr> // [self, parent, grandparent, ..., root]
└── root: NodeAddr // elected root (smallest reachable node_addr)
```
### Per-Peer State
@@ -222,8 +236,8 @@ Nodes do NOT know about other subtrees—only paths toward root.
```
BloomState
├── own_node_id: NodeId // always included in outgoing filters
├── leaf_dependents: HashSet<NodeId> // leaf-only nodes we speak for
├── own_node_addr: NodeAddr // always included in outgoing filters
├── leaf_dependents: HashSet<NodeAddr> // leaf-only nodes we speak for
├── is_leaf_only: bool // if true, no filter processing
└── update_debounce: Duration // rate limit outgoing updates
```
@@ -243,7 +257,7 @@ Outgoing filter to peer Q is computed, not stored:
```
outbound_filter(Q) =
own_node_id
own_node_addr
leaf_dependents
{ entries from peer[P].inbound_filter for all P ≠ Q where filter_ttl > 0 }
```
@@ -373,7 +387,7 @@ PeerEvent
├── LinkFailed { reason }
├── Msg1Received { noise_payload }
├── Msg2Received { noise_payload }
├── HandshakeComplete { npub, node_id }
├── HandshakeComplete { npub, node_addr }
├── HandshakeFailed { reason }
├── TreeAnnounceReceived { declaration, ancestry }
├── FilterAnnounceReceived { filter, sequence, ttl }
@@ -582,7 +596,7 @@ A leaf-only node uses a minimal subset of the full architecture:
```
LeafOnlyNode
├── identity: Identity // full (npub, nsec, node_id, address)
├── identity: Identity // full (npub, nsec, node_addr, address)
├── config: Config // simplified
├── tun: TunInterface // full (provides IPv6 to local apps)
├── transport: Transport // one
@@ -622,7 +636,7 @@ The upstream peer entry for a leaf-only node:
```
UpstreamPeer (leaf-only)
├── node_id: NodeId
├── node_addr: NodeAddr
├── npub: PublicKey
├── link_id: LinkId
├── state: PeerState // auth lifecycle only
@@ -739,7 +753,7 @@ The startup sequence initializes components in dependency order:
2. Initialize identity
├── Load nsec from config (or generate if absent)
├── Derive npub, node_id, and FIPS address
├── Derive npub, node_addr, and FIPS address
└── Log identity information
3. Initialize transports

View File

@@ -38,13 +38,13 @@ coordinates and distances.
TreeAnnounce {
sequence: u64, // Monotonic, increments on parent change
timestamp: u64, // Unix timestamp (seconds)
parent: NodeId, // 32 bytes, SHA-256(npub) of selected parent
parent: NodeAddr, // 32 bytes, SHA-256(pubkey) of selected parent
ancestry: Vec<AncestryEntry>, // Path from self to root
signature: Signature, // 64 bytes, signs (sequence || timestamp || parent || ancestry)
}
AncestryEntry {
node_id: NodeId, // 32 bytes
node_addr: NodeAddr, // 32 bytes
sequence: u64, // That node's sequence number
timestamp: u64, // That node's timestamp
signature: Signature, // That node's signature over its declaration
@@ -59,7 +59,7 @@ Higher sequence numbers supersede lower ones for conflict resolution.
**timestamp**: Used for distributed consistency. A declaration is considered
stale if `now - timestamp > ROOT_TIMEOUT` (default 60 minutes for root).
**parent**: The node_id of the selected parent. If `parent == self.node_id`,
**parent**: The node_addr of the selected parent. If `parent == self.node_addr`,
the node is declaring itself as root.
**ancestry**: The chain from this node up to the root. The first entry is this
@@ -182,7 +182,7 @@ forward-compatible extension to larger filters.
```text
for i in 0..hash_count:
bit_index = hash(node_id, i) % (8 * bits.len())
bit_index = hash(node_addr, i) % (8 * bits.len())
if !bits[bit_index]: return false
return true // "maybe present"
```
@@ -191,7 +191,7 @@ return true // "maybe present"
A node's outgoing filter to peer Q contains:
1. This node's own node_id
1. This node's own node_addr
2. Node_ids of leaf-only dependents (nodes using this node as sole peer)
3. Entries from filters received from other peers (not Q) with TTL > 0
@@ -248,9 +248,9 @@ local Bloom filters.
```text
LookupRequest {
request_id: u64, // Unique identifier for this request
target: NodeId, // 32 bytes, who we're looking for
origin: NodeId, // 32 bytes, who's asking
origin_coords: Vec<NodeId>, // Origin's ancestry (for return path)
target: NodeAddr, // 32 bytes, who we're looking for
origin: NodeAddr, // 32 bytes, who's asking
origin_coords: Vec<NodeAddr>, // Origin's ancestry (for return path)
ttl: u8, // Remaining propagation hops
visited: CompactBloomFilter,// ~256 bytes, prevents loops
}
@@ -266,9 +266,9 @@ CompactBloomFilter {
**request_id**: Randomly generated, used to match responses and detect
duplicates.
**target**: The node_id being searched for.
**target**: The node_addr being searched for.
**origin**: The node_id of the original requester. Used for response routing.
**origin**: The node_addr of the original requester. Used for response routing.
**origin_coords**: The requester's current tree coordinates. Enables greedy
routing of the response back to origin.
@@ -288,7 +288,7 @@ When receiving LookupRequest:
3. Decrement TTL
4. Check if target is local:
- If target == self.node_id: generate LookupResponse
- If target == self.node_addr: generate LookupResponse
- If target in local peer_filters: may respond on behalf (optional)
5. If TTL > 0 and not found locally:
@@ -313,8 +313,8 @@ LookupResponse returns the target's coordinates to the requester.
```text
LookupResponse {
request_id: u64, // Echoes LookupRequest.request_id
target: NodeId, // 32 bytes, confirms who was found
target_coords: Vec<NodeId>, // Target's ancestry (the key payload)
target: NodeAddr, // 32 bytes, confirms who was found
target_coords: Vec<NodeAddr>, // Target's ancestry (the key payload)
proof: Signature, // 64 bytes, target signs to prove existence
}
```
@@ -384,7 +384,7 @@ handshake completion.
## 8. Encoding
All multi-byte integers are little-endian. NodeId is 32 bytes (SHA-256 hash).
All multi-byte integers are little-endian. NodeAddr is 32 bytes (SHA-256 hash).
Signatures are 64 bytes (secp256k1 Schnorr).
Variable-length fields (ancestry, coordinates) are prefixed with a 2-byte
@@ -431,7 +431,7 @@ Propagates spanning tree state between directly connected peers.
│ │ 0 │ msg_type │ 1 byte │ 0x10 │ │
│ │ 1 │ sequence │ 8 bytes │ u64 LE, monotonic counter │ │
│ │ 9 │ timestamp │ 8 bytes │ u64 LE, Unix seconds │ │
│ │ 17 │ parent │ 32 bytes │ NodeId of selected parent │ │
│ │ 17 │ parent │ 32 bytes │ NodeAddr of selected parent │ │
│ │ 49 │ ancestry_count │ 2 bytes │ u16 LE, number of entries │ │
│ │ 51 │ ancestry[0..n] │ 112 × n │ AncestryEntry array │ │
│ │ ... │ signature │ 64 bytes │ Schnorr sig over all above │ │
@@ -442,7 +442,7 @@ Propagates spanning tree state between directly connected peers.
│ ├────────┬──────────────────┬───────────┬───────────────────────────────┤ │
│ │ Offset │ Field │ Size │ Description │ │
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ node_id │ 32 bytes │ SHA-256(npub) of this node │ │
│ │ 0 │ node_addr │ 32 bytes │ SHA-256(pubkey) of this node │ │
│ │ 32 │ sequence │ 8 bytes │ u64 LE, node's seq number │ │
│ │ 40 │ timestamp │ 8 bytes │ u64 LE, node's timestamp │ │
│ │ 48 │ signature │ 64 bytes │ Node's sig over its decl │ │
@@ -467,29 +467,29 @@ PLAINTEXT BYTES (hex layout):
10 ← msg_type = TreeAnnounce
05 00 00 00 00 00 00 00 ← sequence = 5
C3 B2 A1 67 00 00 00 00 ← timestamp (Unix seconds)
[32 bytes P1's node_id] ← parent
[32 bytes P1's node_addr] ← parent
04 00 ← ancestry_count = 4
ANCESTRY[0] - Self (D):
[32 bytes D's node_id]
[32 bytes D's node_addr]
05 00 00 00 00 00 00 00 ← D's sequence
C3 B2 A1 67 00 00 00 00 ← D's timestamp
[64 bytes D's signature]
ANCESTRY[1] - Parent (P1):
[32 bytes P1's node_id]
[32 bytes P1's node_addr]
0A 00 00 00 00 00 00 00 ← P1's sequence
00 B0 A1 67 00 00 00 00 ← P1's timestamp
[64 bytes P1's signature]
ANCESTRY[2] - Grandparent (P2):
[32 bytes P2's node_id]
[32 bytes P2's node_addr]
03 00 00 00 00 00 00 00 ← P2's sequence
00 A0 A1 67 00 00 00 00 ← P2's timestamp
[64 bytes P2's signature]
ANCESTRY[3] - Root:
[32 bytes Root's node_id]
[32 bytes Root's node_addr]
01 00 00 00 00 00 00 00 ← Root's sequence
00 90 A1 67 00 00 00 00 ← Root's timestamp
[64 bytes Root's signature]
@@ -540,10 +540,10 @@ Propagates Bloom filter reachability information.
│ │ bits 0-7 │ bits 8-15 │ │ bits 8184-8191 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ To test membership of node_id: │
│ To test membership of node_addr: │
│ filter_bits = 8 * (512 << size_class) // 8192 for v1 │
│ for i in 0..hash_count: │
│ bit_index = hash(node_id, i) % filter_bits │
│ bit_index = hash(node_addr, i) % filter_bits │
│ if !bits[bit_index]: return false │
│ return true // "maybe present" │
│ │
@@ -585,8 +585,8 @@ Discovers tree coordinates for distant destinations.
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x12 │ │
│ │ 1 │ request_id │ 8 bytes │ u64 LE, unique identifier │ │
│ │ 9 │ target │ 32 bytes │ NodeId being searched for │ │
│ │ 41 │ origin │ 32 bytes │ NodeId of requester │ │
│ │ 9 │ target │ 32 bytes │ NodeAddr being searched for │ │
│ │ 41 │ origin │ 32 bytes │ NodeAddr of requester │ │
│ │ 73 │ ttl │ 1 byte │ Remaining propagation hops │ │
│ │ 74 │ origin_coords_cnt│ 2 bytes │ u16 LE │ │
│ │ 76 │ origin_coords │ 32 × n │ Requester's ancestry │ │
@@ -611,8 +611,8 @@ Discovers tree coordinates for distant destinations.
PLAINTEXT BYTES:
12 ← msg_type = LookupRequest
[8 bytes request_id] ← random unique ID
[32 bytes target node_id] ← who we're looking for
[32 bytes origin node_id] ← who's asking
[32 bytes target node_addr] ← who we're looking for
[32 bytes origin node_addr] ← who's asking
08 ← ttl = 8
04 00 ← origin_coords_count = 4
[32 bytes] × 4 ← origin's ancestry (128 bytes)
@@ -638,7 +638,7 @@ Returns target's coordinates to the requester.
│ ├────────┼──────────────────┼───────────┼───────────────────────────────┤ │
│ │ 0 │ msg_type │ 1 byte │ 0x13 │ │
│ │ 1 │ request_id │ 8 bytes │ u64 LE, echoes request │ │
│ │ 9 │ target │ 32 bytes │ NodeId that was found │ │
│ │ 9 │ target │ 32 bytes │ NodeAddr that was found │ │
│ │ 41 │ target_coords_cnt│ 2 bytes │ u16 LE │ │
│ │ 43 │ target_coords │ 32 × n │ Target's ancestry to root │ │
│ │ ... │ proof │ 64 bytes │ Target's signature │ │
@@ -664,7 +664,7 @@ Returns target's coordinates to the requester.
PLAINTEXT BYTES:
13 ← msg_type = LookupResponse
[8 bytes request_id] ← echoed from request
[32 bytes target node_id] ← confirms who was found
[32 bytes target node_addr] ← confirms who was found
05 00 ← target_coords_count = 5
[32 bytes] × 5 ← target's ancestry (160 bytes)
[64 bytes proof signature] ← target signs to prove existence

View File

@@ -116,37 +116,47 @@ approach, enabling O(1) packet routing without relying on source addresses.
## Identity System
FIPS uses Nostr keypairs (secp256k1) as node identities. The public key (npub)
identifies the node; the private key (nsec) signs protocol messages and
establishes encrypted sessions.
FIPS uses Nostr keypairs (secp256k1) as node identities. The public key
identifies the node; the private key signs protocol messages and establishes
encrypted sessions.
### Node ID Derivation
The FIPS address (synonymous with the pubkey) is the primary means for
application-layer software to identify communication endpoints. The
bech32-encoded npub can be used interchangeably for user interface purposes.
The FIPS datagram service is exposed to the application layer either via a
native API to the FIPS node software, or through an IPv6 shim driver that
converts the node identity into an IPv6 address and provides DNS resolution
from npub to this address for traditional software.
The npub is hashed to derive a node_id used for routing:
### Node Address Derivation
The pubkey is hashed to derive a node_addr used for routing:
```
npub (secp256k1 x-only pubkey, 32 bytes)
pubkey (secp256k1 x-only, 32 bytes)
→ SHA-256
→ node_id (32 bytes)
→ node_addr (32 bytes)
→ truncate with prefix
FIPS address (128 bits, fd::/8)
→ IPv6 address (128 bits, fd::/8)
```
**Why hash the npub?** The node_id is the only identifier used at the protocol
level; the associated npub cannot be derived from it. This one-way derivation
also prevents "grinding" attacks—secp256k1 public keys can be shifted to achieve
specific prefixes via modular addition, but targeting a node_id prefix requires
full brute force against SHA-256.
**Separation of concerns**: The keypair handles cryptographic operations (signing,
encryption). The node_addr derived from the pubkey handles routing. This keeps
cryptographic material out of routing tables and packet headers—the node_addr is
the only identifier used at the protocol level, and the pubkey cannot be derived
from it.
**Separation of concerns**: The nsec/npub keypair handles cryptographic
operations (signing, encryption). The node_id derived from the npub handles
routing. This keeps cryptographic material out of routing tables and packet
headers.
The one-way hash also provides privacy from intermediate routing nodes. Routers
see only node_addrs in packet headers—they can route traffic without learning
the Nostr identities of the endpoints. An observer can verify "does this
node_addr belong to pubkey X?" but cannot enumerate which pubkeys are
communicating by inspecting traffic. Only the endpoints, which complete the
Noise IK handshake, learn each other's pubkeys.
### Address Format
When using the IPv6 protocol adapter, FIPS addresses use the IPv6 Unique Local
Address (ULA) prefix `fd00::/8`, providing 120 bits from the node_id hash.
Address (ULA) prefix `fd00::/8`, providing 120 bits from the node_addr hash.
These are overlay identifiers—they appear in the TUN interface for application
compatibility but are not routable on the underlying transport. The fd prefix
ensures no collision with addresses that may be in use on the transport network.
@@ -163,6 +173,23 @@ See [fips-wire-protocol.md](fips-wire-protocol.md) for the Noise IK handshake
and [fips-session-protocol.md](fips-session-protocol.md) for end-to-end
session establishment.
### Terminology: Addresses and Identifiers
FIPS uses several related but distinct identifiers at different protocol layers:
| Term | Layer | Visible To | Description |
|----------------------------|---------------------|----------------|-------------------------------------------------------|
| **FIPS address / pubkey** | Application/Session | Endpoints only | 32-byte secp256k1 public key - the endpoint identity |
| **npub** | (encoding) | Human readers | Bech32 encoding of pubkey for display/config |
| **node_addr** | Routing | Routing nodes | SHA-256(pubkey) - cannot be reversed to pubkey |
| **link_addr** | Transport | Direct peers | IP:port, MAC, .onion - transport-specific |
| **IPv6 address** | IPv6 shim | Applications | fd::/8 derived from node_addr - optional compatibility|
**Privacy property**: The pubkey (FIPS address / Nostr identity) is never exposed to
intermediate routing nodes. They see only the node_addr, a one-way hash. An observer
can verify "does this node_addr belong to pubkey X?" but cannot derive the pubkey from
traffic.
---
## Two-Layer Encryption
@@ -238,7 +265,7 @@ minimizes distance to the destination.
### Root Election
The root is the node with the lexicographically smallest node_id among all
The root is the node with the lexicographically smallest node_addr among all
reachable nodes. This election is deterministic and requires no coordination—
each node independently examines its view of the network and reaches the same
conclusion.
@@ -270,7 +297,7 @@ unaffected by local path changes and don't receive updates for them.
### Partition Handling
If the network partitions, each isolated segment elects its own root (the
smallest node_id within that segment). When partitions merge, nodes in the
smallest node_addr within that segment). When partitions merge, nodes in the
segment with the larger root discover the globally smaller root and re-parent.
The tree reconverges automatically.
@@ -293,14 +320,14 @@ through a given peer, with occasional false positives handled by backtracking.
### How It Works
Each node maintains bloom filters summarizing which node_ids are reachable
Each node maintains bloom filters summarizing which node_addrs are reachable
through each of its peers. These filters propagate through the tree: a node
aggregates filters from its children and announces the combined filter to its
parent (and vice versa).
When a node needs to reach an unknown destination:
1. Check local bloom filters—which peers might be able to reach this node_id?
1. Check local bloom filters—which peers might be able to reach this node_addr?
2. Send a LookupRequest to peers whose filters indicate "maybe"
3. The request propagates through the tree toward matching subtrees
4. The destination responds with a LookupResponse containing its coordinates
@@ -317,7 +344,7 @@ Filters propagate in the opposite direction from tree announcements:
- Tree state propagates upward (toward root) via ancestry chains
- Bloom filters propagate downward (toward leaves) via subtree aggregation
A node's filter contains all node_ids reachable through its subtree. The root's
A node's filter contains all node_addrs reachable through its subtree. The root's
filter contains everyone; leaf nodes have empty outbound filters.
See [fips-routing.md](fips-routing.md) for bloom filter design and
@@ -490,11 +517,11 @@ Each entity in the network sees different information:
|--------|---------|
| Transport observer | Encrypted packets, timing, packet sizes |
| Direct peer | Your npub (identity), traffic volume, timing |
| Intermediate router | Source and destination node_ids, packet size |
| Intermediate router | Source and destination node_addrs, packet size |
| Destination | Your npub (identity), payload content |
Intermediate routers see node_ids, not npubs. Since node_ids are derived from
npubs via one-way SHA-256 hash, routers cannot determine the actual identities
Intermediate routers see node_addrs, not npubs. Since node_addrs are derived from
pubkeys via one-way SHA-256 hash, routers cannot determine the actual identities
of the endpoints they route for.
The session layer hides payload content from intermediate routers. The link
@@ -507,7 +534,7 @@ layer hides everything from passive observers on the underlying transport.
FIPS combines these elements into a cohesive system that achieves its design
goals:
- **Self-sovereign identity** through Nostr keypairs, with node_ids providing
- **Self-sovereign identity** through Nostr keypairs, with node_addrs providing
routing-level privacy
- **Transport agnosticism** via the transport abstraction layer, enabling the
same routing logic across UDP, Ethernet/WiFi, Tor, and other link types

View File

@@ -177,7 +177,7 @@ node's npub, truncated or used directly as the filter key.
Each node maintains a Bloom filter for each peer direction:
```rust
peer_filters: HashMap<NodeId, BloomFilter>
peer_filters: HashMap<NodeAddr, BloomFilter>
```
The filter for peer P answers: "Which destinations are reachable through P?"
@@ -287,11 +287,11 @@ Discovered coordinates are cached:
```rust
struct RouteCache {
entries: HashMap<NodeId, CachedCoords>,
entries: HashMap<NodeAddr, CachedCoords>,
}
struct CachedCoords {
coords: Vec<NodeId>,
coords: Vec<NodeAddr>,
discovered_at: Timestamp,
last_used: Timestamp,
}
@@ -320,7 +320,7 @@ Example: Node D at depth 4 has coordinates `[D, P1, P2, P3, Root]`.
Distance between two nodes is hops through their lowest common ancestor (LCA):
```rust
fn tree_distance(a_coords: &[NodeId], b_coords: &[NodeId]) -> usize {
fn tree_distance(a_coords: &[NodeAddr], b_coords: &[NodeAddr]) -> usize {
let lca_depth = longest_common_suffix_length(a_coords, b_coords);
let a_to_lca = a_coords.len() - lca_depth;
let b_to_lca = b_coords.len() - lca_depth;
@@ -333,16 +333,16 @@ Note: Coordinates are ordered self-to-root, so common ancestry is a suffix.
### Greedy Routing Algorithm
```rust
fn greedy_next_hop(&self, dest_coords: &[NodeId]) -> NodeId {
fn greedy_next_hop(&self, dest_coords: &[NodeAddr]) -> NodeAddr {
// Check if we are the destination
if dest_coords[0] == self.node_id {
if dest_coords[0] == self.node_addr {
return LOCAL_DELIVERY;
}
// Check if destination is a direct peer
for peer in &self.peers {
if peer.node_id == dest_coords[0] {
return peer.node_id;
if peer.node_addr == dest_coords[0] {
return peer.node_addr;
}
}
@@ -350,7 +350,7 @@ fn greedy_next_hop(&self, dest_coords: &[NodeId]) -> NodeId {
self.peers
.iter()
.min_by_key(|p| tree_distance(&p.coords, dest_coords))
.map(|p| p.node_id)
.map(|p| p.node_addr)
.expect("no peers")
}
```
@@ -389,7 +389,7 @@ relying on application-layer timeouts to detect failures, FIPS provides explicit
feedback that allows rapid route recovery. The tradeoff favors responsiveness
over metadata privacy.
**Partial mitigation**: FIPS addresses are derived from `SHA-256(npub)`, not the
**Partial mitigation**: FIPS addresses are derived from `SHA-256(pubkey)`, not the
npub itself. An observer learns that `fd12:3456:...` is communicating with
`fd78:9abc:...`, but cannot directly determine the Nostr identities without
additional information (e.g., DNS lookup correlation, prior knowledge of the
@@ -493,7 +493,7 @@ impl Router {
// Cache miss — request coordinates
self.send_error(from, CoordsRequired {
dest_addr: packet.dest_addr,
reporter: self.node_id,
reporter: self.node_addr,
});
}
}
@@ -510,7 +510,7 @@ struct CoordCache {
}
struct CacheEntry {
coords: Vec<NodeId>,
coords: Vec<NodeAddr>,
created: Timestamp,
last_used: Timestamp,
expires: Timestamp,

View File

@@ -5,13 +5,31 @@
This document captures design considerations for FIPS protocol message flow,
including peer discovery, authentication, tree announcements, and data routing.
### Access to the FIPS Datagram Service
Applications can access FIPS datagram delivery through two interfaces:
- **Native FIPS API**: Applications address destinations directly by npub or
public key. The FIPS stack resolves the destination's node_addr and routes
without any DNS involvement. This is the preferred interface for
FIPS-aware applications.
- **IPv6 adapter (TUN interface)**: Unmodified IPv6 applications use a TUN
device with `fd::/8` routing. Because IPv6 addresses are one-way hashes of
public keys, a local DNS service maps npub → IPv6 address and primes the
identity cache so the TUN can route arriving packets.
The remainder of this document details the sequence of actions initiated through
the IPv6 adapter path. The native FIPS API bypasses sections 1.11.4 (DNS
entry point, identity cache) entirely, entering the flow at route discovery.
---
## 1. Application-Initiated Traffic Flow
## 1. Application-Initiated Traffic Flow (IPv6 Adapter)
> **Note**: This section applies to traditional IP-based applications using the
> TUN interface. Applications using the native FIPS datagram service address
> destinations directly by npub, and routing proceeds from there without DNS.
> TUN interface. Applications using the native FIPS API address destinations
> directly by npub or public key; routing proceeds from there without DNS.
Traffic flow begins at the application layer with a DNS query, which triggers
a cascade of events through the FIPS stack.
@@ -27,7 +45,7 @@ an npub. The flow:
2. **FIPS DNS service** performs two functions:
- **Address derivation**: Converts the npub to an identity and derives the
corresponding `fd::/8` IPv6 address
- **Cache priming**: Stores the identity mapping (IPv6 address ↔ npub ↔ node_id)
- **Cache priming**: Stores the identity mapping (IPv6 address ↔ npub ↔ node_addr)
in the local FIPS routing cache
3. **DNS Response**: Returns the derived IPv6 address to the application
@@ -65,7 +83,7 @@ for address derivation.
### 1.4 Identity Cache Lifetime
The identity cache (IPv6 address ↔ npub ↔ node_id) has the following lifetime
The identity cache (IPv6 address ↔ npub ↔ node_addr) has the following lifetime
semantics:
- **Configurable timeout**: Cache entries expire after a configured duration
@@ -91,7 +109,7 @@ Cache entry expires
A packet may arrive at the TUN for an `fd::/8` destination without a prior
DNS lookup (cached address, manual configuration, etc.). Since address
derivation is one-way (SHA-256), the npub cannot be recovered from the address,
and without the npub we cannot determine the node_id needed for routing.
and without the npub we cannot determine the node_addr needed for routing.
FIPS returns ICMPv6 Destination Unreachable (Code 0: No route to destination)
for packets to unknown addresses. The identity cache must be populated before
@@ -139,13 +157,13 @@ On receiving a packet, the TUN reader:
4. **Retrieve routing identity**: Cache hit provides:
- `npub`: The Nostr public key of the destination
- `node_id`: SHA-256(npub), used for spanning tree routing
- `node_addr`: SHA-256(pubkey), used for spanning tree routing
5. **Session lookup**: Check for existing FIPS session with destination npub
- **Hit**: Use existing session for encryption/signing
- **Miss**: Initiate session establishment (see §3)
6. **Route determination**: Using node_id, determine the next hop peer:
6. **Route determination**: Using node_addr, determine the next hop peer:
- Check route cache for destination's spanning tree coordinates
- If cache miss, initiate route discovery (see §4.4)
- Select next hop via greedy routing toward destination coordinates
@@ -166,7 +184,7 @@ between two FIPS nodes.
Each session contains:
- **Peer identity**: The remote node's npub and node_id
- **Peer identity**: The remote node's npub and node_addr
- **Symmetric session keys**: Directional keys for encryption (send_key, recv_key)
- **Nonce counters**: Per-direction counters for replay protection
@@ -185,7 +203,7 @@ When the TUN reader has a packet for a destination with no existing session:
```text
TUN reader
├─► Identity cache lookup → node_id
├─► Identity cache lookup → node_addr
├─► Session lookup (by npub) → MISS
@@ -243,7 +261,7 @@ FIPS routing combines three mechanisms:
2. **Discovery protocol**: Query-based lookup for distant destinations
3. **Greedy tree routing**: Coordinate-based forwarding using spanning tree position
The routing layer maintains a route cache mapping `node_id → (coordinates,
The routing layer maintains a route cache mapping `node_addr → (coordinates,
next_hop_peer)`. Cache hits enable immediate greedy routing; cache misses
trigger route discovery via bloom filter queries or LookupRequest flooding.
@@ -400,8 +418,8 @@ The Noise handshake messages embed in SessionSetup/SessionAck:
```text
SessionSetup {
// Routing portion (processed by routers)
src_coords: Vec<NodeId>,
dest_coords: Vec<NodeId>,
src_coords: Vec<NodeAddr>,
dest_coords: Vec<NodeAddr>,
src_addr: Ipv6Addr,
dest_addr: Ipv6Addr,
@@ -411,7 +429,7 @@ SessionSetup {
SessionAck {
// Routing portion
src_coords: Vec<NodeId>, // Responder's coordinates
src_coords: Vec<NodeAddr>, // Responder's coordinates
// Crypto portion
handshake_payload: Vec<u8>, // Noise IK message 2
@@ -514,6 +532,12 @@ the inner payload is encrypted end-to-end with session keys.
| 0x20 | CoordsRequired | R → S | Router cache miss |
| 0x21 | PathBroken | R → S | Greedy routing failed |
> **Address terminology**: The `src_addr` and `dest_addr` fields in session packet
> headers are node_addrs (32-byte SHA-256 hashes of pubkeys). These are visible to
> intermediate routers for routing decisions. The actual FIPS addresses (pubkeys/npubs)
> are exchanged only during the Noise IK handshake and never appear in packet
> headers—routers cannot determine endpoint identities from the node_addrs they see.
### 8.2 SessionSetup (0x00)
Establishes a crypto session and warms router coordinate caches along the path.
@@ -527,12 +551,12 @@ Establishes a crypto session and warms router coordinate caches along the path.
│ 0 │ msg_type │ 1 byte │ 0x00 │
│ 1 │ flags │ 1 byte │ Bit 0: REQUEST_ACK │
│ │ │ │ Bit 1: BIDIRECTIONAL │
│ 2 │ src_addr │ 16 bytes │ Source fd::/8 address
18 │ dest_addr │ 16 bytes │ Destination fd::/8 address
34 │ src_coords_count │ 2 bytes │ u16 LE, number of src coord entries │
36 │ src_coords │ 32 × n │ NodeId array (self → root) │
│ 2 │ src_addr │ 32 bytes │ Source node_addr
34 │ dest_addr │ 32 bytes │ Destination node_addr
66 │ src_coords_count │ 2 bytes │ u16 LE, number of src coord entries │
│ 68 │ src_coords │ 32 × n │ NodeAddr array (self → root) │
│ ... │ dest_coords_count│ 2 bytes │ u16 LE, number of dest coord entries│
│ ... │ dest_coords │ 32 × m │ NodeId array (dest → root) │
│ ... │ dest_coords │ 32 × m │ NodeAddr array (dest → root) │
│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │
│ ... │ handshake_payload│ variable │ Noise IK msg1 (82 bytes typical) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
@@ -543,13 +567,13 @@ Establishes a crypto session and warms router coordinate caches along the path.
```text
┌──────┬───────┬──────────────────┬──────────────────┬───────┬─────────────┐
│ 0x00 │ 0x01 │ src_addr │ dest_addr │ 0x03 │ src_coords │
│ type │ flags │ 16 bytes │ 16 bytes │ count │ 3 × 32 bytes│
│ type │ flags │ 32 bytes │ 32 bytes │ count │ 3 × 32 bytes│
├──────┴───────┴──────────────────┴──────────────────┴───────┴─────────────┤
│ 0x04 │ dest_coords │ 0x52 │ handshake_payload │
│ count │ 4 × 32 bytes │ len=82│ 82 bytes (Noise IK msg1) │
└───────┴───────────────┴───────┴──────────────────────────────────────────┘
Total: 1 + 1 + 16 + 16 + 2 + 96 + 2 + 128 + 2 + 82 = 346 bytes
Total: 1 + 1 + 32 + 32 + 2 + 96 + 2 + 128 + 2 + 82 = 378 bytes
```
### 8.3 SessionAck (0x01)
@@ -564,10 +588,10 @@ Confirms session establishment and completes the Noise handshake.
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x01 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ src_addr │ 16 bytes │ Acknowledger's address
18 │ dest_addr │ 16 bytes │ Original sender's address
34 │ src_coords_count │ 2 bytes │ u16 LE │
36 │ src_coords │ 32 × n │ Acknowledger's coords (for caching) │
│ 2 │ src_addr │ 32 bytes │ Acknowledger's node_addr
34 │ dest_addr │ 32 bytes │ Original sender's node_addr
66 │ src_coords_count │ 2 bytes │ u16 LE │
│ 68 │ src_coords │ 32 × n │ Acknowledger's coords (for caching) │
│ ... │ handshake_len │ 2 bytes │ u16 LE, Noise payload length │
│ ... │ handshake_payload│ variable │ Noise IK msg2 (33 bytes typical) │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
@@ -588,12 +612,12 @@ Carries encrypted application data (typically IPv6 payloads).
│ 2 │ hop_limit │ 1 byte │ Decremented each hop │
│ 3 │ reserved │ 1 byte │ Alignment padding │
│ 4 │ payload_length │ 2 bytes │ u16 LE │
│ 6 │ src_addr │ 16 bytes │ Source fd::/8 address
22 │ dest_addr │ 16 bytes │ Destination fd::/8 address
38 │ payload │ variable │ Encrypted application data │
│ 6 │ src_addr │ 32 bytes │ Source node_addr
38 │ dest_addr │ 32 bytes │ Destination node_addr
70 │ payload │ variable │ Encrypted application data │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Minimal header: 38 bytes (comparable to IPv6's 40 bytes)
Minimal header: 70 bytes
```
When `COORDS_PRESENT` flag is set (route warming after CoordsRequired):
@@ -609,16 +633,16 @@ When `COORDS_PRESENT` flag is set (route warming after CoordsRequired):
│ 2 │ hop_limit │ 1 byte │ Decremented each hop │
│ 3 │ reserved │ 1 byte │ Alignment padding │
│ 4 │ payload_length │ 2 bytes │ u16 LE │
│ 6 │ src_addr │ 16 bytes │ Source fd::/8 address
22 │ dest_addr │ 16 bytes │ Destination fd::/8 address
38 │ src_coords_count │ 2 bytes │ u16 LE │
40 │ src_coords │ 32 × n │ Source coordinates │
│ 6 │ src_addr │ 32 bytes │ Source node_addr
38 │ dest_addr │ 32 bytes │ Destination node_addr
70 │ src_coords_count │ 2 bytes │ u16 LE │
72 │ src_coords │ 32 × n │ Source coordinates │
│ ... │ dest_coords_count│ 2 bytes │ u16 LE │
│ ... │ dest_coords │ 32 × m │ Destination coordinates │
│ ... │ payload │ variable │ Encrypted application data │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
With depth-4 coords both directions: 38 + 2 + 128 + 2 + 128 = 298 bytes header
With depth-4 coords both directions: 70 + 2 + 128 + 2 + 128 = 330 bytes header
```
### 8.5 CoordsRequired (0x20)
@@ -634,11 +658,11 @@ coordinate cache miss.
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x20 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ dest_addr │ 16 bytes │ The address we couldn't route
18 │ reporter │ 32 bytes │ NodeId of reporting router
│ 2 │ dest_addr │ 32 bytes │ The node_addr we couldn't route │
34 │ reporter │ 32 bytes │ NodeAddr of reporting router │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
Total: 50 bytes
Total: 66 bytes
```
### 8.6 PathBroken (0x21)
@@ -653,10 +677,10 @@ Sent when greedy routing fails (no peer is closer to destination).
├────────┼──────────────────┼───────────┼─────────────────────────────────────┤
│ 0 │ msg_type │ 1 byte │ 0x21 │
│ 1 │ flags │ 1 byte │ Reserved │
│ 2 │ dest_addr │ 16 bytes │ The unreachable destination
18 │ reporter │ 32 bytes │ NodeId of reporting router
50 │ last_coords_count│ 2 bytes │ u16 LE │
52 │ last_known_coords│ 32 × n │ Stale coords that failed │
│ 2 │ dest_addr │ 32 bytes │ The unreachable node_addr
34 │ reporter │ 32 bytes │ NodeAddr of reporting router │
66 │ last_coords_count│ 2 bytes │ u16 LE │
68 │ last_known_coords│ 32 × n │ Stale coords that failed │
└────────┴──────────────────┴───────────┴─────────────────────────────────────┘
```
@@ -711,12 +735,12 @@ Router R cannot see: payload contents (encrypted with S↔D keys)
### 8.8 Encoding Rules
- All multi-byte integers are **little-endian**
- NodeId is 32 bytes (SHA-256 hash of npub)
- NodeAddr is 32 bytes (SHA-256 hash of npub)
- IPv6 addresses are 16 bytes (network byte order)
- Variable-length coordinate arrays use 2-byte u16 count prefix
```text
Vec<NodeId> encoding:
Vec<NodeAddr> encoding:
count: u16 (little-endian)
items: [u8; 32] × count
```

View File

@@ -331,11 +331,11 @@ When using `PeerSlot` enum, need reverse lookups for packet dispatch:
```rust
struct Node {
// Primary storage
peers: HashMap<NodeId, PeerSlot>,
peers: HashMap<NodeAddr, PeerSlot>,
// Reverse lookup: (transport, remote_addr) → NodeId
// Needed because ReceivedPacket has addr, not NodeId
addr_to_peer: HashMap<(TransportId, TransportAddr), NodeId>,
// Reverse lookup: (transport, remote_addr) → NodeAddr
// Needed because ReceivedPacket has addr, not NodeAddr
addr_to_peer: HashMap<(TransportId, TransportAddr), NodeAddr>,
}
```
@@ -344,7 +344,7 @@ For inbound connections from unknown addresses:
1. Receive Noise IK msg1 → decrypt to extract sender's static key (identity)
2. Create new PeerConnection with discovered identity
3. Add to `connections` (by LinkId) and `addr_to_link`
4. After handshake completes, promote to ActivePeer (indexed by NodeId)
4. After handshake completes, promote to ActivePeer (indexed by NodeAddr)
## Summary

View File

@@ -38,6 +38,11 @@ authoritative. Only successful cryptographic verification establishes authentici
When a valid packet arrives from a different address than expected, the peer's
address is updated rather than the packet being rejected.
> **Terminology note**: "Source address" in this document refers to the transport-layer
> address (link_addr)—e.g., IP:port for UDP, MAC for Ethernet, .onion for Tor. This is
> distinct from node_addr (the routing identifier) and FIPS address/pubkey (the endpoint
> identity). See [fips-intro.md](fips-intro.md) §Identity System for the full terminology.
---
## 2. Wire Format
@@ -170,11 +175,11 @@ Packet dispatch follows a two-phase approach:
```
Node:
// === Authenticated sessions ===
// Primary dispatch: our_index → NodeId
peers_by_index: HashMap<(TransportId, u32), NodeId>
// Primary dispatch: our_index → NodeAddr
peers_by_index: HashMap<(TransportId, u32), NodeAddr>
// Peer data by identity
peers: HashMap<NodeId, ActivePeer>
peers: HashMap<NodeAddr, ActivePeer>
// === Pending handshakes ===
// Outbound: our sender_idx → connection state
@@ -201,11 +206,11 @@ receive_encrypted(transport_id, source_addr, data):
ciphertext = data[13..]
// O(1) session lookup by index
node_id = peers_by_index.get((transport_id, receiver_idx))
if node_id is None:
node_addr = peers_by_index.get((transport_id, receiver_idx))
if node_addr is None:
drop("unknown index") // No crypto, minimal CPU cost
peer = peers.get(node_id)
peer = peers.get(node_addr)
// Replay check BEFORE decryption (cheap)
if not peer.replay_window.check(counter):
@@ -228,7 +233,7 @@ receive_encrypted(transport_id, source_addr, data):
peer.stats.record_recv(data.len())
// Dispatch to message handler
dispatch_link_message(node_id, plaintext)
dispatch_link_message(node_addr, plaintext)
```
**Key properties**:
@@ -316,7 +321,7 @@ receive_msg1(transport_id, source_addr, data):
// === IDENTITY CHECKS ===
// Check if this is a known peer reconnecting
if peers.contains(peer_identity.node_id):
if peers.contains(peer_identity.node_addr):
// Existing peer from new address - handle reconnection
handle_peer_reconnection(peer_identity, source_addr, ...)
return
@@ -612,14 +617,14 @@ release(transport_id, idx):
When a session rekeys, new indices are allocated:
```
rekey(node_id):
peer = peers.get(node_id)
rekey(node_addr):
peer = peers.get(node_addr)
old_index = peer.our_index
new_index = index_allocator.allocate(peer.transport_id)
// Update index mapping
peers_by_index.remove((peer.transport_id, old_index))
peers_by_index.insert((peer.transport_id, new_index), node_id)
peers_by_index.insert((peer.transport_id, new_index), node_addr)
// Release old index
index_allocator.release(peer.transport_id, old_index)

View File

@@ -64,7 +64,7 @@ maintains roughly 50 TreeState entries, not 1000.
### Root Election
The root is deterministic: the node with the lexicographically smallest node_id
The root is deterministic: the node with the lexicographically smallest node_addr
among all reachable nodes. No explicit election protocol exists—each node
independently derives the same answer from its local TreeState.
@@ -86,7 +86,7 @@ When a node starts with no peers, it bootstraps as a single-node network.
```
Time T0: Node A starts
├── Generates or loads keypair (npub_A, nsec_A)
├── Computes node_id_A = SHA-256(npub_A)
├── Computes node_addr_A = SHA-256(npub_A)
├── Initializes empty TreeState
├── Sets parent = self (A is its own root)
├── Sets sequence = 1
@@ -125,7 +125,7 @@ integrates it into the spanning tree.
```
Existing tree structure:
A (root, smallest node_id)
A (root, smallest node_addr)
/|\
C D E
@@ -157,8 +157,8 @@ B receives D's TreeAnnounce:
│ └── TreeState_B = { (B, parent=B, seq=1), (D, parent=A, seq=47), (A, parent=A, seq=203) }
├── Evaluates root:
│ └── Compares node_id_A vs node_id_B
│ └── If A < B: Root_B = A (A has smaller node_id)
│ └── Compares node_addr_A vs node_addr_B
│ └── If A < B: Root_B = A (A has smaller node_addr)
└── Evaluates parent selection:
└── Only peer is D
@@ -195,7 +195,7 @@ D receives and merges:
**T5: D updates its bloom filter**:
```
D adds B's node_id to its bloom filter
D adds B's node_addr to its bloom filter
D sends FilterAnnounce to parent A
A merges D's bloom filter with its view of D's subtree
@@ -241,7 +241,7 @@ each node only knows its own ancestry and peers. Convergence means:
When multiple isolated nodes connect simultaneously, the network must:
1. Elect a single root (determined by smallest node_id)
1. Elect a single root (determined by smallest node_addr)
2. Form a loop-free tree structure
3. Propagate ancestry information along peer links
@@ -250,7 +250,7 @@ When multiple isolated nodes connect simultaneously, the network must:
```
T0: Nodes A, B, C start isolated
Each is its own root
node_id ordering: A < B < C
node_addr ordering: A < B < C
T1: Links form: A ←→ B, B ←→ C
@@ -301,7 +301,7 @@ depth D with gossip interval G:
**No coordination required**: Convergence emerges from:
- Deterministic root election (smallest node_id)
- Deterministic root election (smallest node_addr)
- Deterministic merge rules (highest sequence wins)
- Eventually consistent gossip
@@ -420,7 +420,7 @@ Link C ←→ D fails:
After:
Partition 1: A ← B ← C
Partition 2: D ← E (or E ← D, depending on node_ids)
Partition 2: D ← E (or E ← D, depending on node_addrs)
```
### Partition Detection
@@ -455,7 +455,7 @@ Partition 1 (nodes A, B, C):
Partition 2 (nodes D, E):
├── Previous root A is unreachable
├── D and E exchange announcements
├── New root = min(node_id_D, node_id_E)
├── New root = min(node_addr_D, node_addr_E)
├── Tree forms between D and E
└── Routing works within partition
```
@@ -489,10 +489,10 @@ T4: Merged network
### Root Stability Across Partitions
A key design consideration: the root should be stable to minimize reconvergence.
If partition 2 elected a "temporary" root with a large node_id, healing is cheap—
If partition 2 elected a "temporary" root with a large node_addr, healing is cheap—
that root immediately defers to the global root.
If by chance partition 2's root has a smaller node_id than partition 1's root,
If by chance partition 2's root has a smaller node_addr than partition 1's root,
healing causes partition 1 to reconverge to the new global root.
---
@@ -837,7 +837,7 @@ Physical topology (full mesh via switch):
│╱ ╲│
D ──── C ──── E
node_id ordering: A < C < B < E < D
node_addr ordering: A < C < B < E < D
```
**Tree formation**:
@@ -911,7 +911,7 @@ Physical topology:
(DSL) (fiber)
1 Mbps
node_id ordering: B < A < D < E < C
node_addr ordering: B < A < D < E < C
```
**Cost calculation** (using bandwidth as primary):
@@ -928,7 +928,7 @@ A ~ C: cost = 100000 (9600 bps)
**Tree formation with costs**:
```
Root = B (smallest node_id)
Root = B (smallest node_addr)
Parent selection:
├── A: peers are B (cost 1), C (cost 100000)
@@ -982,7 +982,7 @@ A ─── B ──────────────────────
│ │
C G
node_id ordering: A < E < B < F < C < G
node_addr ordering: A < E < B < F < C < G
```
**Normal operation**:
@@ -1014,7 +1014,7 @@ T3: Site 2 state:
E loses path to A
E evaluates remaining peers: F
F has no path to A either
E compares: node_id_E < node_id_F
E compares: node_addr_E < node_addr_F
E becomes new root for Site 2
T4: Site 2 reconverges:
@@ -1033,7 +1033,7 @@ T5: WAN link restored
T6: E receives B's announcement:
B's ancestry: [A, B]
E learns: A exists, node_id_A < node_id_E
E learns: A exists, node_addr_A < node_addr_E
E adopts A as root
E selects B as parent
@@ -1064,7 +1064,7 @@ initial exchange).
The gossip-based spanning tree protocol achieves distributed coordination
through:
1. **Deterministic root election** - Smallest node_id, no negotiation needed
1. **Deterministic root election** - Smallest node_addr, no negotiation needed
2. **Local parent selection** - Each node independently chooses best path to root
3. **CRDT merge semantics** - Conflicts resolved by sequence number, then timestamp
4. **Bounded state** - O(peers × depth) entries per node, not O(network size)

View File

@@ -83,7 +83,7 @@ async fn main() {
// Log node information
info!("Node created:");
info!(" npub: {}", node.npub());
info!(" node_id: {}", hex::encode(node.node_id().as_bytes()));
info!(" node_addr: {}", hex::encode(node.node_addr().as_bytes()));
info!(" address: {}", node.identity().address());
info!(" state: {}", node.state());
info!(" leaf_only: {}", node.is_leaf_only());

View File

@@ -15,7 +15,7 @@
//! overcounted by assuming mesh connectivity vs tree structure with TTL-bounded
//! propagation. Actual filter occupancy is ~250-800 entries for typical nodes.
use crate::NodeId;
use crate::NodeAddr;
use std::collections::{HashMap, HashSet};
use std::fmt;
use thiserror::Error;
@@ -114,10 +114,10 @@ impl BloomFilter {
Self::from_bytes(bytes.to_vec(), hash_count)
}
/// Insert a NodeId into the filter.
pub fn insert(&mut self, node_id: &NodeId) {
/// Insert a NodeAddr into the filter.
pub fn insert(&mut self, node_addr: &NodeAddr) {
for i in 0..self.hash_count {
let bit_index = self.hash(node_id.as_bytes(), i);
let bit_index = self.hash(node_addr.as_bytes(), i);
self.set_bit(bit_index);
}
}
@@ -130,12 +130,12 @@ impl BloomFilter {
}
}
/// Check if the filter might contain a NodeId.
/// Check if the filter might contain a NodeAddr.
///
/// Returns `true` if the item might be in the set (possible false positive).
/// Returns `false` if the item is definitely not in the set.
pub fn contains(&self, node_id: &NodeId) -> bool {
self.contains_bytes(node_id.as_bytes())
pub fn contains(&self, node_addr: &NodeAddr) -> bool {
self.contains_bytes(node_addr.as_bytes())
}
/// Check if the filter might contain raw bytes.
@@ -293,27 +293,27 @@ impl fmt::Debug for BloomFilter {
/// Tracks local filter state and what needs to be sent to peers.
#[derive(Clone, Debug)]
pub struct BloomState {
/// This node's NodeId (always included in outgoing filters).
own_node_id: NodeId,
/// This node's NodeAddr (always included in outgoing filters).
own_node_addr: NodeAddr,
/// Leaf-only nodes we speak for (included in our filter).
leaf_dependents: HashSet<NodeId>,
leaf_dependents: HashSet<NodeAddr>,
/// Whether this node operates in leaf-only mode.
is_leaf_only: bool,
/// Rate limiting: minimum interval between outgoing updates (milliseconds).
update_debounce_ms: u64,
/// Timestamp of last update sent (per peer, in milliseconds).
last_update_sent: HashMap<NodeId, u64>,
last_update_sent: HashMap<NodeAddr, u64>,
/// Peers that need a filter update.
pending_updates: HashSet<NodeId>,
pending_updates: HashSet<NodeAddr>,
/// Current sequence number for outgoing filters.
sequence: u64,
}
impl BloomState {
/// Create new Bloom state for a node.
pub fn new(own_node_id: NodeId) -> Self {
pub fn new(own_node_addr: NodeAddr) -> Self {
Self {
own_node_id,
own_node_addr,
leaf_dependents: HashSet::new(),
is_leaf_only: false,
update_debounce_ms: 500,
@@ -324,15 +324,15 @@ impl BloomState {
}
/// Create state for a leaf-only node.
pub fn leaf_only(own_node_id: NodeId) -> Self {
let mut state = Self::new(own_node_id);
pub fn leaf_only(own_node_addr: NodeAddr) -> Self {
let mut state = Self::new(own_node_addr);
state.is_leaf_only = true;
state
}
/// Get the node's own ID.
pub fn own_node_id(&self) -> &NodeId {
&self.own_node_id
pub fn own_node_addr(&self) -> &NodeAddr {
&self.own_node_addr
}
/// Check if this is a leaf-only node.
@@ -362,17 +362,17 @@ impl BloomState {
}
/// Add a leaf dependent that we'll include in our filter.
pub fn add_leaf_dependent(&mut self, node_id: NodeId) {
self.leaf_dependents.insert(node_id);
pub fn add_leaf_dependent(&mut self, node_addr: NodeAddr) {
self.leaf_dependents.insert(node_addr);
}
/// Remove a leaf dependent.
pub fn remove_leaf_dependent(&mut self, node_id: &NodeId) -> bool {
self.leaf_dependents.remove(node_id)
pub fn remove_leaf_dependent(&mut self, node_addr: &NodeAddr) -> bool {
self.leaf_dependents.remove(node_addr)
}
/// Get the set of leaf dependents.
pub fn leaf_dependents(&self) -> &HashSet<NodeId> {
pub fn leaf_dependents(&self) -> &HashSet<NodeAddr> {
&self.leaf_dependents
}
@@ -382,22 +382,22 @@ impl BloomState {
}
/// Mark that a peer needs an update.
pub fn mark_update_needed(&mut self, peer_id: NodeId) {
pub fn mark_update_needed(&mut self, peer_id: NodeAddr) {
self.pending_updates.insert(peer_id);
}
/// Mark all peers as needing updates.
pub fn mark_all_updates_needed(&mut self, peer_ids: impl IntoIterator<Item = NodeId>) {
pub fn mark_all_updates_needed(&mut self, peer_ids: impl IntoIterator<Item = NodeAddr>) {
self.pending_updates.extend(peer_ids);
}
/// Check if a peer needs an update.
pub fn needs_update(&self, peer_id: &NodeId) -> bool {
pub fn needs_update(&self, peer_id: &NodeAddr) -> bool {
self.pending_updates.contains(peer_id)
}
/// Check if we should send an update to a peer (respecting debounce).
pub fn should_send_update(&self, peer_id: &NodeId, current_time_ms: u64) -> bool {
pub fn should_send_update(&self, peer_id: &NodeAddr, current_time_ms: u64) -> bool {
if !self.pending_updates.contains(peer_id) {
return false;
}
@@ -409,7 +409,7 @@ impl BloomState {
}
/// Record that we sent an update to a peer.
pub fn record_update_sent(&mut self, peer_id: NodeId, current_time_ms: u64) {
pub fn record_update_sent(&mut self, peer_id: NodeAddr, current_time_ms: u64) {
self.last_update_sent.insert(peer_id, current_time_ms);
self.pending_updates.remove(&peer_id);
}
@@ -430,13 +430,13 @@ impl BloomState {
/// The filter for `exclude_peer` is excluded to prevent routing loops.
pub fn compute_outgoing_filter(
&self,
exclude_peer: &NodeId,
peer_filters: &HashMap<NodeId, (BloomFilter, u8)>, // (filter, ttl)
exclude_peer: &NodeAddr,
peer_filters: &HashMap<NodeAddr, (BloomFilter, u8)>, // (filter, ttl)
) -> BloomFilter {
let mut filter = BloomFilter::new();
// Always include ourselves
filter.insert(&self.own_node_id);
filter.insert(&self.own_node_addr);
// Include leaf dependents
for dep in &self.leaf_dependents {
@@ -457,7 +457,7 @@ impl BloomState {
/// Create a base filter containing just this node and its dependents.
pub fn base_filter(&self) -> BloomFilter {
let mut filter = BloomFilter::new();
filter.insert(&self.own_node_id);
filter.insert(&self.own_node_addr);
for dep in &self.leaf_dependents {
filter.insert(dep);
}
@@ -469,10 +469,10 @@ impl BloomState {
mod tests {
use super::*;
fn make_node_id(val: u8) -> NodeId {
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
bytes[0] = val;
NodeId::from_bytes(bytes)
NodeAddr::from_bytes(bytes)
}
// ===== BloomFilter Tests =====
@@ -489,8 +489,8 @@ mod tests {
#[test]
fn test_bloom_filter_insert_contains() {
let mut filter = BloomFilter::new();
let node1 = make_node_id(1);
let node2 = make_node_id(2);
let node1 = make_node_addr(1);
let node2 = make_node_addr(2);
assert!(!filter.contains(&node1));
assert!(!filter.contains(&node2));
@@ -507,13 +507,13 @@ mod tests {
let mut filter = BloomFilter::new();
for i in 0..100 {
let node = make_node_id(i);
let node = make_node_addr(i);
filter.insert(&node);
}
// All inserted items should be found
for i in 0..100 {
let node = make_node_id(i);
let node = make_node_addr(i);
assert!(filter.contains(&node), "Node {} not found", i);
}
@@ -527,8 +527,8 @@ mod tests {
let mut filter1 = BloomFilter::new();
let mut filter2 = BloomFilter::new();
let node1 = make_node_id(1);
let node2 = make_node_id(2);
let node1 = make_node_addr(1);
let node2 = make_node_addr(2);
filter1.insert(&node1);
filter2.insert(&node2);
@@ -544,8 +544,8 @@ mod tests {
let mut filter1 = BloomFilter::new();
let mut filter2 = BloomFilter::new();
let node1 = make_node_id(1);
let node2 = make_node_id(2);
let node1 = make_node_addr(1);
let node2 = make_node_addr(2);
filter1.insert(&node1);
filter2.insert(&node2);
@@ -562,7 +562,7 @@ mod tests {
#[test]
fn test_bloom_filter_clear() {
let mut filter = BloomFilter::new();
let node = make_node_id(1);
let node = make_node_addr(1);
filter.insert(&node);
assert!(!filter.is_empty());
@@ -631,7 +631,7 @@ mod tests {
// Insert some items
for i in 0..50 {
filter.insert(&make_node_id(i));
filter.insert(&make_node_addr(i));
}
// Estimate should be reasonably close to 50
@@ -650,10 +650,10 @@ mod tests {
assert_eq!(filter1, filter2);
filter1.insert(&make_node_id(1));
filter1.insert(&make_node_addr(1));
assert_ne!(filter1, filter2);
filter2.insert(&make_node_id(1));
filter2.insert(&make_node_addr(1));
assert_eq!(filter1, filter2);
}
@@ -661,10 +661,10 @@ mod tests {
#[test]
fn test_bloom_state_new() {
let node = make_node_id(0);
let node = make_node_addr(0);
let state = BloomState::new(node);
assert_eq!(state.own_node_id(), &node);
assert_eq!(state.own_node_addr(), &node);
assert!(!state.is_leaf_only());
assert_eq!(state.sequence(), 0);
assert_eq!(state.leaf_dependent_count(), 0);
@@ -672,7 +672,7 @@ mod tests {
#[test]
fn test_bloom_state_leaf_only() {
let node = make_node_id(0);
let node = make_node_addr(0);
let state = BloomState::leaf_only(node);
assert!(state.is_leaf_only());
@@ -680,11 +680,11 @@ mod tests {
#[test]
fn test_bloom_state_leaf_dependents() {
let node = make_node_id(0);
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let leaf1 = make_node_id(1);
let leaf2 = make_node_id(2);
let leaf1 = make_node_addr(1);
let leaf2 = make_node_addr(2);
state.add_leaf_dependent(leaf1);
state.add_leaf_dependent(leaf2);
@@ -698,8 +698,8 @@ mod tests {
#[test]
fn test_bloom_state_debounce() {
let node = make_node_id(0);
let peer = make_node_id(1);
let node = make_node_addr(0);
let peer = make_node_addr(1);
let mut state = BloomState::new(node);
state.set_update_debounce_ms(500);
@@ -721,7 +721,7 @@ mod tests {
#[test]
fn test_bloom_state_sequence() {
let node = make_node_id(0);
let node = make_node_addr(0);
let mut state = BloomState::new(node);
assert_eq!(state.sequence(), 0);
@@ -732,11 +732,11 @@ mod tests {
#[test]
fn test_bloom_state_pending_updates() {
let node = make_node_id(0);
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let peer1 = make_node_id(1);
let peer2 = make_node_id(2);
let peer1 = make_node_addr(1);
let peer2 = make_node_addr(2);
assert!(!state.needs_update(&peer1));
@@ -755,37 +755,37 @@ mod tests {
#[test]
fn test_bloom_state_base_filter() {
let node = make_node_id(0);
let node = make_node_addr(0);
let mut state = BloomState::new(node);
let leaf = make_node_id(1);
let leaf = make_node_addr(1);
state.add_leaf_dependent(leaf);
let filter = state.base_filter();
assert!(filter.contains(&node));
assert!(filter.contains(&leaf));
assert!(!filter.contains(&make_node_id(99)));
assert!(!filter.contains(&make_node_addr(99)));
}
#[test]
fn test_bloom_state_compute_outgoing_filter() {
let my_node = make_node_id(0);
let my_node = make_node_addr(0);
let mut state = BloomState::new(my_node);
let leaf = make_node_id(1);
let leaf = make_node_addr(1);
state.add_leaf_dependent(leaf);
let peer1 = make_node_id(10);
let peer2 = make_node_id(20);
let peer1 = make_node_addr(10);
let peer2 = make_node_addr(20);
// Create peer filters
let mut filter1 = BloomFilter::new();
filter1.insert(&make_node_id(100));
filter1.insert(&make_node_id(101));
filter1.insert(&make_node_addr(100));
filter1.insert(&make_node_addr(101));
let mut filter2 = BloomFilter::new();
filter2.insert(&make_node_id(200));
filter2.insert(&make_node_addr(200));
let mut peer_filters = HashMap::new();
peer_filters.insert(peer1, (filter1, 2)); // TTL 2
@@ -795,38 +795,38 @@ mod tests {
let outgoing1 = state.compute_outgoing_filter(&peer1, &peer_filters);
assert!(outgoing1.contains(&my_node)); // self
assert!(outgoing1.contains(&leaf)); // leaf dependent
assert!(outgoing1.contains(&make_node_id(200))); // from peer2
assert!(outgoing1.contains(&make_node_addr(200))); // from peer2
// peer1's nodes may or may not be present (depends on split brain)
// Filter for peer2 should exclude peer2's contributions
let outgoing2 = state.compute_outgoing_filter(&peer2, &peer_filters);
assert!(outgoing2.contains(&my_node));
assert!(outgoing2.contains(&leaf));
assert!(outgoing2.contains(&make_node_id(100))); // from peer1
assert!(outgoing2.contains(&make_node_id(101))); // from peer1
assert!(outgoing2.contains(&make_node_addr(100))); // from peer1
assert!(outgoing2.contains(&make_node_addr(101))); // from peer1
}
#[test]
fn test_bloom_state_ttl_filtering() {
let my_node = make_node_id(0);
let my_node = make_node_addr(0);
let state = BloomState::new(my_node);
let peer1 = make_node_id(10);
let peer2 = make_node_id(20);
let peer1 = make_node_addr(10);
let peer2 = make_node_addr(20);
let mut filter1 = BloomFilter::new();
filter1.insert(&make_node_id(100));
filter1.insert(&make_node_addr(100));
let mut filter2 = BloomFilter::new();
filter2.insert(&make_node_id(200));
filter2.insert(&make_node_addr(200));
let mut peer_filters = HashMap::new();
peer_filters.insert(peer1, (filter1, 1)); // TTL 1 - included
peer_filters.insert(peer2, (filter2, 0)); // TTL 0 - excluded
let outgoing = state.compute_outgoing_filter(&make_node_id(99), &peer_filters);
let outgoing = state.compute_outgoing_filter(&make_node_addr(99), &peer_filters);
assert!(outgoing.contains(&make_node_id(100))); // TTL 1
assert!(!outgoing.contains(&make_node_id(200))); // TTL 0 excluded
assert!(outgoing.contains(&make_node_addr(100))); // TTL 1
assert!(!outgoing.contains(&make_node_addr(200))); // TTL 0 excluded
}
}

View File

@@ -5,7 +5,7 @@
//! RouteCache stores coordinates learned from discovery queries.
use crate::tree::TreeCoordinate;
use crate::{FipsAddress, NodeId};
use crate::{FipsAddress, NodeAddr};
use std::collections::HashMap;
use thiserror::Error;
@@ -413,11 +413,11 @@ impl CachedCoords {
///
/// Separate from CoordCache, this stores routes learned from the discovery
/// protocol (LookupRequest/LookupResponse) rather than session establishment.
/// Keyed by NodeId rather than FipsAddress.
/// Keyed by NodeAddr rather than FipsAddress.
#[derive(Clone, Debug)]
pub struct RouteCache {
/// NodeId -> discovered coordinates.
entries: HashMap<NodeId, CachedCoords>,
/// NodeAddr -> discovered coordinates.
entries: HashMap<NodeAddr, CachedCoords>,
/// Maximum entries.
max_entries: usize,
}
@@ -442,9 +442,9 @@ impl RouteCache {
}
/// Insert a discovered route.
pub fn insert(&mut self, node_id: NodeId, coords: TreeCoordinate, current_time_ms: u64) {
pub fn insert(&mut self, node_addr: NodeAddr, coords: TreeCoordinate, current_time_ms: u64) {
// Update existing
if let Some(entry) = self.entries.get_mut(&node_id) {
if let Some(entry) = self.entries.get_mut(&node_addr) {
entry.update(coords, current_time_ms);
return;
}
@@ -455,21 +455,21 @@ impl RouteCache {
}
self.entries
.insert(node_id, CachedCoords::new(coords, current_time_ms));
.insert(node_addr, CachedCoords::new(coords, current_time_ms));
}
/// Look up a route (without touching).
pub fn get(&self, node_id: &NodeId) -> Option<&CachedCoords> {
self.entries.get(node_id)
pub fn get(&self, node_addr: &NodeAddr) -> Option<&CachedCoords> {
self.entries.get(node_addr)
}
/// Look up and touch.
pub fn get_and_touch(
&mut self,
node_id: &NodeId,
node_addr: &NodeAddr,
current_time_ms: u64,
) -> Option<&TreeCoordinate> {
if let Some(entry) = self.entries.get_mut(node_id) {
if let Some(entry) = self.entries.get_mut(node_addr) {
entry.touch(current_time_ms);
Some(entry.coords())
} else {
@@ -478,13 +478,13 @@ impl RouteCache {
}
/// Remove a route (e.g., after route failure).
pub fn invalidate(&mut self, node_id: &NodeId) -> Option<CachedCoords> {
self.entries.remove(node_id)
pub fn invalidate(&mut self, node_addr: &NodeAddr) -> Option<CachedCoords> {
self.entries.remove(node_addr)
}
/// Check if a node is cached.
pub fn contains(&self, node_id: &NodeId) -> bool {
self.entries.contains_key(node_id)
pub fn contains(&self, node_addr: &NodeAddr) -> bool {
self.entries.contains_key(node_addr)
}
/// Number of cached routes.
@@ -533,10 +533,10 @@ impl Default for RouteCache {
mod tests {
use super::*;
fn make_node_id(val: u8) -> NodeId {
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
bytes[0] = val;
NodeId::from_bytes(bytes)
NodeAddr::from_bytes(bytes)
}
fn make_address(val: u8) -> FipsAddress {
@@ -546,7 +546,7 @@ mod tests {
}
fn make_coords(ids: &[u8]) -> TreeCoordinate {
TreeCoordinate::new(ids.iter().map(|&v| make_node_id(v)).collect()).unwrap()
TreeCoordinate::new(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
}
// ===== CacheEntry Tests =====
@@ -723,7 +723,7 @@ mod tests {
#[test]
fn test_route_cache_basic() {
let mut cache = RouteCache::new(100);
let node = make_node_id(1);
let node = make_node_addr(1);
let coords = make_coords(&[1, 0]);
cache.insert(node, coords.clone(), 0);
@@ -735,7 +735,7 @@ mod tests {
#[test]
fn test_route_cache_invalidate() {
let mut cache = RouteCache::new(100);
let node = make_node_id(1);
let node = make_node_addr(1);
let coords = make_coords(&[1, 0]);
cache.insert(node, coords, 0);
@@ -749,9 +749,9 @@ mod tests {
fn test_route_cache_lru_eviction() {
let mut cache = RouteCache::new(2);
let node1 = make_node_id(1);
let node2 = make_node_id(2);
let node3 = make_node_id(3);
let node1 = make_node_addr(1);
let node2 = make_node_addr(2);
let node3 = make_node_addr(3);
cache.insert(node1, make_coords(&[1, 0]), 0);
cache.insert(node2, make_coords(&[2, 0]), 100);
@@ -772,9 +772,9 @@ mod tests {
fn test_route_cache_evict_older_than() {
let mut cache = RouteCache::new(100);
cache.insert(make_node_id(1), make_coords(&[1, 0]), 0);
cache.insert(make_node_id(2), make_coords(&[2, 0]), 500);
cache.insert(make_node_id(3), make_coords(&[3, 0]), 1000);
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 500);
cache.insert(make_node_addr(3), make_coords(&[3, 0]), 1000);
let evicted = cache.evict_older_than(600, 1000);
@@ -785,7 +785,7 @@ mod tests {
#[test]
fn test_route_cache_update() {
let mut cache = RouteCache::new(100);
let node = make_node_id(1);
let node = make_node_addr(1);
cache.insert(node, make_coords(&[1, 0]), 0);
cache.insert(node, make_coords(&[1, 2, 0]), 500);

View File

@@ -1,6 +1,6 @@
//! FIPS Identity System
//!
//! Node identity based on Nostr keypairs (secp256k1). The node_id is derived
//! Node identity based on Nostr keypairs (secp256k1). The node_addr is derived
//! from the public key via SHA-256, and the FIPS address uses an IPv6-compatible
//! format with the 0xfd prefix.
@@ -33,8 +33,8 @@ pub enum IdentityError {
#[error("signature verification failed")]
SignatureVerificationFailed,
#[error("invalid node_id length: expected 32, got {0}")]
InvalidNodeIdLength(usize),
#[error("invalid node_addr length: expected 32, got {0}")]
InvalidNodeAddrLength(usize),
#[error("invalid address length: expected 16, got {0}")]
InvalidAddressLength(usize),
@@ -64,31 +64,31 @@ pub enum IdentityError {
InvalidHex(#[from] hex::FromHexError),
}
/// 32-byte node identifier derived from SHA-256(npub).
/// 32-byte node identifier derived from SHA-256(pubkey).
///
/// The node_id is used in protocol messages and bloom filters. Hashing the
/// The node_addr is used in protocol messages and bloom filters. Hashing the
/// public key prevents grinding attacks that exploit secp256k1's algebraic
/// structure.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NodeId([u8; 32]);
pub struct NodeAddr([u8; 32]);
impl NodeId {
/// Create a NodeId from a 32-byte array.
impl NodeAddr {
/// Create a NodeAddr from a 32-byte array.
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
/// Create a NodeId from a slice.
/// Create a NodeAddr from a slice.
pub fn from_slice(slice: &[u8]) -> Result<Self, IdentityError> {
if slice.len() != 32 {
return Err(IdentityError::InvalidNodeIdLength(slice.len()));
return Err(IdentityError::InvalidNodeAddrLength(slice.len()));
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(slice);
Ok(Self(bytes))
}
/// Derive a NodeId from an x-only public key (npub).
/// Derive a NodeAddr from an x-only public key (npub).
pub fn from_pubkey(pubkey: &XOnlyPublicKey) -> Self {
let mut hasher = Sha256::new();
hasher.update(pubkey.serialize());
@@ -109,19 +109,19 @@ impl NodeId {
}
}
impl fmt::Debug for NodeId {
impl fmt::Debug for NodeAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "NodeId({})", hex_encode(&self.0[..8]))
write!(f, "NodeAddr({})", hex_encode(&self.0[..8]))
}
}
impl fmt::Display for NodeId {
impl fmt::Display for NodeAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", hex_encode(&self.0))
}
}
impl AsRef<[u8]> for NodeId {
impl AsRef<[u8]> for NodeAddr {
fn as_ref(&self) -> &[u8] {
&self.0
}
@@ -130,7 +130,7 @@ impl AsRef<[u8]> for NodeId {
/// 128-bit FIPS address with IPv6-compatible format.
///
/// The address uses the IPv6 Unique Local Address (ULA) prefix `fd00::/8`,
/// providing 120 bits for the node_id hash. This format allows applications
/// providing 120 bits for the node_addr hash. This format allows applications
/// designed for IP transports to bind to FIPS addresses via a TUN interface.
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct FipsAddress([u8; 16]);
@@ -154,13 +154,13 @@ impl FipsAddress {
Self::from_bytes(bytes)
}
/// Derive a FipsAddress from a NodeId.
/// Derive a FipsAddress from a NodeAddr.
///
/// Takes the first 15 bytes of the node_id and prepends the 0xfd prefix.
pub fn from_node_id(node_id: &NodeId) -> Self {
/// Takes the first 15 bytes of the node_addr and prepends the 0xfd prefix.
pub fn from_node_addr(node_addr: &NodeAddr) -> Self {
let mut bytes = [0u8; 16];
bytes[0] = FIPS_ADDRESS_PREFIX;
bytes[1..16].copy_from_slice(&node_id.0[0..15]);
bytes[1..16].copy_from_slice(&node_addr.0[0..15]);
Self(bytes)
}
@@ -202,7 +202,7 @@ pub struct PeerIdentity {
pubkey: XOnlyPublicKey,
/// Full public key if known (includes parity for ECDH operations).
pubkey_full: Option<PublicKey>,
node_id: NodeId,
node_addr: NodeAddr,
address: FipsAddress,
}
@@ -212,12 +212,12 @@ impl PeerIdentity {
/// Note: When only the x-only key is available, the full public key
/// will be derived assuming even parity for ECDH operations.
pub fn from_pubkey(pubkey: XOnlyPublicKey) -> Self {
let node_id = NodeId::from_pubkey(&pubkey);
let address = FipsAddress::from_node_id(&node_id);
let node_addr = NodeAddr::from_pubkey(&pubkey);
let address = FipsAddress::from_node_addr(&node_addr);
Self {
pubkey,
pubkey_full: None,
node_id,
node_addr,
address,
}
}
@@ -228,12 +228,12 @@ impl PeerIdentity {
/// handshake) to preserve parity information for ECDH operations.
pub fn from_pubkey_full(pubkey: PublicKey) -> Self {
let (x_only, _parity) = pubkey.x_only_public_key();
let node_id = NodeId::from_pubkey(&x_only);
let address = FipsAddress::from_node_id(&node_id);
let node_addr = NodeAddr::from_pubkey(&x_only);
let address = FipsAddress::from_node_addr(&node_addr);
Self {
pubkey: x_only,
pubkey_full: Some(pubkey),
node_id,
node_addr,
address,
}
}
@@ -266,8 +266,8 @@ impl PeerIdentity {
}
/// Return the node ID.
pub fn node_id(&self) -> &NodeId {
&self.node_id
pub fn node_addr(&self) -> &NodeAddr {
&self.node_addr
}
/// Return the FIPS address.
@@ -286,7 +286,7 @@ impl PeerIdentity {
impl fmt::Debug for PeerIdentity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PeerIdentity")
.field("node_id", &self.node_id)
.field("node_addr", &self.node_addr)
.field("address", &self.address)
.finish()
}
@@ -304,7 +304,7 @@ impl fmt::Display for PeerIdentity {
/// and verifying protocol messages.
pub struct Identity {
keypair: Keypair,
node_id: NodeId,
node_addr: NodeAddr,
address: FipsAddress,
}
@@ -319,11 +319,11 @@ impl Identity {
/// Create an identity from an existing keypair.
pub fn from_keypair(keypair: Keypair) -> Self {
let (pubkey, _parity) = keypair.x_only_public_key();
let node_id = NodeId::from_pubkey(&pubkey);
let address = FipsAddress::from_node_id(&node_id);
let node_addr = NodeAddr::from_pubkey(&pubkey);
let address = FipsAddress::from_node_addr(&node_addr);
Self {
keypair,
node_id,
node_addr,
address,
}
}
@@ -370,8 +370,8 @@ impl Identity {
}
/// Return the node ID.
pub fn node_id(&self) -> &NodeId {
&self.node_id
pub fn node_addr(&self) -> &NodeAddr {
&self.node_addr
}
/// Return the FIPS address.
@@ -404,7 +404,7 @@ impl Identity {
impl fmt::Debug for Identity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identity")
.field("node_id", &self.node_id)
.field("node_addr", &self.node_addr)
.field("address", &self.address)
.finish_non_exhaustive()
}
@@ -433,14 +433,14 @@ impl AuthChallenge {
}
/// Verify a response to this challenge.
pub fn verify(&self, response: &AuthResponse) -> Result<NodeId, IdentityError> {
pub fn verify(&self, response: &AuthResponse) -> Result<NodeAddr, IdentityError> {
let digest = auth_challenge_digest(&self.0, response.timestamp);
let secp = Secp256k1::new();
secp.verify_schnorr(&response.signature, &digest, &response.pubkey)
.map_err(|_| IdentityError::SignatureVerificationFailed)?;
Ok(NodeId::from_pubkey(&response.pubkey))
Ok(NodeAddr::from_pubkey(&response.pubkey))
}
}
@@ -547,28 +547,28 @@ mod tests {
fn test_identity_generation() {
let identity = Identity::generate();
// NodeId should be 32 bytes
assert_eq!(identity.node_id().as_bytes().len(), 32);
// NodeAddr should be 32 bytes
assert_eq!(identity.node_addr().as_bytes().len(), 32);
// Address should start with 0xfd
assert_eq!(identity.address().as_bytes()[0], 0xfd);
// Address bytes 1-15 should match node_id bytes 0-14
// Address bytes 1-15 should match node_addr bytes 0-14
assert_eq!(
&identity.address().as_bytes()[1..16],
&identity.node_id().as_bytes()[0..15]
&identity.node_addr().as_bytes()[0..15]
);
}
#[test]
fn test_node_id_from_pubkey_deterministic() {
fn test_node_addr_from_pubkey_deterministic() {
let identity = Identity::generate();
let pubkey = identity.pubkey();
let node_id1 = NodeId::from_pubkey(&pubkey);
let node_id2 = NodeId::from_pubkey(&pubkey);
let node_addr1 = NodeAddr::from_pubkey(&pubkey);
let node_addr2 = NodeAddr::from_pubkey(&pubkey);
assert_eq!(node_id1, node_id2);
assert_eq!(node_addr1, node_addr2);
}
#[test]
@@ -595,7 +595,7 @@ mod tests {
let result = challenge.verify(&response);
assert!(result.is_ok());
assert_eq!(result.unwrap(), *identity.node_id());
assert_eq!(result.unwrap(), *identity.node_addr());
}
#[test]
@@ -636,12 +636,12 @@ mod tests {
}
#[test]
fn test_node_id_ordering() {
fn test_node_addr_ordering() {
let id1 = Identity::generate();
let id2 = Identity::generate();
// NodeIds should be comparable for root election
let _cmp = id1.node_id().cmp(id2.node_id());
// NodeAddrs should be comparable for root election
let _cmp = id1.node_addr().cmp(id2.node_addr());
}
#[test]
@@ -656,22 +656,22 @@ mod tests {
let identity1 = Identity::from_secret_bytes(&secret_bytes).unwrap();
let identity2 = Identity::from_secret_bytes(&secret_bytes).unwrap();
// Same secret key should produce same node_id
assert_eq!(identity1.node_id(), identity2.node_id());
// Same secret key should produce same node_addr
assert_eq!(identity1.node_addr(), identity2.node_addr());
assert_eq!(identity1.address(), identity2.address());
}
#[test]
fn test_node_id_from_slice() {
fn test_node_addr_from_slice() {
let bytes = [0u8; 32];
let node_id = NodeId::from_slice(&bytes).unwrap();
assert_eq!(node_id.as_bytes(), &bytes);
let node_addr = NodeAddr::from_slice(&bytes).unwrap();
assert_eq!(node_addr.as_bytes(), &bytes);
// Wrong length should fail
let short = [0u8; 16];
assert!(matches!(
NodeId::from_slice(&short),
Err(IdentityError::InvalidNodeIdLength(16))
NodeAddr::from_slice(&short),
Err(IdentityError::InvalidNodeAddrLength(16))
));
}
@@ -772,7 +772,7 @@ mod tests {
let peer = PeerIdentity::from_npub(&npub).unwrap();
assert_eq!(peer.pubkey(), identity.pubkey());
assert_eq!(peer.node_id(), identity.node_id());
assert_eq!(peer.node_addr(), identity.node_addr());
assert_eq!(peer.address(), identity.address());
assert_eq!(peer.npub(), npub);
}
@@ -877,7 +877,7 @@ mod tests {
let identity = Identity::from_secret_str(&nsec).unwrap();
let identity_from_bytes = Identity::from_secret_bytes(&secret_bytes).unwrap();
assert_eq!(identity.node_id(), identity_from_bytes.node_id());
assert_eq!(identity.node_addr(), identity_from_bytes.node_addr());
}
#[test]
@@ -892,6 +892,6 @@ mod tests {
let identity = Identity::from_secret_str(hex_str).unwrap();
let identity_from_bytes = Identity::from_secret_bytes(&secret_bytes).unwrap();
assert_eq!(identity.node_id(), identity_from_bytes.node_id());
assert_eq!(identity.node_addr(), identity_from_bytes.node_addr());
}
}

View File

@@ -22,7 +22,7 @@ pub mod wire;
// Re-export identity types
pub use identity::{
decode_npub, decode_nsec, decode_secret, encode_npub, encode_nsec, AuthChallenge, AuthResponse,
FipsAddress, Identity, IdentityError, NodeId, PeerIdentity,
FipsAddress, Identity, IdentityError, NodeAddr, PeerIdentity,
};
// Re-export config types

View File

@@ -23,7 +23,7 @@ use crate::wire::{
build_msg1, build_msg2, EncryptedHeader, Msg1Header, Msg2Header,
DISCRIMINATOR_ENCRYPTED, DISCRIMINATOR_MSG1, DISCRIMINATOR_MSG2,
};
use crate::{Config, ConfigError, Identity, IdentityError, NodeId, PeerIdentity};
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr, PeerIdentity};
use std::collections::HashMap;
use std::fmt;
use std::thread::{self, JoinHandle};
@@ -56,10 +56,10 @@ pub enum NodeError {
ConnectionNotFound(LinkId),
#[error("peer not found: {0:?}")]
PeerNotFound(NodeId),
PeerNotFound(NodeAddr),
#[error("peer already exists: {0:?}")]
PeerAlreadyExists(NodeId),
PeerAlreadyExists(NodeAddr),
#[error("connection already exists for link: {0}")]
ConnectionAlreadyExists(LinkId),
@@ -142,7 +142,7 @@ type AddrKey = (TransportId, TransportAddr);
///
/// Peers go through two phases:
/// 1. **Connection phase** (`connections`): Handshake in progress, indexed by LinkId
/// 2. **Active phase** (`peers`): Authenticated, indexed by NodeId
/// 2. **Active phase** (`peers`): Authenticated, indexed by NodeAddr
///
/// The `addr_to_link` map enables dispatching incoming packets to the right
/// connection before authentication completes.
@@ -195,8 +195,8 @@ pub struct Node {
// === Peers (Active Phase) ===
/// Authenticated peers.
/// Indexed by NodeId (verified identity).
peers: HashMap<NodeId, ActivePeer>,
/// Indexed by NodeAddr (verified identity).
peers: HashMap<NodeAddr, ActivePeer>,
// === Resource Limits ===
/// Maximum connections (0 = unlimited).
@@ -227,9 +227,9 @@ pub struct Node {
// === Index-Based Session Dispatch ===
/// Allocator for session indices.
index_allocator: IndexAllocator,
/// O(1) lookup: (transport_id, our_index) → NodeId.
/// O(1) lookup: (transport_id, our_index) → NodeAddr.
/// This maps our session index to the peer that uses it.
peers_by_index: HashMap<(TransportId, u32), NodeId>,
peers_by_index: HashMap<(TransportId, u32), NodeAddr>,
/// Pending outbound handshakes by our sender_idx.
/// Tracks which LinkId corresponds to which session index.
pending_outbound: HashMap<(TransportId, u32), LinkId>,
@@ -243,13 +243,13 @@ impl Node {
/// Create a new node from configuration.
pub fn new(config: Config) -> Result<Self, NodeError> {
let identity = config.create_identity()?;
let node_id = *identity.node_id();
let node_addr = *identity.node_addr();
let is_leaf_only = config.is_leaf_only();
let bloom_state = if is_leaf_only {
BloomState::leaf_only(node_id)
BloomState::leaf_only(node_addr)
} else {
BloomState::new(node_id)
BloomState::new(node_addr)
};
let tun_state = if config.tun.enabled {
@@ -259,7 +259,7 @@ impl Node {
};
// Initialize tree state with signed self-declaration
let mut tree_state = TreeState::new(node_id);
let mut tree_state = TreeState::new(node_addr);
tree_state
.sign_declaration(&identity)
.expect("signing own declaration should never fail");
@@ -298,7 +298,7 @@ impl Node {
/// Create a node with a specific identity.
pub fn with_identity(identity: Identity, config: Config) -> Self {
let node_id = *identity.node_id();
let node_addr = *identity.node_addr();
let tun_state = if config.tun.enabled {
TunState::Configured
} else {
@@ -306,7 +306,7 @@ impl Node {
};
// Initialize tree state with signed self-declaration
let mut tree_state = TreeState::new(node_id);
let mut tree_state = TreeState::new(node_addr);
tree_state
.sign_declaration(&identity)
.expect("signing own declaration should never fail");
@@ -317,7 +317,7 @@ impl Node {
state: NodeState::Created,
is_leaf_only: false,
tree_state,
bloom_state: BloomState::new(node_id),
bloom_state: BloomState::new(node_addr),
coord_cache: CoordCache::with_defaults(),
transports: HashMap::new(),
links: HashMap::new(),
@@ -347,7 +347,7 @@ impl Node {
pub fn leaf_only(config: Config) -> Result<Self, NodeError> {
let mut node = Self::new(config)?;
node.is_leaf_only = true;
node.bloom_state = BloomState::leaf_only(*node.identity.node_id());
node.bloom_state = BloomState::leaf_only(*node.identity.node_addr());
Ok(node)
}
@@ -428,10 +428,10 @@ impl Node {
}
})?;
let peer_node_id = *peer_identity.node_id();
let peer_node_addr = *peer_identity.node_addr();
// Check if peer already exists (fully authenticated)
if self.peers.contains_key(&peer_node_id) {
if self.peers.contains_key(&peer_node_addr) {
debug!(
npub = %peer_config.npub,
"Peer already exists, skipping"
@@ -442,7 +442,7 @@ impl Node {
// Check if connection already in progress to this peer
let already_connecting = self.connections.values().any(|conn| {
conn.expected_identity()
.map(|id| id.node_id() == &peer_node_id)
.map(|id| id.node_addr() == &peer_node_addr)
.unwrap_or(false)
});
if already_connecting {
@@ -545,7 +545,7 @@ impl Node {
info!("Peer connection initiated{}", alias_display);
info!(" npub: {}", peer_config.npub);
info!(" node_id: {}", peer_node_id);
info!(" node_addr: {}", peer_node_addr);
info!(" transport: {}", addr.transport);
info!(" addr: {}", addr.addr);
info!(" link_id: {}", link_id);
@@ -599,9 +599,9 @@ impl Node {
&self.identity
}
/// Get this node's NodeId.
pub fn node_id(&self) -> &NodeId {
self.identity.node_id()
/// Get this node's NodeAddr.
pub fn node_addr(&self) -> &NodeAddr {
self.identity.node_addr()
}
/// Get this node's npub.
@@ -855,23 +855,23 @@ impl Node {
.remove(&link_id)
.ok_or(NodeError::ConnectionNotFound(link_id))?;
let peer_node_id = *verified_identity.node_id();
let peer_node_addr = *verified_identity.node_addr();
let is_outbound = connection.is_outbound();
// Check for cross-connection
if let Some(existing_peer) = self.peers.get(&peer_node_id) {
if let Some(existing_peer) = self.peers.get(&peer_node_addr) {
let existing_link_id = existing_peer.link_id();
// Determine which connection wins
let this_wins = cross_connection_winner(
self.identity.node_id(),
&peer_node_id,
self.identity.node_addr(),
&peer_node_addr,
is_outbound,
);
if this_wins {
// This connection wins, replace the existing peer
let old_peer = self.peers.remove(&peer_node_id).unwrap();
let old_peer = self.peers.remove(&peer_node_addr).unwrap();
let loser_link_id = old_peer.link_id();
// Create new active peer with stats from handshake
@@ -882,10 +882,10 @@ impl Node {
connection.link_stats().clone(),
);
self.peers.insert(peer_node_id, new_peer);
self.peers.insert(peer_node_addr, new_peer);
info!(
node_id = %peer_node_id,
node_addr = %peer_node_addr,
winner_link = %link_id,
loser_link = %loser_link_id,
"Cross-connection resolved: this connection won"
@@ -893,12 +893,12 @@ impl Node {
Ok(PromotionResult::CrossConnectionWon {
loser_link_id,
node_id: peer_node_id,
node_addr: peer_node_addr,
})
} else {
// This connection loses, keep existing
info!(
node_id = %peer_node_id,
node_addr = %peer_node_addr,
winner_link = %existing_link_id,
loser_link = %link_id,
"Cross-connection resolved: this connection lost"
@@ -921,33 +921,33 @@ impl Node {
connection.link_stats().clone(),
);
self.peers.insert(peer_node_id, new_peer);
self.peers.insert(peer_node_addr, new_peer);
info!(
node_id = %peer_node_id,
node_addr = %peer_node_addr,
link_id = %link_id,
"Connection promoted to active peer"
);
Ok(PromotionResult::Promoted(peer_node_id))
Ok(PromotionResult::Promoted(peer_node_addr))
}
}
// === Peer Management (Active Phase) ===
/// Get a peer by NodeId.
pub fn get_peer(&self, node_id: &NodeId) -> Option<&ActivePeer> {
self.peers.get(node_id)
/// Get a peer by NodeAddr.
pub fn get_peer(&self, node_addr: &NodeAddr) -> Option<&ActivePeer> {
self.peers.get(node_addr)
}
/// Get a mutable peer by NodeId.
pub fn get_peer_mut(&mut self, node_id: &NodeId) -> Option<&mut ActivePeer> {
self.peers.get_mut(node_id)
/// Get a mutable peer by NodeAddr.
pub fn get_peer_mut(&mut self, node_addr: &NodeAddr) -> Option<&mut ActivePeer> {
self.peers.get_mut(node_addr)
}
/// Remove a peer.
pub fn remove_peer(&mut self, node_id: &NodeId) -> Option<ActivePeer> {
self.peers.remove(node_id)
pub fn remove_peer(&mut self, node_addr: &NodeAddr) -> Option<ActivePeer> {
self.peers.remove(node_addr)
}
/// Iterate over all peers.
@@ -956,7 +956,7 @@ impl Node {
}
/// Iterate over all peer node IDs.
pub fn peer_ids(&self) -> impl Iterator<Item = &NodeId> {
pub fn peer_ids(&self) -> impl Iterator<Item = &NodeAddr> {
self.peers.keys()
}
@@ -975,13 +975,13 @@ impl Node {
/// Find next hop for a destination (stub).
///
/// Returns the peer that minimizes tree distance to the destination.
pub fn find_next_hop(&self, _dest_node_id: &NodeId) -> Option<&ActivePeer> {
pub fn find_next_hop(&self, _dest_node_addr: &NodeAddr) -> Option<&ActivePeer> {
// Stub: would implement greedy tree routing
None
}
/// Check if a destination is in any peer's bloom filter.
pub fn destination_in_filters(&self, dest: &NodeId) -> Vec<&ActivePeer> {
pub fn destination_in_filters(&self, dest: &NodeAddr) -> Vec<&ActivePeer> {
self.peers.values().filter(|p| p.may_reach(dest)).collect()
}
@@ -1153,7 +1153,7 @@ impl Node {
// O(1) session lookup by our receiver index
let key = (packet.transport_id, header.receiver_idx.as_u32());
let node_id = match self.peers_by_index.get(&key) {
let node_addr = match self.peers_by_index.get(&key) {
Some(id) => *id,
None => {
// Unknown index - could be stale session or attack
@@ -1166,7 +1166,7 @@ impl Node {
}
};
let peer = match self.peers.get_mut(&node_id) {
let peer = match self.peers.get_mut(&node_addr) {
Some(p) => p,
None => {
// Peer removed but index not cleaned up - fix it
@@ -1180,7 +1180,7 @@ impl Node {
Some(s) => s,
None => {
warn!(
node_id = %node_id,
node_addr = %node_addr,
"Peer in index map has no session"
);
return;
@@ -1193,7 +1193,7 @@ impl Node {
Ok(p) => p,
Err(e) => {
debug!(
node_id = %node_id,
node_addr = %node_addr,
counter = header.counter,
error = %e,
"Decryption failed"
@@ -1212,7 +1212,7 @@ impl Node {
peer.touch(packet.timestamp_ms);
// Dispatch to link message handler
self.dispatch_link_message(&node_id, &plaintext).await;
self.dispatch_link_message(&node_addr, &plaintext).await;
}
/// Handle handshake message 1 (discriminator 0x01).
@@ -1284,14 +1284,14 @@ impl Node {
return;
}
};
let peer_node_id = *peer_identity.node_id();
let peer_node_addr = *peer_identity.node_addr();
// Check if this peer is already connected
if self.peers.contains_key(&peer_node_id) {
if self.peers.contains_key(&peer_node_addr) {
// TODO: Handle reconnection case (future: session replacement)
self.msg1_rate_limiter.complete_handshake();
debug!(
node_id = %peer_node_id,
node_addr = %peer_node_addr,
"Peer already connected, ignoring msg1"
);
return;
@@ -1355,7 +1355,7 @@ impl Node {
}
info!(
node_id = %peer_node_id,
node_addr = %peer_node_addr,
link_id = %link_id,
our_index = %our_index,
"Inbound handshake initiated"
@@ -1425,7 +1425,7 @@ impl Node {
};
info!(
node_id = %peer_identity.node_id(),
node_addr = %peer_identity.node_addr(),
link_id = %link_id,
their_index = %header.sender_idx,
"Outbound handshake completed"
@@ -1439,15 +1439,15 @@ impl Node {
self.pending_outbound.remove(&key);
match result {
PromotionResult::Promoted(node_id) => {
PromotionResult::Promoted(node_addr) => {
info!(
node_id = %node_id,
node_addr = %node_addr,
"Peer promoted to active"
);
}
PromotionResult::CrossConnectionWon { loser_link_id, node_id } => {
PromotionResult::CrossConnectionWon { loser_link_id, node_addr } => {
info!(
node_id = %node_id,
node_addr = %node_addr,
loser_link_id = %loser_link_id,
"Cross-connection won"
);
@@ -1473,7 +1473,7 @@ impl Node {
/// Dispatch a decrypted link message to the appropriate handler.
///
/// Link messages are protocol messages exchanged between authenticated peers.
async fn dispatch_link_message(&mut self, _from: &NodeId, plaintext: &[u8]) {
async fn dispatch_link_message(&mut self, _from: &NodeAddr, plaintext: &[u8]) {
if plaintext.is_empty() {
return;
}
@@ -1584,7 +1584,7 @@ impl Node {
impl fmt::Debug for Node {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Node")
.field("node_id", self.node_id())
.field("node_addr", self.node_addr())
.field("state", &self.state)
.field("is_leaf_only", &self.is_leaf_only)
.field("connections", &self.connection_count())
@@ -1607,10 +1607,10 @@ mod tests {
}
#[allow(dead_code)]
fn make_node_id(val: u8) -> NodeId {
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
bytes[0] = val;
NodeId::from_bytes(bytes)
NodeAddr::from_bytes(bytes)
}
fn make_peer_identity() -> PeerIdentity {
@@ -1632,12 +1632,12 @@ mod tests {
#[test]
fn test_node_with_identity() {
let identity = Identity::generate();
let expected_node_id = *identity.node_id();
let expected_node_addr = *identity.node_addr();
let config = Config::new();
let node = Node::with_identity(identity, config);
assert_eq!(node.node_id(), &expected_node_id);
assert_eq!(node.node_addr(), &expected_node_addr);
}
#[test]
@@ -1783,7 +1783,7 @@ mod tests {
let mut node = make_node();
let identity = make_peer_identity();
let node_id = *identity.node_id();
let node_addr = *identity.node_addr();
let link_id = LinkId::new(1);
let conn = PeerConnection::outbound(link_id, identity.clone(), 1000);
@@ -1797,7 +1797,7 @@ mod tests {
assert_eq!(node.connection_count(), 0);
assert_eq!(node.peer_count(), 1);
let peer = node.get_peer(&node_id).unwrap();
let peer = node.get_peer(&node_addr).unwrap();
assert_eq!(peer.authenticated_at(), 2000);
}
@@ -1807,7 +1807,7 @@ mod tests {
// First connection and promotion (becomes active peer)
let identity = make_peer_identity();
let node_id = *identity.node_id();
let node_addr = *identity.node_addr();
let link_id1 = LinkId::new(1);
let conn1 = PeerConnection::outbound(link_id1, identity.clone(), 1000);
@@ -1815,7 +1815,7 @@ mod tests {
node.promote_connection(link_id1, identity.clone(), 1500).unwrap();
assert_eq!(node.peer_count(), 1);
assert_eq!(node.get_peer(&node_id).unwrap().link_id(), link_id1);
assert_eq!(node.get_peer(&node_addr).unwrap().link_id(), link_id1);
// Second connection (simulates cross-connection scenario)
let link_id2 = LinkId::new(2);
@@ -1830,11 +1830,11 @@ mod tests {
match result {
PromotionResult::CrossConnectionWon { loser_link_id, .. } => {
assert_eq!(loser_link_id, link_id1);
assert_eq!(node.get_peer(&node_id).unwrap().link_id(), link_id2);
assert_eq!(node.get_peer(&node_addr).unwrap().link_id(), link_id2);
}
PromotionResult::CrossConnectionLost { winner_link_id } => {
assert_eq!(winner_link_id, link_id1);
assert_eq!(node.get_peer(&node_id).unwrap().link_id(), link_id1);
assert_eq!(node.get_peer(&node_addr).unwrap().link_id(), link_id1);
}
PromotionResult::Promoted(_) => {
panic!("Expected cross-connection, got normal promotion");
@@ -1912,7 +1912,7 @@ mod tests {
// Add a healthy peer
let identity1 = make_peer_identity();
let node_id1 = *identity1.node_id();
let node_addr1 = *identity1.node_addr();
let link_id1 = LinkId::new(1);
let conn1 = PeerConnection::outbound(link_id1, identity1.clone(), 1000);
node.add_connection(conn1).unwrap();
@@ -1927,19 +1927,19 @@ mod tests {
// Add a third peer and mark it disconnected (not sendable)
let identity3 = make_peer_identity();
let node_id3 = *identity3.node_id();
let node_addr3 = *identity3.node_addr();
let link_id3 = LinkId::new(3);
let conn3 = PeerConnection::outbound(link_id3, identity3.clone(), 1000);
node.add_connection(conn3).unwrap();
node.promote_connection(link_id3, identity3, 2000).unwrap();
node.get_peer_mut(&node_id3).unwrap().mark_disconnected();
node.get_peer_mut(&node_addr3).unwrap().mark_disconnected();
assert_eq!(node.peer_count(), 3);
assert_eq!(node.sendable_peer_count(), 2);
let sendable: Vec<_> = node.sendable_peers().collect();
assert_eq!(sendable.len(), 2);
assert!(sendable.iter().any(|p| p.node_id() == &node_id1));
assert!(sendable.iter().any(|p| p.node_addr() == &node_addr1));
}
// === RX Loop Tests ===
@@ -1979,17 +1979,17 @@ mod tests {
fn test_node_peers_by_index_tracking() {
let mut node = make_node();
let transport_id = TransportId::new(1);
let node_id = make_node_id(42);
let node_addr = make_node_addr(42);
// Allocate an index
let index = node.index_allocator.allocate().unwrap();
// Track in peers_by_index
node.peers_by_index.insert((transport_id, index.as_u32()), node_id);
node.peers_by_index.insert((transport_id, index.as_u32()), node_addr);
// Verify lookup
let found = node.peers_by_index.get(&(transport_id, index.as_u32()));
assert_eq!(found, Some(&node_id));
assert_eq!(found, Some(&node_addr));
// Clean up
node.peers_by_index.remove(&(transport_id, index.as_u32()));

View File

@@ -8,7 +8,7 @@ use crate::index::SessionIndex;
use crate::noise::NoiseSession;
use crate::transport::{LinkId, LinkStats, TransportAddr, TransportId};
use crate::tree::{ParentDeclaration, TreeCoordinate};
use crate::{FipsAddress, NodeId, PeerIdentity};
use crate::{FipsAddress, NodeAddr, PeerIdentity};
use secp256k1::XOnlyPublicKey;
use std::fmt;
@@ -203,9 +203,9 @@ impl ActivePeer {
&self.identity
}
/// Get the peer's NodeId.
pub fn node_id(&self) -> &NodeId {
self.identity.node_id()
/// Get the peer's NodeAddr.
pub fn node_addr(&self) -> &NodeAddr {
self.identity.node_addr()
}
/// Get the peer's FIPS address.
@@ -338,9 +338,9 @@ impl ActivePeer {
}
/// Check if a destination might be reachable through this peer.
pub fn may_reach(&self, node_id: &NodeId) -> bool {
pub fn may_reach(&self, node_addr: &NodeAddr) -> bool {
match &self.inbound_filter {
Some(filter) => filter.contains(node_id),
Some(filter) => filter.contains(node_addr),
None => false,
}
}
@@ -487,14 +487,14 @@ mod tests {
PeerIdentity::from_pubkey(identity.pubkey())
}
fn make_node_id(val: u8) -> NodeId {
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
bytes[0] = val;
NodeId::from_bytes(bytes)
NodeAddr::from_bytes(bytes)
}
fn make_coords(ids: &[u8]) -> TreeCoordinate {
TreeCoordinate::new(ids.iter().map(|&v| make_node_id(v)).collect()).unwrap()
TreeCoordinate::new(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
}
#[test]
@@ -516,7 +516,7 @@ mod tests {
let identity = make_peer_identity();
let peer = ActivePeer::new(identity.clone(), LinkId::new(1), 1000);
assert_eq!(peer.identity().node_id(), identity.node_id());
assert_eq!(peer.identity().node_addr(), identity.node_addr());
assert_eq!(peer.link_id(), LinkId::new(1));
assert!(peer.is_healthy());
assert!(peer.can_send());
@@ -558,8 +558,8 @@ mod tests {
assert!(!peer.has_tree_position());
assert!(peer.coords().is_none());
let node = make_node_id(1);
let parent = make_node_id(2);
let node = make_node_addr(1);
let parent = make_node_addr(2);
let decl = ParentDeclaration::new(node, parent, 1, 1000);
let coords = make_coords(&[1, 2, 0]);
@@ -574,7 +574,7 @@ mod tests {
fn test_bloom_filter() {
let identity = make_peer_identity();
let mut peer = ActivePeer::new(identity, LinkId::new(1), 1000);
let target = make_node_id(42);
let target = make_node_addr(42);
assert!(!peer.may_reach(&target));
assert!(peer.filter_is_stale(2000, 500));

View File

@@ -14,7 +14,7 @@ pub use active::{ActivePeer, ConnectivityState};
pub use connection::{HandshakeState, PeerConnection};
use crate::transport::LinkId;
use crate::NodeId;
use crate::NodeAddr;
use std::fmt;
use thiserror::Error;
@@ -29,13 +29,13 @@ pub enum PeerError {
NotAuthenticated,
#[error("peer not found: {0:?}")]
NotFound(NodeId),
NotFound(NodeAddr),
#[error("connection not found: {0}")]
ConnectionNotFound(LinkId),
#[error("peer already exists: {0:?}")]
AlreadyExists(NodeId),
AlreadyExists(NodeAddr),
#[error("handshake failed: {0}")]
HandshakeFailed(String),
@@ -44,7 +44,7 @@ pub enum PeerError {
HandshakeTimeout,
#[error("identity mismatch: expected {expected:?}, got {actual:?}")]
IdentityMismatch { expected: NodeId, actual: NodeId },
IdentityMismatch { expected: NodeAddr, actual: NodeAddr },
#[error("peer disconnected")]
Disconnected,
@@ -66,13 +66,13 @@ pub enum PeerError {
/// connection to this peer (cross-connection). The tie-breaker rule
/// determines which connection survives.
///
/// Note: Returns NodeId instead of ActivePeer because ActivePeer cannot
/// Note: Returns NodeAddr instead of ActivePeer because ActivePeer cannot
/// be cloned (it contains NoiseSession which has cryptographic state).
/// Callers can look up the peer from the peers map using the NodeId.
/// Callers can look up the peer from the peers map using the NodeAddr.
#[derive(Debug, Clone, Copy)]
pub enum PromotionResult {
/// New peer created successfully.
Promoted(NodeId),
Promoted(NodeAddr),
/// Cross-connection detected. This connection lost the tie-breaker
/// and should be closed.
@@ -87,16 +87,16 @@ pub enum PromotionResult {
/// The link that lost (previous connection, now closed).
loser_link_id: LinkId,
/// The node ID of the peer.
node_id: NodeId,
node_addr: NodeAddr,
},
}
impl PromotionResult {
/// Get the node ID if promotion succeeded.
pub fn node_id(&self) -> Option<NodeId> {
pub fn node_addr(&self) -> Option<NodeAddr> {
match self {
PromotionResult::Promoted(node_id) => Some(*node_id),
PromotionResult::CrossConnectionWon { node_id, .. } => Some(*node_id),
PromotionResult::Promoted(node_addr) => Some(*node_addr),
PromotionResult::CrossConnectionWon { node_addr, .. } => Some(*node_addr),
PromotionResult::CrossConnectionLost { .. } => None,
}
}
@@ -118,22 +118,22 @@ impl PromotionResult {
/// Determine winner of cross-connection tie-breaker.
///
/// Rule: The node with the smaller node_id prefers its OUTBOUND connection.
/// Rule: The node with the smaller node_addr prefers its OUTBOUND connection.
/// This is deterministic and symmetric: both nodes will reach the same conclusion.
///
/// # Arguments
/// * `our_node_id` - Our node's ID
/// * `their_node_id` - The peer's node ID
/// * `our_node_addr` - Our node's ID
/// * `their_node_addr` - The peer's node ID
/// * `this_is_outbound` - Whether the connection being evaluated is our outbound
///
/// # Returns
/// `true` if this connection should win (survive), `false` if it should close.
pub fn cross_connection_winner(
our_node_id: &NodeId,
their_node_id: &NodeId,
our_node_addr: &NodeAddr,
their_node_addr: &NodeAddr,
this_is_outbound: bool,
) -> bool {
let we_are_smaller = our_node_id < their_node_id;
let we_are_smaller = our_node_addr < their_node_addr;
// Smaller node's outbound wins
// If we're smaller: our outbound wins, our inbound loses
@@ -224,14 +224,14 @@ impl PeerSlot {
}
}
/// Get the known node_id, if any.
/// Get the known node_addr, if any.
///
/// For connections, this is the expected identity (may be None for inbound).
/// For active peers, this is always known.
pub fn node_id(&self) -> Option<&NodeId> {
pub fn node_addr(&self) -> Option<&NodeAddr> {
match self {
PeerSlot::Connecting(conn) => conn.expected_identity().map(|id| id.node_id()),
PeerSlot::Active(peer) => Some(peer.node_id()),
PeerSlot::Connecting(conn) => conn.expected_identity().map(|id| id.node_addr()),
PeerSlot::Active(peer) => Some(peer.node_addr()),
}
}
}
@@ -243,7 +243,7 @@ impl fmt::Display for PeerSlot {
write!(f, "connecting(link={}, state={})", conn.link_id(), conn.handshake_state())
}
PeerSlot::Active(peer) => {
write!(f, "active(node={:?}, link={})", peer.node_id(), peer.link_id())
write!(f, "active(node={:?}, link={})", peer.node_addr(), peer.link_id())
}
}
}
@@ -259,10 +259,10 @@ mod tests {
use crate::transport::LinkId;
use crate::{Identity, PeerIdentity};
fn make_node_id(val: u8) -> NodeId {
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
bytes[0] = val;
NodeId::from_bytes(bytes)
NodeAddr::from_bytes(bytes)
}
fn make_peer_identity() -> PeerIdentity {
@@ -272,8 +272,8 @@ mod tests {
#[test]
fn test_cross_connection_smaller_node_wins_outbound() {
let node_a = make_node_id(1); // smaller
let node_b = make_node_id(2); // larger
let node_a = make_node_addr(1); // smaller
let node_b = make_node_addr(2); // larger
// Node A's perspective
assert!(cross_connection_winner(&node_a, &node_b, true)); // A's outbound wins
@@ -286,8 +286,8 @@ mod tests {
#[test]
fn test_cross_connection_symmetric() {
let node_a = make_node_id(1);
let node_b = make_node_id(2);
let node_a = make_node_addr(1);
let node_b = make_node_addr(2);
// A's outbound = B's inbound
let a_outbound_wins = cross_connection_winner(&node_a, &node_b, true);
@@ -332,11 +332,11 @@ mod tests {
#[test]
fn test_promotion_result_promoted() {
let identity = make_peer_identity();
let node_id = *identity.node_id();
let result = PromotionResult::Promoted(node_id);
let node_addr = *identity.node_addr();
let result = PromotionResult::Promoted(node_addr);
assert!(result.node_id().is_some());
assert_eq!(result.node_id(), Some(node_id));
assert!(result.node_addr().is_some());
assert_eq!(result.node_addr(), Some(node_addr));
assert!(!result.should_close_this_connection());
assert!(result.link_to_close().is_none());
}
@@ -347,7 +347,7 @@ mod tests {
winner_link_id: LinkId::new(1),
};
assert!(result.node_id().is_none());
assert!(result.node_addr().is_none());
assert!(result.should_close_this_connection());
assert!(result.link_to_close().is_none()); // Caller closes their own
}
@@ -355,37 +355,37 @@ mod tests {
#[test]
fn test_promotion_result_cross_won() {
let identity = make_peer_identity();
let node_id = *identity.node_id();
let node_addr = *identity.node_addr();
let result = PromotionResult::CrossConnectionWon {
loser_link_id: LinkId::new(1),
node_id,
node_addr,
};
assert!(result.node_id().is_some());
assert_eq!(result.node_id(), Some(node_id));
assert!(result.node_addr().is_some());
assert_eq!(result.node_addr(), Some(node_addr));
assert!(!result.should_close_this_connection());
assert_eq!(result.link_to_close(), Some(LinkId::new(1)));
}
#[test]
fn test_peer_slot_node_id() {
fn test_peer_slot_node_addr() {
// Outbound connection knows expected identity
let identity = make_peer_identity();
let expected_node_id = *identity.node_id();
let expected_node_addr = *identity.node_addr();
let conn = PeerConnection::outbound(LinkId::new(1), identity, 1000);
let slot = PeerSlot::Connecting(conn);
assert_eq!(slot.node_id(), Some(&expected_node_id));
assert_eq!(slot.node_addr(), Some(&expected_node_addr));
// Inbound connection doesn't know identity yet
let conn_inbound = PeerConnection::inbound(LinkId::new(2), 2000);
let slot_inbound = PeerSlot::Connecting(conn_inbound);
assert!(slot_inbound.node_id().is_none());
assert!(slot_inbound.node_addr().is_none());
// Active peer always knows identity
let identity2 = make_peer_identity();
let active_node_id = *identity2.node_id();
let active_node_addr = *identity2.node_addr();
let peer = ActivePeer::new(identity2, LinkId::new(3), 3000);
let slot_active = PeerSlot::Active(peer);
assert_eq!(slot_active.node_id(), Some(&active_node_id));
assert_eq!(slot_active.node_addr(), Some(&active_node_addr));
}
}

View File

@@ -22,7 +22,7 @@
use crate::bloom::BloomFilter;
use crate::tree::{ParentDeclaration, TreeCoordinate};
use crate::{FipsAddress, NodeId};
use crate::{FipsAddress, NodeAddr};
use secp256k1::schnorr::Signature;
use std::fmt;
use thiserror::Error;
@@ -378,9 +378,9 @@ pub struct LookupRequest {
/// Unique request identifier.
pub request_id: u64,
/// Node we're looking for.
pub target: NodeId,
pub target: NodeAddr,
/// Who's asking (for response routing).
pub origin: NodeId,
pub origin: NodeAddr,
/// Origin's coordinates (for return path).
pub origin_coords: TreeCoordinate,
/// Remaining propagation hops.
@@ -393,8 +393,8 @@ impl LookupRequest {
/// Create a new lookup request.
pub fn new(
request_id: u64,
target: NodeId,
origin: NodeId,
target: NodeAddr,
origin: NodeAddr,
origin_coords: TreeCoordinate,
ttl: u8,
) -> Self {
@@ -412,8 +412,8 @@ impl LookupRequest {
/// Generate a new request with a random ID.
pub fn generate(
target: NodeId,
origin: NodeId,
target: NodeAddr,
origin: NodeAddr,
origin_coords: TreeCoordinate,
ttl: u8,
) -> Self {
@@ -425,12 +425,12 @@ impl LookupRequest {
/// Decrement TTL and add self to visited.
///
/// Returns false if TTL was already 0.
pub fn forward(&mut self, my_node_id: &NodeId) -> bool {
pub fn forward(&mut self, my_node_addr: &NodeAddr) -> bool {
if self.ttl == 0 {
return false;
}
self.ttl -= 1;
self.visited.insert(my_node_id);
self.visited.insert(my_node_addr);
true
}
@@ -440,8 +440,8 @@ impl LookupRequest {
}
/// Check if a node was already visited.
pub fn was_visited(&self, node_id: &NodeId) -> bool {
self.visited.contains(node_id)
pub fn was_visited(&self, node_addr: &NodeAddr) -> bool {
self.visited.contains(node_addr)
}
}
@@ -453,7 +453,7 @@ pub struct LookupResponse {
/// Echoed request identifier.
pub request_id: u64,
/// The target node.
pub target: NodeId,
pub target: NodeAddr,
/// Target's coordinates in the tree.
pub target_coords: TreeCoordinate,
/// Proof that target authorized this response (signature over request).
@@ -464,7 +464,7 @@ impl LookupResponse {
/// Create a new lookup response.
pub fn new(
request_id: u64,
target: NodeId,
target: NodeAddr,
target_coords: TreeCoordinate,
proof: Signature,
) -> Self {
@@ -479,7 +479,7 @@ impl LookupResponse {
/// Get the bytes that should be signed as proof.
///
/// Format: request_id (8) || target (32)
pub fn proof_bytes(request_id: u64, target: &NodeId) -> Vec<u8> {
pub fn proof_bytes(request_id: u64, target: &NodeAddr) -> Vec<u8> {
let mut bytes = Vec::with_capacity(40);
bytes.extend_from_slice(&request_id.to_le_bytes());
bytes.extend_from_slice(target.as_bytes());
@@ -803,12 +803,12 @@ pub struct CoordsRequired {
/// Destination that couldn't be routed.
pub dest_addr: FipsAddress,
/// Router reporting the miss.
pub reporter: NodeId,
pub reporter: NodeAddr,
}
impl CoordsRequired {
/// Create a new CoordsRequired error.
pub fn new(dest_addr: FipsAddress, reporter: NodeId) -> Self {
pub fn new(dest_addr: FipsAddress, reporter: NodeAddr) -> Self {
Self { dest_addr, reporter }
}
}
@@ -823,14 +823,14 @@ pub struct PathBroken {
/// Destination that couldn't be reached.
pub dest_addr: FipsAddress,
/// Node that detected the failure.
pub reporter: NodeId,
pub reporter: NodeAddr,
/// Optional: last known coordinates of destination.
pub last_known_coords: Option<TreeCoordinate>,
}
impl PathBroken {
/// Create a new PathBroken error.
pub fn new(original_src: FipsAddress, dest_addr: FipsAddress, reporter: NodeId) -> Self {
pub fn new(original_src: FipsAddress, dest_addr: FipsAddress, reporter: NodeAddr) -> Self {
Self {
original_src,
dest_addr,
@@ -850,10 +850,10 @@ impl PathBroken {
mod tests {
use super::*;
fn make_node_id(val: u8) -> NodeId {
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
bytes[0] = val;
NodeId::from_bytes(bytes)
NodeAddr::from_bytes(bytes)
}
fn make_address(val: u8) -> FipsAddress {
@@ -863,7 +863,7 @@ mod tests {
}
fn make_coords(ids: &[u8]) -> TreeCoordinate {
TreeCoordinate::new(ids.iter().map(|&v| make_node_id(v)).collect()).unwrap()
TreeCoordinate::new(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
}
// ===== HandshakeMessageType Tests =====
@@ -1042,10 +1042,10 @@ mod tests {
#[test]
fn test_lookup_request_forward() {
let target = make_node_id(1);
let origin = make_node_id(2);
let target = make_node_addr(1);
let origin = make_node_addr(2);
let coords = make_coords(&[2, 0]);
let forwarder = make_node_id(3);
let forwarder = make_node_addr(3);
let mut request = LookupRequest::new(123, target, origin, coords, 5);
@@ -1060,21 +1060,21 @@ mod tests {
#[test]
fn test_lookup_request_ttl_exhausted() {
let target = make_node_id(1);
let origin = make_node_id(2);
let target = make_node_addr(1);
let origin = make_node_addr(2);
let coords = make_coords(&[2, 0]);
let mut request = LookupRequest::new(123, target, origin, coords, 1);
assert!(request.forward(&make_node_id(3)));
assert!(request.forward(&make_node_addr(3)));
assert!(!request.can_forward());
assert!(!request.forward(&make_node_id(4)));
assert!(!request.forward(&make_node_addr(4)));
}
#[test]
fn test_lookup_request_generate() {
let target = make_node_id(1);
let origin = make_node_id(2);
let target = make_node_addr(1);
let origin = make_node_addr(2);
let coords = make_coords(&[2, 0]);
let req1 = LookupRequest::generate(target, origin, coords.clone(), 5);
@@ -1088,7 +1088,7 @@ mod tests {
#[test]
fn test_lookup_response_proof_bytes() {
let target = make_node_id(42);
let target = make_node_addr(42);
let bytes = LookupResponse::proof_bytes(12345, &target);
assert_eq!(bytes.len(), 40); // 8 + 32
@@ -1166,17 +1166,17 @@ mod tests {
#[test]
fn test_coords_required() {
let err = CoordsRequired::new(make_address(1), make_node_id(2));
let err = CoordsRequired::new(make_address(1), make_node_addr(2));
assert_eq!(err.dest_addr, make_address(1));
assert_eq!(err.reporter, make_node_id(2));
assert_eq!(err.reporter, make_node_addr(2));
}
// ===== PathBroken Tests =====
#[test]
fn test_path_broken() {
let err = PathBroken::new(make_address(1), make_address(2), make_node_id(3))
let err = PathBroken::new(make_address(1), make_address(2), make_node_addr(3))
.with_last_coords(make_coords(&[2, 0]));
assert!(err.last_known_coords.is_some());
@@ -1186,14 +1186,14 @@ mod tests {
#[test]
fn test_tree_announce() {
let node = make_node_id(1);
let parent = make_node_id(2);
let node = make_node_addr(1);
let parent = make_node_addr(2);
let decl = ParentDeclaration::new(node, parent, 1, 1000);
let ancestry = make_coords(&[1, 2, 0]);
let announce = TreeAnnounce::new(decl, ancestry);
assert_eq!(announce.declaration.node_id(), &node);
assert_eq!(announce.declaration.node_addr(), &node);
assert_eq!(announce.ancestry.depth(), 2);
}
}

View File

@@ -4,7 +4,7 @@
//! The spanning tree provides a routing topology where each node maintains
//! a path to a common root, enabling greedy distance-based routing.
use crate::{Identity, IdentityError, NodeId};
use crate::{Identity, IdentityError, NodeAddr};
use secp256k1::schnorr::Signature;
use secp256k1::XOnlyPublicKey;
use std::collections::HashMap;
@@ -21,13 +21,13 @@ pub enum TreeError {
AncestryNotToRoot,
#[error("signature verification failed for node {0:?}")]
InvalidSignature(NodeId),
InvalidSignature(NodeAddr),
#[error("sequence number regression: got {got}, expected > {expected}")]
SequenceRegression { got: u64, expected: u64 },
#[error("parent not in peers: {0:?}")]
ParentNotPeer(NodeId),
ParentNotPeer(NodeAddr),
#[error("identity error: {0}")]
Identity(#[from] IdentityError),
@@ -37,14 +37,14 @@ pub enum TreeError {
///
/// Each node periodically announces its parent selection. The declaration
/// includes a monotonic sequence number for freshness and a signature
/// for authenticity. When `parent_id == node_id`, the node declares itself
/// for authenticity. When `parent_id == node_addr`, the node declares itself
/// as a root candidate.
#[derive(Clone)]
pub struct ParentDeclaration {
/// The node making this declaration.
node_id: NodeId,
/// The selected parent (equals node_id if self-declaring as root).
parent_id: NodeId,
node_addr: NodeAddr,
/// The selected parent (equals node_addr if self-declaring as root).
parent_id: NodeAddr,
/// Monotonically increasing sequence number.
sequence: u64,
/// Timestamp when this declaration was created (Unix seconds).
@@ -57,9 +57,9 @@ impl ParentDeclaration {
/// Create a new unsigned parent declaration.
///
/// The declaration must be signed before transmission using `set_signature()`.
pub fn new(node_id: NodeId, parent_id: NodeId, sequence: u64, timestamp: u64) -> Self {
pub fn new(node_addr: NodeAddr, parent_id: NodeAddr, sequence: u64, timestamp: u64) -> Self {
Self {
node_id,
node_addr,
parent_id,
sequence,
timestamp,
@@ -68,20 +68,20 @@ impl ParentDeclaration {
}
/// Create a self-declaration (node is root candidate).
pub fn self_root(node_id: NodeId, sequence: u64, timestamp: u64) -> Self {
Self::new(node_id, node_id, sequence, timestamp)
pub fn self_root(node_addr: NodeAddr, sequence: u64, timestamp: u64) -> Self {
Self::new(node_addr, node_addr, sequence, timestamp)
}
/// Create a declaration with a pre-computed signature.
pub fn with_signature(
node_id: NodeId,
parent_id: NodeId,
node_addr: NodeAddr,
parent_id: NodeAddr,
sequence: u64,
timestamp: u64,
signature: Signature,
) -> Self {
Self {
node_id,
node_addr,
parent_id,
sequence,
timestamp,
@@ -90,12 +90,12 @@ impl ParentDeclaration {
}
/// Get the declaring node's ID.
pub fn node_id(&self) -> &NodeId {
&self.node_id
pub fn node_addr(&self) -> &NodeAddr {
&self.node_addr
}
/// Get the parent node's ID.
pub fn parent_id(&self) -> &NodeId {
pub fn parent_id(&self) -> &NodeAddr {
&self.parent_id
}
@@ -121,11 +121,11 @@ impl ParentDeclaration {
/// Sign this declaration with the given identity.
///
/// The identity's node_id must match this declaration's node_id.
/// Returns an error if the node_ids don't match.
/// The identity's node_addr must match this declaration's node_addr.
/// Returns an error if the node_addrs don't match.
pub fn sign(&mut self, identity: &Identity) -> Result<(), TreeError> {
if identity.node_id() != &self.node_id {
return Err(TreeError::InvalidSignature(self.node_id));
if identity.node_addr() != &self.node_addr {
return Err(TreeError::InvalidSignature(self.node_addr));
}
let signature = identity.sign(&self.signing_bytes());
self.signature = Some(signature);
@@ -134,7 +134,7 @@ impl ParentDeclaration {
/// Check if this is a root declaration (parent == self).
pub fn is_root(&self) -> bool {
self.node_id == self.parent_id
self.node_addr == self.parent_id
}
/// Check if this declaration is signed.
@@ -144,10 +144,10 @@ impl ParentDeclaration {
/// Get the bytes that should be signed.
///
/// Format: node_id (32) || parent_id (32) || sequence (8) || timestamp (8)
/// Format: node_addr (32) || parent_id (32) || sequence (8) || timestamp (8)
pub fn signing_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(80);
bytes.extend_from_slice(self.node_id.as_bytes());
bytes.extend_from_slice(self.node_addr.as_bytes());
bytes.extend_from_slice(self.parent_id.as_bytes());
bytes.extend_from_slice(&self.sequence.to_le_bytes());
bytes.extend_from_slice(&self.timestamp.to_le_bytes());
@@ -161,13 +161,13 @@ impl ParentDeclaration {
let signature = self
.signature
.as_ref()
.ok_or(TreeError::InvalidSignature(self.node_id))?;
.ok_or(TreeError::InvalidSignature(self.node_addr))?;
let secp = secp256k1::Secp256k1::verification_only();
let hash = self.signing_hash();
secp.verify_schnorr(signature, &hash, pubkey)
.map_err(|_| TreeError::InvalidSignature(self.node_id))
.map_err(|_| TreeError::InvalidSignature(self.node_addr))
}
/// Compute the SHA-256 hash of the signing bytes.
@@ -187,7 +187,7 @@ impl ParentDeclaration {
impl fmt::Debug for ParentDeclaration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ParentDeclaration")
.field("node_id", &self.node_id)
.field("node_addr", &self.node_addr)
.field("parent_id", &self.parent_id)
.field("sequence", &self.sequence)
.field("is_root", &self.is_root())
@@ -198,7 +198,7 @@ impl fmt::Debug for ParentDeclaration {
impl PartialEq for ParentDeclaration {
fn eq(&self, other: &Self) -> bool {
self.node_id == other.node_id
self.node_addr == other.node_addr
&& self.parent_id == other.parent_id
&& self.sequence == other.sequence
&& self.timestamp == other.timestamp
@@ -216,13 +216,13 @@ impl Eq for ParentDeclaration {}
/// Two nodes can compute the hops between them by finding their lowest
/// common ancestor (LCA) in the tree.
#[derive(Clone, PartialEq, Eq)]
pub struct TreeCoordinate(Vec<NodeId>);
pub struct TreeCoordinate(Vec<NodeAddr>);
impl TreeCoordinate {
/// Create a coordinate from a path (self to root).
///
/// The path must be non-empty and ordered from the node to the root.
pub fn new(path: Vec<NodeId>) -> Result<Self, TreeError> {
pub fn new(path: Vec<NodeAddr>) -> Result<Self, TreeError> {
if path.is_empty() {
return Err(TreeError::EmptyCoordinate);
}
@@ -230,22 +230,22 @@ impl TreeCoordinate {
}
/// Create a coordinate for a root node.
pub fn root(node_id: NodeId) -> Self {
Self(vec![node_id])
pub fn root(node_addr: NodeAddr) -> Self {
Self(vec![node_addr])
}
/// The node this coordinate belongs to (first element).
pub fn node_id(&self) -> &NodeId {
pub fn node_addr(&self) -> &NodeAddr {
&self.0[0]
}
/// The root of the tree (last element).
pub fn root_id(&self) -> &NodeId {
pub fn root_id(&self) -> &NodeAddr {
self.0.last().expect("coordinate never empty")
}
/// The immediate parent (second element, or self if root).
pub fn parent_id(&self) -> &NodeId {
pub fn parent_id(&self) -> &NodeAddr {
self.0.get(1).unwrap_or(&self.0[0])
}
@@ -255,7 +255,7 @@ impl TreeCoordinate {
}
/// The full ancestry path.
pub fn path(&self) -> &[NodeId] {
pub fn path(&self) -> &[NodeAddr] {
&self.0
}
@@ -302,7 +302,7 @@ impl TreeCoordinate {
}
/// Get the lowest common ancestor node ID.
pub fn lca(&self, other: &TreeCoordinate) -> Option<&NodeId> {
pub fn lca(&self, other: &TreeCoordinate) -> Option<&NodeAddr> {
let self_rev: Vec<_> = self.0.iter().rev().collect();
let other_rev: Vec<_> = other.0.iter().rev().collect();
@@ -318,19 +318,19 @@ impl TreeCoordinate {
}
/// Check if `other` is an ancestor (appears in our path after self).
pub fn has_ancestor(&self, other: &NodeId) -> bool {
pub fn has_ancestor(&self, other: &NodeAddr) -> bool {
self.0.iter().skip(1).any(|id| id == other)
}
/// Check if `other` is in our ancestry (including self).
pub fn contains(&self, other: &NodeId) -> bool {
pub fn contains(&self, other: &NodeAddr) -> bool {
self.0.iter().any(|id| id == other)
}
/// Get the ancestor at a specific depth from self.
///
/// `ancestor_at(0)` returns self, `ancestor_at(1)` returns parent, etc.
pub fn ancestor_at(&self, depth: usize) -> Option<&NodeId> {
pub fn ancestor_at(&self, depth: usize) -> Option<&NodeAddr> {
self.0.get(depth)
}
}
@@ -349,8 +349,8 @@ impl fmt::Debug for TreeCoordinate {
}
}
impl AsRef<[NodeId]> for TreeCoordinate {
fn as_ref(&self) -> &[NodeId] {
impl AsRef<[NodeAddr]> for TreeCoordinate {
fn as_ref(&self) -> &[NodeAddr] {
&self.0
}
}
@@ -361,46 +361,46 @@ impl AsRef<[NodeId]> for TreeCoordinate {
/// tree positions. State is bounded by O(P × D) where P is peer count
/// and D is tree depth.
pub struct TreeState {
/// This node's NodeId.
my_node_id: NodeId,
/// This node's NodeAddr.
my_node_addr: NodeAddr,
/// This node's current parent declaration.
my_declaration: ParentDeclaration,
/// This node's current coordinates (computed from declaration chain).
my_coords: TreeCoordinate,
/// The current elected root (smallest reachable node_id).
root: NodeId,
/// The current elected root (smallest reachable node_addr).
root: NodeAddr,
/// Each peer's most recent parent declaration.
peer_declarations: HashMap<NodeId, ParentDeclaration>,
peer_declarations: HashMap<NodeAddr, ParentDeclaration>,
/// Each peer's full ancestry to root.
peer_ancestry: HashMap<NodeId, TreeCoordinate>,
peer_ancestry: HashMap<NodeAddr, TreeCoordinate>,
}
impl TreeState {
/// Create initial tree state for a node (as root candidate).
///
/// The node starts as its own root until it learns of a smaller node_id.
/// The node starts as its own root until it learns of a smaller node_addr.
/// Initial sequence is 1 per protocol spec; timestamp is current Unix time.
pub fn new(my_node_id: NodeId) -> Self {
pub fn new(my_node_addr: NodeAddr) -> Self {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let my_declaration = ParentDeclaration::self_root(my_node_id, 1, timestamp);
let my_coords = TreeCoordinate::root(my_node_id);
let my_declaration = ParentDeclaration::self_root(my_node_addr, 1, timestamp);
let my_coords = TreeCoordinate::root(my_node_addr);
Self {
my_node_id,
my_node_addr,
my_declaration,
my_coords,
root: my_node_id,
root: my_node_addr,
peer_declarations: HashMap::new(),
peer_ancestry: HashMap::new(),
}
}
/// Get this node's NodeId.
pub fn my_node_id(&self) -> &NodeId {
&self.my_node_id
/// Get this node's NodeAddr.
pub fn my_node_addr(&self) -> &NodeAddr {
&self.my_node_addr
}
/// Get this node's current declaration.
@@ -414,22 +414,22 @@ impl TreeState {
}
/// Get the current root.
pub fn root(&self) -> &NodeId {
pub fn root(&self) -> &NodeAddr {
&self.root
}
/// Check if this node is currently the root.
pub fn is_root(&self) -> bool {
self.root == self.my_node_id
self.root == self.my_node_addr
}
/// Get coordinates for a peer, if known.
pub fn peer_coords(&self, peer_id: &NodeId) -> Option<&TreeCoordinate> {
pub fn peer_coords(&self, peer_id: &NodeAddr) -> Option<&TreeCoordinate> {
self.peer_ancestry.get(peer_id)
}
/// Get declaration for a peer, if known.
pub fn peer_declaration(&self, peer_id: &NodeId) -> Option<&ParentDeclaration> {
pub fn peer_declaration(&self, peer_id: &NodeAddr) -> Option<&ParentDeclaration> {
self.peer_declarations.get(peer_id)
}
@@ -439,7 +439,7 @@ impl TreeState {
}
/// Iterate over all peer node IDs.
pub fn peer_ids(&self) -> impl Iterator<Item = &NodeId> {
pub fn peer_ids(&self) -> impl Iterator<Item = &NodeAddr> {
self.peer_declarations.keys()
}
@@ -451,7 +451,7 @@ impl TreeState {
declaration: ParentDeclaration,
ancestry: TreeCoordinate,
) -> bool {
let peer_id = *declaration.node_id();
let peer_id = *declaration.node_addr();
// Check if this is a fresh update
if let Some(existing) = self.peer_declarations.get(&peer_id)
@@ -466,7 +466,7 @@ impl TreeState {
}
/// Remove a peer from the tree state.
pub fn remove_peer(&mut self, peer_id: &NodeId) {
pub fn remove_peer(&mut self, peer_id: &NodeAddr) {
self.peer_declarations.remove(peer_id);
self.peer_ancestry.remove(peer_id);
}
@@ -474,23 +474,23 @@ impl TreeState {
/// Update this node's parent selection.
///
/// Call this when switching parents. Updates the declaration and coordinates.
pub fn set_parent(&mut self, parent_id: NodeId, sequence: u64, timestamp: u64) {
self.my_declaration = ParentDeclaration::new(self.my_node_id, parent_id, sequence, timestamp);
pub fn set_parent(&mut self, parent_id: NodeAddr, sequence: u64, timestamp: u64) {
self.my_declaration = ParentDeclaration::new(self.my_node_addr, parent_id, sequence, timestamp);
// Coordinates will be recomputed when ancestry is available
}
/// Update this node's coordinates based on current parent's ancestry.
pub fn recompute_coords(&mut self) {
if self.my_declaration.is_root() {
self.my_coords = TreeCoordinate::root(self.my_node_id);
self.root = self.my_node_id;
self.my_coords = TreeCoordinate::root(self.my_node_addr);
self.root = self.my_node_addr;
return;
}
let parent_id = self.my_declaration.parent_id();
if let Some(parent_coords) = self.peer_ancestry.get(parent_id) {
// Our coords = [self] ++ parent_coords
let mut path = vec![self.my_node_id];
let mut path = vec![self.my_node_addr];
path.extend_from_slice(parent_coords.path());
self.my_coords = TreeCoordinate::new(path).expect("non-empty path");
self.root = *self.my_coords.root_id();
@@ -498,7 +498,7 @@ impl TreeState {
}
/// Calculate tree distance to a peer.
pub fn distance_to_peer(&self, peer_id: &NodeId) -> Option<usize> {
pub fn distance_to_peer(&self, peer_id: &NodeAddr) -> Option<usize> {
self.peer_ancestry
.get(peer_id)
.map(|coords| self.my_coords.distance_to(coords))
@@ -508,7 +508,7 @@ impl TreeState {
///
/// Returns the peer that minimizes tree distance to the destination.
/// This is a stub - full implementation requires greedy routing logic.
pub fn find_next_hop(&self, _dest_coords: &TreeCoordinate) -> Option<NodeId> {
pub fn find_next_hop(&self, _dest_coords: &TreeCoordinate) -> Option<NodeAddr> {
// Stub: would implement greedy tree routing
None
}
@@ -516,14 +516,14 @@ impl TreeState {
/// Check if a parent switch to `candidate` would be beneficial.
///
/// This is a stub - full implementation requires policy decisions.
pub fn should_switch_parent(&self, _candidate: &NodeId) -> bool {
pub fn should_switch_parent(&self, _candidate: &NodeAddr) -> bool {
// Stub: would evaluate parent switch criteria
false
}
/// Sign this node's declaration with the given identity.
///
/// The identity's node_id must match this TreeState's node_id.
/// The identity's node_addr must match this TreeState's node_addr.
pub fn sign_declaration(&mut self, identity: &Identity) -> Result<(), TreeError> {
self.my_declaration.sign(identity)
}
@@ -537,7 +537,7 @@ impl TreeState {
impl fmt::Debug for TreeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TreeState")
.field("my_node_id", &self.my_node_id)
.field("my_node_addr", &self.my_node_addr)
.field("root", &self.root)
.field("is_root", &self.is_root())
.field("depth", &self.my_coords.depth())
@@ -550,37 +550,37 @@ impl fmt::Debug for TreeState {
mod tests {
use super::*;
fn make_node_id(val: u8) -> NodeId {
fn make_node_addr(val: u8) -> NodeAddr {
let mut bytes = [0u8; 32];
bytes[0] = val;
NodeId::from_bytes(bytes)
NodeAddr::from_bytes(bytes)
}
// ===== TreeCoordinate Tests =====
#[test]
fn test_tree_coordinate_root() {
let root_id = make_node_id(1);
let root_id = make_node_addr(1);
let coord = TreeCoordinate::root(root_id);
assert!(coord.is_root());
assert_eq!(coord.depth(), 0);
assert_eq!(coord.node_id(), &root_id);
assert_eq!(coord.node_addr(), &root_id);
assert_eq!(coord.root_id(), &root_id);
assert_eq!(coord.parent_id(), &root_id);
}
#[test]
fn test_tree_coordinate_path() {
let node = make_node_id(1);
let parent = make_node_id(2);
let root = make_node_id(3);
let node = make_node_addr(1);
let parent = make_node_addr(2);
let root = make_node_addr(3);
let coord = TreeCoordinate::new(vec![node, parent, root]).unwrap();
assert!(!coord.is_root());
assert_eq!(coord.depth(), 2);
assert_eq!(coord.node_id(), &node);
assert_eq!(coord.node_addr(), &node);
assert_eq!(coord.parent_id(), &parent);
assert_eq!(coord.root_id(), &root);
}
@@ -593,7 +593,7 @@ mod tests {
#[test]
fn test_tree_distance_same_node() {
let node = make_node_id(1);
let node = make_node_addr(1);
let coord = TreeCoordinate::root(node);
assert_eq!(coord.distance_to(&coord), 0);
@@ -601,9 +601,9 @@ mod tests {
#[test]
fn test_tree_distance_siblings() {
let root = make_node_id(0);
let a = make_node_id(1);
let b = make_node_id(2);
let root = make_node_addr(0);
let a = make_node_addr(1);
let b = make_node_addr(2);
let coord_a = TreeCoordinate::new(vec![a, root]).unwrap();
let coord_b = TreeCoordinate::new(vec![b, root]).unwrap();
@@ -614,9 +614,9 @@ mod tests {
#[test]
fn test_tree_distance_ancestor() {
let root = make_node_id(0);
let parent = make_node_id(1);
let child = make_node_id(2);
let root = make_node_addr(0);
let parent = make_node_addr(1);
let child = make_node_addr(2);
let coord_parent = TreeCoordinate::new(vec![parent, root]).unwrap();
let coord_child = TreeCoordinate::new(vec![child, parent, root]).unwrap();
@@ -633,11 +633,11 @@ mod tests {
// a b
// / \
// c d
let root = make_node_id(0);
let a = make_node_id(1);
let b = make_node_id(2);
let c = make_node_id(3);
let d = make_node_id(4);
let root = make_node_addr(0);
let a = make_node_addr(1);
let b = make_node_addr(2);
let c = make_node_addr(3);
let d = make_node_addr(4);
let coord_c = TreeCoordinate::new(vec![c, a, root]).unwrap();
let coord_d = TreeCoordinate::new(vec![d, b, root]).unwrap();
@@ -648,8 +648,8 @@ mod tests {
#[test]
fn test_tree_distance_different_roots() {
let root1 = make_node_id(1);
let root2 = make_node_id(2);
let root1 = make_node_addr(1);
let root2 = make_node_addr(2);
let coord1 = TreeCoordinate::root(root1);
let coord2 = TreeCoordinate::root(root2);
@@ -659,9 +659,9 @@ mod tests {
#[test]
fn test_has_ancestor() {
let root = make_node_id(0);
let parent = make_node_id(1);
let child = make_node_id(2);
let root = make_node_addr(0);
let parent = make_node_addr(1);
let child = make_node_addr(2);
let coord = TreeCoordinate::new(vec![child, parent, root]).unwrap();
@@ -672,10 +672,10 @@ mod tests {
#[test]
fn test_contains() {
let root = make_node_id(0);
let parent = make_node_id(1);
let child = make_node_id(2);
let other = make_node_id(99);
let root = make_node_addr(0);
let parent = make_node_addr(1);
let child = make_node_addr(2);
let other = make_node_addr(99);
let coord = TreeCoordinate::new(vec![child, parent, root]).unwrap();
@@ -687,9 +687,9 @@ mod tests {
#[test]
fn test_ancestor_at() {
let root = make_node_id(0);
let parent = make_node_id(1);
let child = make_node_id(2);
let root = make_node_addr(0);
let parent = make_node_addr(1);
let child = make_node_addr(2);
let coord = TreeCoordinate::new(vec![child, parent, root]).unwrap();
@@ -701,11 +701,11 @@ mod tests {
#[test]
fn test_lca() {
let root = make_node_id(0);
let a = make_node_id(1);
let b = make_node_id(2);
let c = make_node_id(3);
let d = make_node_id(4);
let root = make_node_addr(0);
let a = make_node_addr(1);
let b = make_node_addr(2);
let c = make_node_addr(3);
let d = make_node_addr(4);
// c under a, d under b, both under root
let coord_c = TreeCoordinate::new(vec![c, a, root]).unwrap();
@@ -722,12 +722,12 @@ mod tests {
#[test]
fn test_parent_declaration_new() {
let node = make_node_id(1);
let parent = make_node_id(2);
let node = make_node_addr(1);
let parent = make_node_addr(2);
let decl = ParentDeclaration::new(node, parent, 1, 1000);
assert_eq!(decl.node_id(), &node);
assert_eq!(decl.node_addr(), &node);
assert_eq!(decl.parent_id(), &parent);
assert_eq!(decl.sequence(), 1);
assert_eq!(decl.timestamp(), 1000);
@@ -737,18 +737,18 @@ mod tests {
#[test]
fn test_parent_declaration_self_root() {
let node = make_node_id(1);
let node = make_node_addr(1);
let decl = ParentDeclaration::self_root(node, 5, 2000);
assert!(decl.is_root());
assert_eq!(decl.node_id(), decl.parent_id());
assert_eq!(decl.node_addr(), decl.parent_id());
}
#[test]
fn test_parent_declaration_freshness() {
let node = make_node_id(1);
let parent = make_node_id(2);
let node = make_node_addr(1);
let parent = make_node_addr(2);
let old_decl = ParentDeclaration::new(node, parent, 1, 1000);
let new_decl = ParentDeclaration::new(node, parent, 2, 2000);
@@ -760,8 +760,8 @@ mod tests {
#[test]
fn test_parent_declaration_signing_bytes() {
let node = make_node_id(1);
let parent = make_node_id(2);
let node = make_node_addr(1);
let parent = make_node_addr(2);
let decl = ParentDeclaration::new(node, parent, 100, 1234567890);
let bytes = decl.signing_bytes();
@@ -778,8 +778,8 @@ mod tests {
#[test]
fn test_parent_declaration_equality() {
let node = make_node_id(1);
let parent = make_node_id(2);
let node = make_node_addr(1);
let parent = make_node_addr(2);
let decl1 = ParentDeclaration::new(node, parent, 1, 1000);
let decl2 = ParentDeclaration::new(node, parent, 1, 1000);
@@ -793,10 +793,10 @@ mod tests {
#[test]
fn test_tree_state_new() {
let node = make_node_id(1);
let node = make_node_addr(1);
let state = TreeState::new(node);
assert_eq!(state.my_node_id(), &node);
assert_eq!(state.my_node_addr(), &node);
assert!(state.is_root());
assert_eq!(state.root(), &node);
assert_eq!(state.my_coords().depth(), 0);
@@ -805,11 +805,11 @@ mod tests {
#[test]
fn test_tree_state_update_peer() {
let my_node = make_node_id(0);
let my_node = make_node_addr(0);
let mut state = TreeState::new(my_node);
let peer = make_node_id(1);
let root = make_node_id(2);
let peer = make_node_addr(1);
let root = make_node_addr(2);
let decl = ParentDeclaration::new(peer, root, 1, 1000);
let coords = TreeCoordinate::new(vec![peer, root]).unwrap();
@@ -830,11 +830,11 @@ mod tests {
#[test]
fn test_tree_state_remove_peer() {
let my_node = make_node_id(0);
let my_node = make_node_addr(0);
let mut state = TreeState::new(my_node);
let peer = make_node_id(1);
let root = make_node_id(2);
let peer = make_node_addr(1);
let root = make_node_addr(2);
let decl = ParentDeclaration::new(peer, root, 1, 1000);
let coords = TreeCoordinate::new(vec![peer, root]).unwrap();
@@ -849,10 +849,10 @@ mod tests {
#[test]
fn test_tree_state_distance_to_peer() {
let my_node = make_node_id(0);
let my_node = make_node_addr(0);
let mut state = TreeState::new(my_node);
let peer = make_node_id(1);
let peer = make_node_addr(1);
// Both are roots in their own trees initially - different roots
let peer_coords = TreeCoordinate::root(peer);
@@ -863,7 +863,7 @@ mod tests {
assert_eq!(state.distance_to_peer(&peer), Some(usize::MAX));
// If they share a root, distance should be finite
let shared_root = make_node_id(99);
let shared_root = make_node_addr(99);
// Update my state to have shared root
state.set_parent(shared_root, 1, 1000);
@@ -883,11 +883,11 @@ mod tests {
#[test]
fn test_tree_state_peer_ids() {
let my_node = make_node_id(0);
let my_node = make_node_addr(0);
let mut state = TreeState::new(my_node);
let peer1 = make_node_id(1);
let peer2 = make_node_id(2);
let peer1 = make_node_addr(1);
let peer2 = make_node_addr(2);
state.update_peer(
ParentDeclaration::self_root(peer1, 1, 1000),