Skip to content

Commit

Permalink
feat(iroh): add ticket prefixes and a doctor ticket-inspect command (
Browse files Browse the repository at this point in the history
…#1711)

## Description

Adds a `doc:` and `blob:` prefix to the doc and blob tickets
respectively. This is mandatory and identifies the kind of iroh ticket.

## Notes & open questions

doctor sample run (happy path):
```
doctor ticket-inspect blob:edjoqrkky753mdwphqrxbr2wjzkizv4hkb5s5ovav73c2zfuaja7aaibaqalkpj5jcqzsaqawu6t2sfbvubqbqfiaaf4ivyaycuaadgek4adtjetziwzcpcdqzemuxmwei2fp5wtmfjcy4qysovyzjhahfao3aaa
```
```
Blob ticket:
Ticket {
    node: PeerAddr {
        peer_id: PublicKey(2luekswh7o3a5tz4),
        info: AddrInfo {
            derp_region: Some(
                1,
            ),
            direct_addresses: {
                181.61.61.72:34144,
                181.61.61.72:61486,
                192.168.0.12:11204,
                192.168.0.25:11204,
            },
        },
    },
    format: Raw,
    hash: Hash(
        39a493ca2d913c438648ca5d96223457f6d361522c721893ab8ca4e03940ed80,
    ),
    token: None,
}
```
doctor sample run (unhappy path):
```
 doctor ticket-inspect blob:EDJOQRKKY753MDWPHQRXBR2WJZKIZV4HKB5S5OVAV73C2ZFUAJA7AAIBAQALKPJ5JDQIUAQAWU6T2SFO4ABQBQFIAAGMIVYAYCU'
Error: failed parsing blob ticket

Caused by:
    0: decoding failed: invalid length at 98
    1: invalid length at 98
```

another unhappy path:
```
doctor ticket-inspect asds:EDJOQRKKY753MDWPHQRXBR2WJZKIZV4HKB5S5OVAV73C2ZFUAJA7AAIBAQALKPJ5JDQIUAQAWU6T2SFO4ABQBQFIAAGMIVYAYCUAAGOEK4ADTJETZIWZCPCDQZEMUXMWEI2FP5WTMFJCY4QYSOVYZJHAHFAO3AAA'
```
```
Error: unrecogized ticket prefix

Caused by:
    Matching variant not found
```

## Change checklist

- [x] Self-review.
- [x] Documentation updates if relevant.
- [x] Tests if relevant.
  • Loading branch information
divagant-martian authored Oct 26, 2023
1 parent b9ee93e commit 2d292e3
Show file tree
Hide file tree
Showing 15 changed files with 386 additions and 276 deletions.
6 changes: 2 additions & 4 deletions iroh/examples/dump-blob-fsm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
//! copy that ticket value (the long string after `--ticket`) & feed it to this example:
//! $ cargo run --example dump-blob-fsm <ticket>
use std::env::args;
use std::str::FromStr;

use iroh::dial::Ticket;
use iroh::ticket::blob::Ticket;
use iroh_bytes::get::fsm::{ConnectedNext, EndBlobNext};
use iroh_bytes::protocol::GetRequest;
use iroh_io::ConcatenateSliceWriter;
Expand All @@ -29,8 +28,7 @@ pub fn setup_logging() {
async fn main() -> anyhow::Result<()> {
setup_logging();

let ticket = args().nth(1).expect("missing ticket");
let ticket = Ticket::from_str(&ticket)?;
let ticket: Ticket = args().nth(1).expect("missing ticket").parse()?;

// generate a transient secretkey for this connection
//
Expand Down
6 changes: 2 additions & 4 deletions iroh/examples/dump-blob-stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
//! $ cargo run --example dump-blob-stream <ticket>
use std::env::args;
use std::io;
use std::str::FromStr;

use bao_tree::io::fsm::BaoContentItem;
use bytes::Bytes;
use futures::{Stream, StreamExt};
use genawaiter::sync::Co;
use genawaiter::sync::Gen;
use iroh::dial::Ticket;
use iroh::ticket::blob::Ticket;
use iroh_bytes::get::fsm::{AtInitial, BlobContentNext, ConnectedNext, EndBlobNext};
use iroh_bytes::protocol::GetRequest;
use iroh_net::key::SecretKey;
Expand Down Expand Up @@ -166,8 +165,7 @@ fn stream_children(initial: AtInitial) -> impl Stream<Item = io::Result<Bytes>>
async fn main() -> anyhow::Result<()> {
setup_logging();

let ticket = args().nth(1).expect("missing ticket");
let ticket = Ticket::from_str(&ticket)?;
let ticket: Ticket = args().nth(1).expect("missing ticket").parse()?;

// generate a transient secret key for this connection
//
Expand Down
2 changes: 1 addition & 1 deletion iroh/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use indicatif::{
ProgressStyle,
};
use iroh::client::quic::Iroh;
use iroh::dial::Ticket;
use iroh::rpc_protocol::*;
use iroh::ticket::blob::Ticket;
use iroh_bytes::{protocol::RequestToken, util::runtime, BlobFormat, Hash, Tag};
use iroh_net::magicsock::DirectAddrInfo;
use iroh_net::PeerAddr;
Expand Down
2 changes: 1 addition & 1 deletion iroh/src/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use futures::{Stream, StreamExt};
use indicatif::{HumanBytes, MultiProgress, ProgressBar, ProgressStyle};
use iroh::{
client::Iroh,
dial::Ticket,
rpc_protocol::{ProviderService, SetTagOption, WrapOption},
ticket::blob::Ticket,
};
use iroh_bytes::{
protocol::RequestToken,
Expand Down
28 changes: 27 additions & 1 deletion iroh/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ use std::{
collections::HashMap,
net::SocketAddr,
num::NonZeroU16,
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};

use crate::config::{path_with_env, NodeConfig};

use anyhow::Context;
use anyhow::{anyhow, Context};
use clap::Subcommand;
use indicatif::{HumanBytes, MultiProgress, ProgressBar};
use iroh::util::{path::IrohPaths, progress::ProgressWriter};
Expand Down Expand Up @@ -148,6 +149,8 @@ pub enum Commands {
#[clap(long, default_value_t = 5)]
count: usize,
},
/// Inspect a ticket.
TicketInspect { ticket: String },
}

#[derive(Debug, Serialize, Deserialize, MaxSize)]
Expand Down Expand Up @@ -913,6 +916,28 @@ fn create_secret_key(secret_key: SecretKeyOption) -> anyhow::Result<SecretKey> {
})
}

fn inspect_ticket(ticket: &str) -> anyhow::Result<()> {
let (kind, _) = iroh::ticket::Kind::parse_prefix(ticket)
.ok_or_else(|| anyhow!("missing ticket prefix"))??;
match kind {
iroh::ticket::Kind::Blob => {
let ticket = iroh::ticket::blob::Ticket::from_str(ticket)
.context("failed parsing blob ticket")?;
println!("Blob ticket:\n{ticket:#?}");
}
iroh::ticket::Kind::Doc => {
let ticket =
iroh::ticket::doc::Ticket::from_str(ticket).context("failed parsing doc ticket")?;
println!("Document ticket:\n{ticket:#?}");
}
iroh::ticket::Kind::Node => {
println!("node tickets are yet to be implemented :)");
}
}

Ok(())
}

pub async fn run(command: Commands, config: &NodeConfig) -> anyhow::Result<()> {
match command {
Commands::Report {
Expand Down Expand Up @@ -971,5 +996,6 @@ pub async fn run(command: Commands, config: &NodeConfig) -> anyhow::Result<()> {
let config = NodeConfig::from_env(None)?;
derp_regions(count, config).await
}
Commands::TicketInspect { ticket } => inspect_ticket(&ticket),
}
}
216 changes: 2 additions & 214 deletions iroh/src/dial.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
//! The ticket type for the provider.
//!
//! This is in it's own module to enforce the invariant that you can not construct a ticket
//! with an empty address list.
//! Utilities to dial a node.
use std::fmt::{self, Display};
use std::str::FromStr;

use anyhow::{ensure, Context, Result};
use iroh_bytes::protocol::RequestToken;
use iroh_bytes::{BlobFormat, Hash};
use anyhow::Context;
use iroh_net::derp::{DerpMap, DerpMode};
use iroh_net::key::SecretKey;
use iroh_net::PeerAddr;
use serde::{Deserialize, Serialize};

/// Options for the client
#[derive(Clone, Debug)]
Expand Down Expand Up @@ -47,206 +38,3 @@ pub async fn dial(opts: Options) -> anyhow::Result<quinn::Connection> {
.await
.context("failed to connect to provider")
}

/// A token containing everything to get a file from the provider.
///
/// It is a single item which can be easily serialized and deserialized. The [`Display`]
/// and [`FromStr`] implementations serialize to base32.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ticket {
/// The provider to get a file from.
peer: PeerAddr,
/// The format of the blob.
format: BlobFormat,
/// The hash to retrieve.
hash: Hash,
/// Optional Request token.
token: Option<RequestToken>,
}

impl Ticket {
/// Creates a new ticket.
pub fn new(
peer: PeerAddr,
hash: Hash,
format: BlobFormat,
token: Option<RequestToken>,
) -> Result<Self> {
ensure!(
!peer.info.direct_addresses.is_empty(),
"addrs list can not be empty"
);
Ok(Self {
hash,
format,
peer,
token,
})
}

/// Deserializes from bytes.
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let slf: Ticket = postcard::from_bytes(bytes)?;
ensure!(
!slf.peer.info.direct_addresses.is_empty(),
"Invalid address list in ticket"
);
Ok(slf)
}

/// Serializes to bytes.
pub fn to_bytes(&self) -> Vec<u8> {
postcard::to_stdvec(self).expect("postcard::to_stdvec is infallible")
}

/// The hash of the item this ticket can retrieve.
pub fn hash(&self) -> Hash {
self.hash
}

/// The [`PeerAddr`] of the provider for this ticket.
pub fn node_addr(&self) -> &PeerAddr {
&self.peer
}

/// The [`RequestToken`] for this ticket.
pub fn token(&self) -> Option<&RequestToken> {
self.token.as_ref()
}

/// The [`BlobFormat`] for this ticket.
pub fn format(&self) -> BlobFormat {
self.format
}

/// Set the [`RequestToken`] for this ticket.
pub fn with_token(self, token: Option<RequestToken>) -> Self {
Self { token, ..self }
}

/// True if the ticket is for a collection and should retrieve all blobs in it.
pub fn recursive(&self) -> bool {
self.format.is_hash_seq()
}

/// Get the contents of the ticket, consuming it.
pub fn into_parts(self) -> (PeerAddr, Hash, BlobFormat, Option<RequestToken>) {
let Ticket {
peer,
hash,
format,
token,
} = self;
(peer, hash, format, token)
}

/// Convert this ticket into a [`Options`], adding the given secret key.
pub fn as_get_options(&self, secret_key: SecretKey, derp_map: Option<DerpMap>) -> Options {
Options {
peer: self.peer.clone(),
secret_key,
keylog: true,
derp_map,
}
}
}

/// Serializes to base32.
impl Display for Ticket {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let encoded = self.to_bytes();
let mut text = data_encoding::BASE32_NOPAD.encode(&encoded);
text.make_ascii_lowercase();
write!(f, "{text}")
}
}

