diff --git a/src/app/docs/layout.jsx b/src/app/docs/layout.jsx index 471d27c9..b760156e 100644 --- a/src/app/docs/layout.jsx +++ b/src/app/docs/layout.jsx @@ -39,12 +39,6 @@ export const navItems = [ {title: 'FAQ', href: '/docs/faq' }, ], }, - {title: 'Writing a protocol', - links: [ - {title: 'Writing your first protocol', href: '/docs/protocols/writing'}, - {title: 'Submitting to the registry', href: '/docs/protocols/submitting'}, - ] - }, ]; export default function DocsLayout({children, sections = {}}) { diff --git a/src/app/docs/protocols/submitting/page.mdx b/src/app/docs/protocols/submitting/page.mdx deleted file mode 100644 index 559d6a58..00000000 --- a/src/app/docs/protocols/submitting/page.mdx +++ /dev/null @@ -1,4 +0,0 @@ - -## Submitting a Protocol to the registry - -Currently the registry is just a [JSON file on this web site](https://github.com/n0-computer/iroh.computer/blob/main/src/app/proto/protocols.js). If you'd like your protocol to be included, please submit a pull request that adds your protocol to the list! diff --git a/src/app/docs/protocols/writing/page.mdx b/src/app/docs/protocols/writing/page.mdx deleted file mode 100644 index fe0a5918..00000000 --- a/src/app/docs/protocols/writing/page.mdx +++ /dev/null @@ -1,433 +0,0 @@ - -# 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 -``` - -## 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> { - 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, - signature: Signature, -} - -impl SignedMessage { - - pub fn sign_and_encode(secret_key: &SecretKey, message: &Message) -> anyhow::Result> { - 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 }, - // more message types will be added later -} -``` - -Encryption: - -Support `/for ` 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 ` - -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 ` - -User can define an alias. All receivers of this alias from then on refer to the user just as `` instead of by node id. - -This requires changing the code to have common mutable state between send and receive. diff --git a/src/app/docs/quickstart/page.mdx b/src/app/docs/quickstart/page.mdx index ff746254..54336a0f 100644 --- a/src/app/docs/quickstart/page.mdx +++ b/src/app/docs/quickstart/page.mdx @@ -154,7 +154,7 @@ Here's our plan for turning this into a CLI that actually does what we set out t 4. If we're supposed to receive data: - we'll parse the ticket out of the CLI arguments, - download the file using [`download`](https://docs.rs/iroh-blobs/latest/iroh_blobs/rpc/client/blobs/struct.Client.html#method.download), - - and copy the result the local file system. + - and copy the result the local file system. Phew okay! Here's how we'll grab an iroh-blobs client and look at the CLI arguments: @@ -291,6 +291,5 @@ The full example with the very latest version of iroh and iroh-blobs can be [vie If you're hungry for more, check out - the [iroh rust documentation](https://docs.rs/iroh), -- [other examples](/docs/examples), -- other available [protocols](/proto) or -- a longer guide on [how to write your own protocol](/docs/protocols/writing). +- [other examples](/docs/examples), or +- other available [protocols](/proto).