# Iroh Web3Summit Workshop
Clone https://github.com/n0-computer/iroh-workshop-web3summit
```
git clone https://github.com/n0-computer/iroh-workshop-web3summit
```
Join https://iroh.computer/discord
Use channel `web3summit-workshop`
WIFI: `Web3Summit`
# Agenda
Exploring how to open direct P2P QUIC connections using iroh-net, with a focus on developing a simple gossip-based encrypted chat.
---
# Introduction
- Who wrote a simple TCP server before?
- Who has got experience with rust?
- Who has cargo installed?
---
# Exercise 1: Direct 1:1 connections
We will write a tool similar to https://www.dumbpipe.dev/ that connects two devices anywhere in the world.
## Project setup
Clone https://github.com/n0-computer/iroh-workshop-web3summit.
This will give you the code for all steps.
## Connect
Creating an endpoint:
```rust
let endpoint = Endpoint::builder()
.bind(0)
.await?;
```
Connecting
```rust
const WEB3_ALPN: &[u8] = b"WEB3_2024";
let connection = endpoint.connect(addr, WEB3_ALPN).await?;
```
(Works only if the remote accepts WEB3_ALPN)
Opening a stream
```rust
let (send, recv) = connection.open_bi().await?;
```
Copy stdin to send and recv to stdout
```rust
tokio::spawn(copy_to_stdout(remote, recv));
copy_stdin_to(send).await?;
```
## Accept
Creating an endpoint:
For accept we must provide the set of ALPNs
```rust
const WEB3_ALPN: &[u8] = b"WEB3_2024";
let endpoint = Endpoint::builder()
.alpns(vec![WEB3_ALPN.to_vec()])
.bind(0)
.await?;
```
Print ticket:
```rust
let addr = endpoint.node_addr().await?;
println!("I am {}", addr.node_id);
println!("Listening on {:#?}", addr.info);
println!("ticket: {}", NodeTicket::new(addr)?);
```
Accept loop:
```rust
while let Some(connecting) = endpoint.accept().await {
// handle each incoming connection in separate tasks.
}
```
For each accept:
```rust=
let alpn = connecting.alpn().await?;
let connection = connecting.await?;
let remote_node_id = endpoint::get_remote_node_id(&connection)?;
let (send, recv) = connection.accept_bi().await?;
let author = remote_node_id.to_string();
// Send a greeting to the remote node.
send.write_all("hello\n".as_bytes()).await?;
// Spawn two tasks to copy data in both directions.
tokio::spawn(copy_stdin_to(send));
tokio::spawn(copy_to_stdout(author, recv));
```
## Polish
```rust
let secret_key = get_or_create_secret()?;
```
Allows to specify the secret via an environment variable, to have a stable node id over multiple runs.
```rust
wait_for_relay(&endpoint).await?;
```
Wait for the node to figure out it's own relay URL
## Let's try it out
One terminal
```
cargo run -p pipe1
```
```
cargo run -p pipe1 <ticket>
```
## Use iroh DNS node discovery
https://www.iroh.computer/blog/iroh-dns
on the connect side, I want to *look up* node ids using the default iroh dns server
```rust
let discovery = DnsDiscovery::n0_dns();
let endpoint = Endpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
on the accept side, I want to *publish* node ids to the default iroh dns server
```rust
let discovery = PkarrPublisher::n0_dns(secret_key.clone());
let endpoint = Endpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
cargo run -p pipe2
```
https://www.diggui.com
## Use pkarr node discovery
both use `PkarrNodeDiscovery`.
on the connect side, we don't want to publish, so we don't need the secret key.
```rust
let discovery = PkarrNodeDiscovery::default();
let endpoint = Endpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
on the accept side, we do want to publish, so we do need the secret key
```rust
let discovery = PkarrNodeDiscovery::builder()
.secret_key(secret_key.clone())
.build()?;
let endpoint = Endpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
```
cargo run -p pipe3
```
## Publish full addresses, not just relay URL
```rust
let discovery = PkarrNodeDiscovery::builder()
.secret_key(secret_key.clone())
.include_direct_addresses(true)
.build()?;
let endpoint = Endpoint::builder()
.secret_key(secret_key)
.discovery(Box::new(discovery))
...
```
```
cargo run -p pipe4
```
# Exercise 2: Group chat
We will write a command line group chat.
## Project setup
We need an additional dependency
```toml
# iroh crate
iroh = { version = "0.22" }
```
## Create the iroh node
We use an in-memory node for the example
```rust
// create a new Iroh node, giving it the secret key
let iroh = iroh::node::Node::memory()
.secret_key(secret_key)
.spawn()
.await?;
```
## Add the info from the addresses
```rust
let mut bootstrap = Vec::new();
for ticket in &args.tickets {
let addr = ticket.node_addr();
iroh.endpoint().add_node_addr(addr.clone()).ok();
bootstrap.push(addr.node_id);
}
```
## Subscribe to a hardcoded topic with the collected bootstrap nodes
```rust
// hardcoded topic
let topic = [0u8; 32];
// subscribe to the topic, giving the bootstrap nodes
// if the tickets contained additional info, this is available in the address book of the endpoint
let (mut sink, mut stream) = iroh.gossip().subscribe(topic, bootstrap).await?;
```
## Send stdin to gossip
```rust
line = stdin.next_line() => {
if let Ok(Some(line)) = line {
// got a line from stdin
match parse_as_command(line).await {
Ok(cmd) => {
if let Some(cmd) = cmd {
sink.send(cmd).await?;
}
}
Err(cause) => {
tracing::warn!("error parsing command: {}", cause);
}
}
}
}
}
```
```rust
async fn parse_as_command(text: String) -> anyhow::Result<Option<Command>> {
let cmd = Command::Broadcast(text.as_bytes().to_vec().into());
Ok(Some(cmd))
}
```
## Print incoming messages to stdout
```rust
select! {
message = stream.next() => {
// got a message from the gossip network
if let Some(Ok(event)) = message {
if let Err(cause) = handle_event(event).await {
tracing::warn!("error handling message: {}", cause);
}
} else {
break;
}
}
```
```rust
async fn handle_event(event: Event) -> anyhow::Result<()> {
if let Event::Gossip(GossipEvent::Received(msg)) = event {
println!(
"Received message from node {}: {:?}",
msg.delivered_from, msg.content
);
} else {
tracing::info!("Got other event: {:?}", event);
}
Ok(())
}
```
We got a working chat!
```
cargo run -p chat1
```
## A proper protocol
We send around signed messages, so we know whom they are from!
```rust
#[derive(Debug, Serialize, Deserialize)]
enum Message {
Message { text: String },
// more message types will be added later
}
#[derive(Debug, Serialize, Deserialize)]
struct SignedMessage {
from: PublicKey,
data: Vec<u8>,
signature: Signature,
}
impl SignedMessage {
pub fn sign_and_encode(secret_key: &SecretKey, message: &Message) -> anyhow::Result<Vec<u8>> {
let data = postcard::to_stdvec(&message)?;
let signature = secret_key.sign(&data);
let from = secret_key.public();
let signed_message = Self {
from,
data,
signature,
};
let encoded = postcard::to_stdvec(&signed_message)?;
Ok(encoded)
}
pub fn verify_and_decode(bytes: &[u8]) -> anyhow::Result<(PublicKey, Message)> {
let signed_message: Self = postcard::from_bytes(bytes)?;
let key = signed_message.from;
key.verify(&signed_message.data, &signed_message.signature)?;
let message: Message = postcard::from_bytes(&signed_message.data)?;
Ok((signed_message.from, message))
}
}
```
## Wiring it up
Verify and decode incoming messages. Silently ignore non-verified messages
```rust
let msg = Message::Message { text };
let signed = SignedMessage::sign_and_encode(secret_key, &msg)?;
let cmd = Command::Broadcast(signed.into());
}
```
Handle the incoming messages (only one message type for now):
```rust
let Ok((from, msg)) = SignedMessage::verify_and_decode(&msg.content) else {
tracing::warn!("Failed to verify message: {:?}", msg.content);
return Ok(());
};
```
```
cargo run -p chat2
```
## Encrypted direct messages
Extend the Message enum
```rust
enum Message {
Message { text: String },
Direct { to: PublicKey, encrypted: Vec<u8> },
// more message types will be added later
}
```
Encryption:
Support `/for <publickey> <message>` syntax
```rust
let msg = if let Some(private) = text.strip_prefix("/for ") {
// yeah yeah, there are nicer ways to do this, sue me...
let mut parts = private.splitn(2, ' ');
let Some(to) = parts.next() else {
anyhow::bail!("missing recipient");
};
let Some(msg) = parts.next() else {
anyhow::bail!("missing message");
};
let Ok(to) = PublicKey::from_str(to) else {
anyhow::bail!("invalid recipient");
};
let mut encrypted = msg.as_bytes().to_vec();
// encrypt the data in place
secret_key.shared(&to).seal(&mut encrypted);
Message::Direct { to, encrypted }
} else ...
```
Decryption:
```rust
if to != secret_key.public() {
// not for us
return Ok(());
}
let mut buffer = encrypted;
secret_key.shared(&from).open(&mut buffer)?;
let message = std::str::from_utf8(&buffer)?;
println!("got encrypted message from {}: {}", from, message);
```
```
cargo run -p chat3
```
## Homework: support sending files
Syntax:
`/share <file>`
This involves using iroh-bytes and handling two different ALPNs, [GOSSIP_ALPN](https://docs.rs/iroh-gossip/latest/iroh_gossip/net/constant.GOSSIP_ALPN.html) and [iroh_bytes::protocol::ALPN](https://docs.rs/iroh-bytes/latest/iroh_bytes/protocol/constant.ALPN.html)
Depending on the incoming ALPN you have to dispatch to gossip or bytes.
## Homework: aliases
Syntax:
`/alias <alias>`
User can define an alias. All receivers of this alias from then on refer to the user just as `<alias>` instead of by node id.
This requires changing the code to have common mutable state between send and receive.