/// Deserializes from base32.
impl FromStr for Ticket {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = data_encoding::BASE32_NOPAD.decode(s.to_ascii_uppercase().as_bytes())?;
let slf = Self::from_bytes(&bytes)?;
Ok(slf)
}
}

impl Serialize for Ticket {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
serializer.serialize_str(&self.to_string())
} else {
let Ticket {
peer,
format,
hash,
token,
} = self;
(peer, format, hash, token).serialize(serializer)
}
}
}

impl<'de> Deserialize<'de> for Ticket {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
if deserializer.is_human_readable() {
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(serde::de::Error::custom)
} else {
let (peer, format, hash, token) = Deserialize::deserialize(deserializer)?;
Self::new(peer, hash, format, token).map_err(serde::de::Error::custom)
}
}
}

#[cfg(test)]
mod tests {
use std::net::SocketAddr;

use bao_tree::blake3;

use super::*;

fn make_ticket() -> Ticket {
let hash = blake3::hash(b"hi there");
let hash = Hash::from(hash);
let peer = SecretKey::generate().public();
let addr = SocketAddr::from_str("127.0.0.1:1234").unwrap();
let token = RequestToken::new(vec![1, 2, 3, 4, 5, 6]).unwrap();
let derp_region = Some(0);
Ticket {
hash,
peer: PeerAddr::from_parts(peer, derp_region, vec![addr]),
token: Some(token),
format: BlobFormat::HashSeq,
}
}

#[test]
fn test_ticket_postcard() {
let ticket = make_ticket();
let bytes = postcard::to_stdvec(&ticket).unwrap();
let ticket2: Ticket = postcard::from_bytes(&bytes).unwrap();
assert_eq!(ticket2, ticket);
}

#[test]
fn test_ticket_json() {
let ticket = make_ticket();
let json = serde_json::to_string(&ticket).unwrap();
let ticket2: Ticket = serde_json::from_str(&json).unwrap();
assert_eq!(ticket2, ticket);
}

#[test]
fn test_ticket_base32_roundtrip() {
let ticket = make_ticket();
let base32 = ticket.to_string();
println!("Ticket: {base32}");
println!("{} bytes", base32.len());

let ticket2: Ticket = base32.parse().unwrap();
assert_eq!(ticket2, ticket);
}
}
1 change: 1 addition & 0 deletions iroh/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod get;
pub mod node;
pub mod rpc_protocol;
pub mod sync_engine;
pub mod ticket;
pub mod util;

/// Expose metrics module
Expand Down
2 changes: 1 addition & 1 deletion iroh/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ use tokio::task::JoinError;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, error_span, info, trace, warn, Instrument};

use crate::dial::Ticket;
use crate::downloader::Downloader;
use crate::rpc_protocol::{
BlobAddPathRequest, BlobAddPathResponse, BlobAddStreamRequest, BlobAddStreamResponse,
Expand All @@ -68,6 +67,7 @@ use crate::rpc_protocol::{
ProviderResponse, ProviderService, SetTagOption,
};
use crate::sync_engine::{SyncEngine, SYNC_ALPN};
use crate::ticket::blob::Ticket;

const MAX_CONNECTIONS: u32 = 1024;
const MAX_STREAMS: u64 = 10;
Expand Down
Loading

0 comments on commit 2d292e3

Please sign in to comment.