iroh_blobs/
ticket.rs

1//! Tickets for blobs.
2use std::{collections::BTreeSet, net::SocketAddr, str::FromStr};
3
4use anyhow::Result;
5use iroh::{EndpointAddr, EndpointId, RelayUrl};
6use iroh_tickets::{ParseError, Ticket};
7use serde::{Deserialize, Serialize};
8
9use crate::{BlobFormat, Hash, HashAndFormat};
10
11/// A token containing everything to get a file from the provider.
12///
13/// It is a single item which can be easily serialized and deserialized.
14#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
15#[display("{}", Ticket::serialize(self))]
16pub struct BlobTicket {
17    /// The provider to get a file from.
18    addr: EndpointAddr,
19    /// The format of the blob.
20    format: BlobFormat,
21    /// The hash to retrieve.
22    hash: Hash,
23}
24
25impl From<BlobTicket> for HashAndFormat {
26    fn from(val: BlobTicket) -> Self {
27        HashAndFormat {
28            hash: val.hash,
29            format: val.format,
30        }
31    }
32}
33
34/// Wire format for [`BlobTicket`].
35///
36/// In the future we might have multiple variants (not versions, since they
37/// might be both equally valid), so this is a single variant enum to force
38/// postcard to add a discriminator.
39#[derive(Serialize, Deserialize)]
40enum TicketWireFormat {
41    Variant0(Variant0BlobTicket),
42}
43
44// Legacy
45#[derive(Serialize, Deserialize)]
46struct Variant0BlobTicket {
47    node: Variant0NodeAddr,
48    format: BlobFormat,
49    hash: Hash,
50}
51
52#[derive(Serialize, Deserialize)]
53struct Variant0NodeAddr {
54    endpoint_id: EndpointId,
55    info: Variant0AddrInfo,
56}
57
58#[derive(Serialize, Deserialize)]
59struct Variant0AddrInfo {
60    relay_url: Option<RelayUrl>,
61    direct_addresses: BTreeSet<SocketAddr>,
62}
63
64impl Ticket for BlobTicket {
65    const KIND: &'static str = "blob";
66
67    fn to_bytes(&self) -> Vec<u8> {
68        let data = TicketWireFormat::Variant0(Variant0BlobTicket {
69            node: Variant0NodeAddr {
70                endpoint_id: self.addr.id,
71                info: Variant0AddrInfo {
72                    relay_url: self.addr.relay_urls().next().cloned(),
73                    direct_addresses: self.addr.ip_addrs().cloned().collect(),
74                },
75            },
76            format: self.format,
77            hash: self.hash,
78        });
79        postcard::to_stdvec(&data).expect("postcard serialization failed")
80    }
81
82    fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, ParseError> {
83        let res: TicketWireFormat = postcard::from_bytes(bytes)?;
84        let TicketWireFormat::Variant0(Variant0BlobTicket { node, format, hash }) = res;
85        let mut addr = EndpointAddr::new(node.endpoint_id);
86        if let Some(relay_url) = node.info.relay_url {
87            addr = addr.with_relay_url(relay_url);
88        }
89        for ip_addr in node.info.direct_addresses {
90            addr = addr.with_ip_addr(ip_addr);
91        }
92        Ok(Self { addr, format, hash })
93    }
94}
95
96impl FromStr for BlobTicket {
97    type Err = ParseError;
98
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        Ticket::deserialize(s)
101    }
102}
103
104impl BlobTicket {
105    /// Creates a new ticket.
106    pub fn new(addr: EndpointAddr, hash: Hash, format: BlobFormat) -> Self {
107        Self { hash, format, addr }
108    }
109
110    /// The hash of the item this ticket can retrieve.
111    pub fn hash(&self) -> Hash {
112        self.hash
113    }
114
115    /// The [`EndpointAddr`] of the provider for this ticket.
116    pub fn addr(&self) -> &EndpointAddr {
117        &self.addr
118    }
119
120    /// The [`BlobFormat`] for this ticket.
121    pub fn format(&self) -> BlobFormat {
122        self.format
123    }
124
125    pub fn hash_and_format(&self) -> HashAndFormat {
126        HashAndFormat {
127            hash: self.hash,
128            format: self.format,
129        }
130    }
131
132    /// True if the ticket is for a collection and should retrieve all blobs in it.
133    pub fn recursive(&self) -> bool {
134        self.format.is_hash_seq()
135    }
136
137    /// Get the contents of the ticket, consuming it.
138    pub fn into_parts(self) -> (EndpointAddr, Hash, BlobFormat) {
139        let BlobTicket { addr, hash, format } = self;
140        (addr, hash, format)
141    }
142}
143
144impl Serialize for BlobTicket {
145    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
146        if serializer.is_human_readable() {
147            serializer.serialize_str(&self.to_string())
148        } else {
149            let BlobTicket {
150                addr: node,
151                format,
152                hash,
153            } = self;
154            (node, format, hash).serialize(serializer)
155        }
156    }
157}
158
159impl<'de> Deserialize<'de> for BlobTicket {
160    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
161        if deserializer.is_human_readable() {
162            let s = String::deserialize(deserializer)?;
163            Self::from_str(&s).map_err(serde::de::Error::custom)
164        } else {
165            let (peer, format, hash) = Deserialize::deserialize(deserializer)?;
166            Ok(Self::new(peer, hash, format))
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use std::net::SocketAddr;
174
175    use iroh::{PublicKey, SecretKey, TransportAddr};
176    use iroh_test::{assert_eq_hex, hexdump::parse_hexdump};
177
178    use super::*;
179
180    fn make_ticket() -> BlobTicket {
181        let hash = Hash::new(b"hi there");
182        let peer = SecretKey::generate(&mut rand::rng()).public();
183        let addr = SocketAddr::from_str("127.0.0.1:1234").unwrap();
184        BlobTicket {
185            hash,
186            addr: EndpointAddr::from_parts(peer, [TransportAddr::Ip(addr)]),
187            format: BlobFormat::HashSeq,
188        }
189    }
190
191    #[test]
192    fn test_ticket_postcard() {
193        let ticket = make_ticket();
194        let bytes = postcard::to_stdvec(&ticket).unwrap();
195        let ticket2: BlobTicket = postcard::from_bytes(&bytes).unwrap();
196        assert_eq!(ticket2, ticket);
197    }
198
199    #[test]
200    fn test_ticket_json() {
201        let ticket = make_ticket();
202        let json = serde_json::to_string(&ticket).unwrap();
203        let ticket2: BlobTicket = serde_json::from_str(&json).unwrap();
204        assert_eq!(ticket2, ticket);
205    }
206
207    #[test]
208    fn test_ticket_base32() {
209        let hash =
210            Hash::from_str("0b84d358e4c8be6c38626b2182ff575818ba6bd3f4b90464994be14cb354a072")
211                .unwrap();
212        let endpoint_id =
213            PublicKey::from_str("ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6")
214                .unwrap();
215
216        let ticket = BlobTicket {
217            addr: EndpointAddr::new(endpoint_id),
218            format: BlobFormat::Raw,
219            hash,
220        };
221        let encoded = ticket.to_string();
222        let stripped = encoded.strip_prefix("blob").unwrap();
223        let base32 = data_encoding::BASE32_NOPAD
224            .decode(stripped.to_ascii_uppercase().as_bytes())
225            .unwrap();
226        let expected = parse_hexdump("
227            00 # discriminator for variant 0
228            ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6 # endpoint id, 32 bytes, see above
229            00 # relay url
230            00 # number of addresses (0)
231            00 # format (raw)
232            0b84d358e4c8be6c38626b2182ff575818ba6bd3f4b90464994be14cb354a072 # hash, 32 bytes, see above
233        ").unwrap();
234        assert_eq_hex!(base32, expected);
235    }
236}