How to Use MemoryLookup for Peer Address Management in iroh
MemoryLookup provides a thread-safe, in-memory store for peer addressing information that you can populate at runtime and inject into iroh endpoints to resolve EndpointId values to relay URLs and direct transport addresses.
MemoryLookup serves as the in-memory implementation of iroh’s address-lookup abstraction, providing a mechanism to manage EndpointId-to-address mappings without external dependencies. Located in iroh/src/address_lookup/memory.rs, this struct enables applications to inject relay URLs, direct transport addresses, and user data obtained from tickets, configuration files, or discovery services directly into the connection establishment flow.
Initializing MemoryLookup and Attaching to Endpoints
Create a MemoryLookup using the new constructor, or use with_provenance to attach a custom source identifier for debugging. Attach the lookup to an endpoint through the Builder::address_lookup method before calling bind.
use iroh::{
address_lookup::memory::MemoryLookup,
endpoint::{presets, Builder},
};
use iroh_base::{SecretKey, TransportAddr};
let lookup = MemoryLookup::new(); // ← empty lookup
let ep = Builder::new(presets::Minimal) // ← pick a preset
.address_lookup(lookup.clone()) // ← inject the lookup
.bind()
.await?; // ← creates the endpoint
You can customize the provenance string—useful for tracing which component supplied an address—via the with_provenance method:
let lookup = MemoryLookup::with_provenance("my_app");
let mut stream = lookup.resolve(some_endpoint_id).unwrap();
let item = stream.next().await.unwrap()?;
assert_eq!(item.provenance(), "my_app");
Adding and Updating Peer Addresses
MemoryLookup provides distinct methods for different update semantics. Use add_endpoint_info to merge new addressing data with existing entries, or set_endpoint_info to perform a complete replacement and retrieve the previous value.
Augmenting Existing Entries
The add_endpoint_info method merges new transport addresses with any existing entry while overwriting the relay URL. This corresponds to the implementation in lines 184-198 of [iroh/src/address_lookup/memory.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup/memory.rs#L184-L198).
use iroh_base::{EndpointAddr, TransportAddr};
let relay = TransportAddr::Relay("https://my.relay.example".parse()?);
let ep_id = SecretKey::generate().public(); // any endpoint id you own
let info = EndpointAddr::from_parts(ep_id, [relay].into_iter().collect());
lookup.add_endpoint_info(info); // merges with existing entry if present
Adding Direct Transport Addresses
Populate the lookup with direct socket addresses for peer-to-peer connectivity:
use iroh_base::{TransportAddr, SocketAddr};
let direct = TransportAddr::Direct(SocketAddr::from(([127,0,0,1], 8080)));
let info = EndpointAddr::from_parts(ep_id, [direct].into_iter().collect());
lookup.add_endpoint_info(info); // augments the stored entry
Replacing Entire Entries
When you need to overwrite the complete stored information for an endpoint, use set_endpoint_info. This method replaces the entire entry and returns the previous EndpointData, as implemented in lines 62-77 of the source file.
let previous_data = lookup.set_endpoint_info(ep_id, new_info).await?;
Querying and Removing Address Information
Retrieve current mappings with get_endpoint_info, which returns a cloned EndpointInfo for the given EndpointId (lines 200-206). To delete stale information, call remove_endpoint_info:
lookup.remove_endpoint_info(ep_id); // returns the removed EndpointInfo (or None)
Resolving Addresses for Connection Establishment
MemoryLookup implements the AddressLookup trait, providing the resolve method that iroh endpoints invoke internally. The lookup stores EndpointData together with a timestamp (last_updated) in a thread-safe Arc<RwLock<BTreeMap>> according to lines 75-80 of [iroh/src/address_lookup/memory.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/address_lookup/memory.rs#L75-L80).
When resolve is called (lines 220-237), the lookup searches for the EndpointId, builds an Item containing the EndpointInfo, the provenance string, and the update time as microseconds since the epoch, then yields it as a stream. If the lookup returns None, the endpoint falls back to other resolution services such as DNS or PKARR.
Sharing Address Data Between Endpoints
A single MemoryLookup instance can be shared between multiple endpoints to maintain a synchronized view of the network. The test helper endpoint_pair in [iroh/src/socket.rs](https://github.com/n0-computer/iroh/blob/main/iroh/src/socket.rs#L2337-L2659) demonstrates this pattern by having both endpoints write their addressing information to the same shared lookup.
use iroh::{address_lookup::memory::MemoryLookup, endpoint::presets, Endpoint};
let shared_lookup = MemoryLookup::new();
let ep1 = Endpoint::builder(presets::Minimal)
.address_lookup(shared_lookup.clone())
.bind()
.await?;
let ep2 = Endpoint::builder(presets::Minimal)
.address_lookup(shared_lookup.clone())
.bind()
.await?;
// Keep the lookup up‑to‑date with the endpoints' own address streams:
tokio::spawn(async move {
let mut stream = ep1.watch_addr().stream().merge(ep2.watch_addr().stream());
while let Some(info) = stream.next().await {
shared_lookup.add_endpoint_info(info);
}
});
Summary
MemoryLookupstoresEndpointId-to-address mappings in anArc<RwLock<BTreeMap>>located iniroh/src/address_lookup/memory.rs, enabling thread-safe concurrent access.- Initialization occurs via
MemoryLookup::new()orwith_provenance(), with integration throughBuilder::address_lookupbefore binding the endpoint. - Updates use
add_endpoint_infofor merging addresses orset_endpoint_infofor complete replacement, whileremove_endpoint_infodeletes stale entries and returns the previous data. - Resolution happens through the
AddressLookup::resolvetrait implementation, yieldingItemstreams containing provenance metadata and timestamps for the endpoint's connection logic. - Sharing a single lookup instance across multiple endpoints allows dynamic, synchronized peer address management at runtime.
Frequently Asked Questions
How does MemoryLookup handle concurrent updates from multiple tasks?
MemoryLookup wraps its internal BTreeMap in an Arc<RwLock<>>, allowing multiple concurrent readers or a single writer. This design prevents blocking scenarios during high-frequency address updates while ensuring that the endpoint always receives consistent EndpointInfo snapshots during resolution.
What is the difference between add_endpoint_info and set_endpoint_info?
add_endpoint_info merges new transport addresses with existing entries and overwrites only the relay URL, preserving other stored metadata. set_endpoint_info completely replaces the entire stored entry for an EndpointId and returns the previous EndpointData, making it suitable for full synchronization scenarios where you want to replace stale information entirely.
Can I use MemoryLookup with direct peer-to-peer addresses?
Yes. MemoryLookup accepts any TransportAddr variant, including TransportAddr::Direct(SocketAddr) for direct connections and TransportAddr::Relay(url) for relay-based connectivity. You can populate the lookup with any combination of addressing types that your application discovers through out-of-band mechanisms.
How do I debug which source provided a resolved address?
Each Item produced by MemoryLookup::resolve carries a provenance string (defaulting to "memory_lookup"). When creating the lookup with MemoryLookup::with_provenance("custom_name"), this identifier attaches to every resolved item and appears in logs, allowing you to trace whether addresses came from memory, DNS, or other discovery mechanisms.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →