Skip to content

Commit 37dcb88

Browse files
committed
add tickets & more on quickstart
1 parent 02e1434 commit 37dcb88

File tree

6 files changed

+235
-16
lines changed

6 files changed

+235
-16
lines changed

concepts/discovery.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Discovery is the glue that connects an [Endpoint](/concepts/endpoints#endpoint-i
1111

1212
Endpoint discovery is an automated system for an [Endpoint](/concepts/endpoints) to retrieve addressing information. Each iroh endpoint will automatically publish their own addressing information with configured discovery services. Usually this means publishing which [Home Relay](/concepts/relay#home-relay) an endpoint is findable at, but they could also publish their direct addresses.
1313

14+
1415
## Discovery Services
1516

1617
There are four different implementations of the discovery service in iroh. iroh

concepts/endpoints.mdx

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,9 @@ endpoints, while still remaining independent connections. This will result in
1313
more optimal network behaviour.
1414

1515

16-
## Endpoint Identifiers
17-
18-
Each endpoint in iroh has a unique identifier (`EndpointID`) created as a
19-
cryptographic key. This can be used to globally identify an endpoint. Because
20-
`EndpointIDs` are cryptographic keys, they are also the mechanism by which all
21-
traffic is always encrypted for a specific endpoint only.
22-
23-
See the [EndpointID](https://docs.rs/iroh/latest/iroh/type.EndpointId.html) documentation for more information.
24-
2516
## Connections
2617

27-
Because we're in a peer-2-peer context, either endpoint might be operating as
18+
Because we're in a peer-to-peer context, either endpoint might be operating as
2819
the "server", so we use `connect` and `accept` to distinguish between the two.
2920
The `connect` method is used to create a new connection to a remote endpoint,
3021
while `accept` is used to accept incoming connections from a remote endpoint.
@@ -38,6 +29,65 @@ A [Relay](/concepts/relays) server can be used to make the connections reliable.
3829
Due to the light-weight properties of QUIC streams a stream can only be accepted once the initiating peer has sent some data on it.
3930
</Note>
4031

32+
33+
## Endpoint Identifiers
34+
35+
Each endpoint in iroh has a unique identifier (`EndpointID`) created as a
36+
cryptographic key. This can be used to globally identify an endpoint. Because
37+
`EndpointIDs` are cryptographic keys, they are also the mechanism by which all
38+
traffic is always encrypted for a specific endpoint only.
39+
40+
See the [EndpointID](https://docs.rs/iroh/latest/iroh/type.EndpointId.html) documentation for more information.
41+
42+
# Endpoint Addresses
43+
44+
Endpoint Addresses or [`EndpointAddrs`](https://docs.rs/iroh/latest/iroh/struct.EndpointAddr.html) are a common struct you'll interact when working with iroh to tell iroh what & where to dial. In rust they look like this:
45+
46+
```rust
47+
pub struct Addr {
48+
pub id: PublicKey,
49+
pub addrs: BTreeSet<TransportAddr>,
50+
}
51+
```
52+
53+
You'll interact with `EndpointAddr`s a fair amount when working with iroh. It's also quite normal to construct addresses manually from, say, endpoint identifiers stored in your application database.
54+
55+
When we call [`connect`](https://docs.rs/iroh/latest/iroh/endpoint/struct.Endpoint.html#method.connect) on an [Endpoint](https://www.iroh.computer/docs/concepts/endpoint), we need to pass either a `EndpointAddr`, or something that can turn into a `EndpointAddr`. In iroh `Endpoint`s will have different fields populated depending on where they came from, and the discovery services you've configured your endpoint with.
56+
57+
### Interaction with discovery
58+
59+
From the above struct, the only _required_ field is the `id`. And because of
60+
this, there's an implementation of `From` that can turn `EndpointIDs` directly
61+
into EndpointAddrs. _but this will only work if you have a discovery service
62+
that can resolve EndpointIDs enabled_. Thankfully, we enable discovery by
63+
default:
64+
65+
```rust
66+
use iroh::Endpoint;
67+
68+
// enables dialing by EndpointAddrs that only have EndpointIDs by default:
69+
let ep = Endpoint::builder()
70+
.bind()
71+
.await?;
72+
```
73+
74+
This is why we actively encourage configuring a discovery service, and DNS is the most common one we recommend. Because we're in p2p land dialing details & even home relays for an endpoint can change on very short notice, making this data go stale quickly. Endpoint Identifiers are a practical source of stability that counteracts this.
75+
76+
### When to provide full details
77+
78+
If you have full dialing details, it's well worth providing them as part of a
79+
`EndpointAddr` passed to `connect`. Iroh can use this to skip the network
80+
roundtrip required to either do initial address discovery, or update cached
81+
addresses. So if you have a source of up to date home relay & dialing info,
82+
provide it!
83+
84+
### Don't store relay_url & direct_addresses values
85+
86+
If you're persisting the contents of `EndpointAddrs` in your app, it's probably
87+
not worth keeping the `relay_url` and `direct_address` fields, unless you _know_
88+
these details are unlikely to change. Providing stale details to the endpoint
89+
can slow down connection construction.
90+
4191
## Building on Endpoints
4292

4393
Endpoints are a low-level primitive that iroh exposes on purpose. For some

concepts/tickets.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Tickets are a way to share dialing information between iroh endpoints. They're a
2+
single token that contains everything needed to connect to another endpoint or gossip topic.
3+
4+
You can use the default iroh endpoint tickets or build your own tickets using the `iroh-tickets` crate.
5+
6+
<Note>
7+
Have a ticket? Try pasting it into the [iroh ticket explorer](https://ticket.iroh.computer) to break it down! Here's an [example](https://ticket.iroh.computer?ticket=docaaacarwhmusoqf362j3jpzrehzkw3bqamcp2mmbhn3fmag3mzzfjp4beahj2v7aezhojvfqi5wltr4vxymgzqnctryyup327ct7iy4s5noxy6aaa)
8+
</Note>
9+
10+
Tickets are a single serialized token containing everything needed to kick off an interaction with another endpoint running iroh. Here's an example of one:
11+
12+
```text
13+
docaaacarwhmusoqf362j3jpzrehzkw3bqamcp2mmbhn3fmag3mzzfjp4beahj2v7aezhojvfqi5wltr4vxymgzqnctryyup327ct7iy4s5noxy6aaa
14+
```
15+
16+
Yes, they're long.
17+
18+
They're also very powerful. Tickets combine a piece of info with _dialing information for the endpoint to fetch from_. We've seen numerous content-addressed systems force users to copy and paste long hashes around, and we figure if you're going to have to copy and paste something, it might as well contain both the hash and a hint about where to find the data. Tickets also work very well in QR codes!
19+
20+
In practice this is a _massive_ speed pickup for iroh. When you're given a ticket, you have everything you need to construct a request
21+
22+
## Kinds of Tickets
23+
24+
Currently, there are three kinds of tickets:
25+
26+
| Type | Description | Contents |
27+
| --- | --- | --- |
28+
| `endpoint` | A token for connecting to an iroh endpoint | `Endpoint Address` |
29+
| `blob` | A token for fetching a [blob](/docs/layers/blobs) or [collection](/docs/layers/blobs#collections) | `Hash`, `HashOrCollection`, `Endpoint Address` |
30+
| `document` | A read/write access token to a [document](/proto/iroh-docs), plus an endpoint address | `DocID`, `Read/Write Capability`, `[Endpoint Address]` |
31+
32+
Tickets always start with an ascii string that describes the type of ticket (eg: `blob`), followed by a base32-lowercase-encoded payload. The payload is [postcard-encoded data](https://postcard.jamesmunns.com/wire-format.html) that contains the information needed to use the ticket. We chose postcard because it's extremely succinct.
33+
34+
## Tickets are sensitive
35+
36+
When you create a ticket, it embeds the IP addresses of the machines you're using to create the ticket. This means that if you share a ticket with someone, they can use it to connect to your machine. This is a feature, not a bug. It's a way to bootstrap connections between peers without needing a central server. It also means you should be careful about sharing tickets with people you don't want to have your IP address.
37+
38+
It's worth pointing out this setup is considerably better than full peer-2-peer systems, which _broadcast_ your IP to peers. Instead in iroh, we use tickets to form a "cozy network" between peers you explicitly want to connect with. It's possible to go "full p2p" & configure your app to broadcast dialing details, but we think tickets represent a better middle-ground default.
39+
40+
## Document Tickets are _secrets_
41+
42+
When you create a document ticket, you're creating a secret that allows someone to read or write to a document. This means that you should be careful about sharing document tickets with people you don't trust. What's more, someone who has a document ticket can use it to create new tickets for the same document. This means that if you share a document ticket with someone, they can use it to create new tickets for the same document, and share those tickets with others.
43+
44+
## Tickets in Apps
45+
Using tickets in your app comes down to what you're trying to accomplish. For short-lived sessions where both devices are online at the same time, tickets are an incredibly powerful way to bootstrap connections, and require no additional servers for coordination.
46+
47+
If you have any means of automating (like, a central database or server to bootstrap from) we recommend you _do not_ use tickets in your app, and instead program around the idea that you can _dial by EndpointID_. Tickets can contain information that can go stale quickly. instead focus on caching `endpointIDs`, and letting iroh transparently resolve dialing details at runtime.

docs.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"group": "How it works",
2424
"pages": [
2525
"concepts/endpoints",
26+
"concepts/tickets",
2627
"concepts/discovery",
2728
"concepts/relays",
2829
"concepts/holepunching",
@@ -42,13 +43,14 @@
4243
]
4344
},
4445
{
45-
"group": "Sending Data",
46+
"group": "Communicating over Iroh",
4647
"pages": [
4748
"protocols/kv-crdts",
4849
"protocols/blobs",
4950
"protocols/rpc",
5051
"protocols/automerge",
5152
"protocols/streaming",
53+
"examples/chat",
5254
"protocols/writing-a-protocol"
5355
]
5456
},

examples/chat.mdx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
---
2-
title: "Building a P2P Chat Application with Iroh"
3-
---
41
import { YouTube } from '@/components/youtube'
52

3+
## Building a P2P Chat Application with Iroh
4+
65
<iframe width="560" height="315" src="https://www.youtube.com/embed/ogN_mBkWu7o?si=Y-GqkmGBRZfRhclc" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
76

87
This tutorial demonstrates how to build a peer-to-peer chat application from scratch using Rust and the Iroh library. While this implementation is simplified, it illustrates core concepts of P2P networking and the Iroh gossip protocol.

quickstart.mdx

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ cd ping-pong
2626
Now, add the dependencies we'll need for this example:
2727

2828
```bash
29-
cargo add iroh iroh-ping tokio anyhow
29+
cargo add iroh iroh-ping iroh-tickets tokio anyhow
3030
```
3131

3232
From here on we'll be working inside the `src/main.rs` file.
@@ -105,6 +105,9 @@ async fn main() -> anyhow::Result<()> {
105105
// connections in the iroh p2p world
106106
let recv_ep = Endpoint::builder().bind().await?;
107107

108+
// bring the endpoint online before accepting connections
109+
recv_ep.online().await;
110+
108111
// Then we initialize a struct that can accept ping requests over iroh connections
109112
let ping = Ping::new();
110113

@@ -121,6 +124,7 @@ async fn main() -> anyhow::Result<()> {
121124

122125
With these two lines, we've initialized iroh-blobs and gave it access to our `Endpoint`.
123126

127+
### Ping: Send
124128
At this point what we want to do depends on whether we want to accept incoming iroh connections from the network or create outbound iroh connections to other endpoints.
125129
Which one we want to do depends on if the executable was called with `send` as
126130
an argument or `receive`, so let's parse these two options out from the CLI
@@ -171,10 +175,126 @@ cargo run
171175
I Compiling ping-quickstart v0.1.0 (/Users/raemckelvey/dev/ping-quickstart)
172176
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.62s
173177
Running `target/debug/ping-quickstart`
174-
accepted connection from 32c5772d26bff78923ef6e85f665f23b96ee06655832649910a560af59c84e22
175178
ping took: 1.189375ms to complete
176179
```
177180

181+
## Part 2: Separate Sender and Receiver
182+
183+
Round-trip time isn't very useful when both roles live in the same binary
184+
instance. Let's split the app into two subcommands so you can run the receiver
185+
on one machine and the sender on another.
186+
187+
### What is a ticket?
188+
189+
When an iroh endpoint comes online, it has an address containing its node ID,
190+
relay URL, and direct addresses. An `EndpointTicket` wraps this address into a
191+
serializable ticket—a short string you can copy and paste. Share this ticket
192+
with senders so they can dial the receiver without manually exchanging
193+
networking details.
194+
195+
A ticket is made from an endpoint's address like this:
196+
197+
```rust
198+
use iroh_tickets::{Ticket, endpoint::EndpointTicket};
199+
200+
let ticket = EndpointTicket::new(recv_ep.addr());
201+
println!("{ticket}");
202+
```
203+
204+
For more details on tickets, see [Discovery](/concepts/discovery).
205+
206+
### Receiver
207+
208+
The receiver creates an endpoint, brings it online, prints its ticket, then runs
209+
a router that accepts incoming ping requests indefinitely:
210+
211+
```rust
212+
// filepath: src/main.rs
213+
use anyhow::{anyhow, Result};
214+
use iroh::{Endpoint, protocol::Router};
215+
use iroh_ping::Ping;
216+
use iroh_tickets::{Ticket, endpoint::EndpointTicket};
217+
use std::env;
218+
219+
async fn run_receiver() -> Result<()> {
220+
let recv_ep = Endpoint::builder().bind().await?;
221+
recv_ep.online().await;
222+
223+
let ping = Ping::new();
224+
225+
let ticket = EndpointTicket::new(recv_ep.addr());
226+
println!("{ticket}");
227+
228+
Router::builder(recv_ep)
229+
.accept(iroh_ping::ALPN, ping)
230+
.spawn();
231+
232+
// Keep the receiver running indefinitely
233+
loop {
234+
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
235+
}
236+
}
237+
```
238+
239+
### Sender
240+
241+
The sender parses the ticket, creates its own endpoint, and pings the receiver's address:
242+
243+
```rust
244+
// filepath: src/main.rs
245+
async fn run_sender(ticket: EndpointTicket) -> Result<()> {
246+
let send_ep = Endpoint::builder().bind().await?;
247+
let send_pinger = Ping::new();
248+
let rtt = send_pinger.ping(&send_ep, ticket.endpoint_addr().clone()).await?;
249+
println!("ping took: {:?} to complete", rtt);
250+
Ok(())
251+
}
252+
```
253+
254+
### Wiring it together
255+
256+
Parse the command-line arguments to determine whether to run as receiver or sender:
257+
258+
```rust
259+
// filepath: src/main.rs
260+
#[tokio::main]
261+
async fn main() -> Result<()> {
262+
let mut args = env::args().skip(1);
263+
let role = args
264+
.next()
265+
.ok_or_else(|| anyhow!("expected 'receiver' or 'sender' as the first argument"))?;
266+
267+
match role.as_str() {
268+
"receiver" => run_receiver().await,
269+
"sender" => {
270+
let ticket_str = args
271+
.next()
272+
.ok_or_else(|| anyhow!("expected ticket as the second argument"))?;
273+
let ticket = EndpointTicket::deserialize(&ticket_str)
274+
.map_err(|e| anyhow!("failed to parse ticket: {}", e))?;
275+
276+
run_sender(ticket).await
277+
}
278+
_ => Err(anyhow!("unknown role '{}'; use 'receiver' or 'sender'", role)),
279+
}
280+
}
281+
```
282+
283+
### Running it
284+
285+
In one terminal, start the receiver:
286+
287+
```bash
288+
cargo run -- receiver
289+
```
290+
291+
It will print a ticket. Copy that ticket and run the sender in another terminal:
292+
293+
```bash
294+
cargo run -- sender <TICKET>
295+
```
296+
297+
You should see the round-trip time printed by the sender.
178298

179299
## That's it!
180300

0 commit comments

Comments
 (0)