iroh_blobs/
ticket.rs

1//! Tickets for blobs.
2use std::{collections::BTreeSet, net::SocketAddr, str::FromStr};
3
4use anyhow::Result;
5use iroh::{NodeAddr, NodeId, RelayUrl};
6use iroh_base::ticket::{self, 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    node: NodeAddr,
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    node_id: NodeId,
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                node_id: self.node.node_id,
71                info: Variant0AddrInfo {
72                    relay_url: self.node.relay_url.clone(),
73                    direct_addresses: self.node.direct_addresses.clone(),
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, ticket::ParseError> {
83        let res: TicketWireFormat = postcard::from_bytes(bytes)?;
84        let TicketWireFormat::Variant0(Variant0BlobTicket { node, format, hash }) = res;
85        Ok(Self {
86            node: NodeAddr {
87                node_id: node.node_id,
88                relay_url: node.info.relay_url,
89                direct_addresses: node.info.direct_addresses,
90            },
91            format,
92            hash,
93        })
94    }
95}
96
97impl FromStr for BlobTicket {
98    type Err = ticket::ParseError;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        Ticket::deserialize(s)
102    }
103}
104
105impl BlobTicket {
106    /// Creates a new ticket.
107    pub fn new(node: NodeAddr, hash: Hash, format: BlobFormat) -> Self {
108        Self { hash, format, node }
109    }
110
111    /// The hash of the item this ticket can retrieve.
112    pub fn hash(&self) -> Hash {
113        self.hash
114    }
115
116    /// The [`NodeAddr`] of the provider for this ticket.
117    pub fn node_addr(&self) -> &NodeAddr {
118        &self.node
119    }
120
121    /// The [`BlobFormat`] for this ticket.
122    pub fn format(&self) -> BlobFormat {
123        self.format
124    }
125
126    pub fn hash_and_format(&self) -> HashAndFormat {
127        HashAndFormat {
128            hash: self.hash,
129            format: self.format,
130        }
131    }
132
133    /// True if the ticket is for a collection and should retrieve all blobs in it.
134    pub fn recursive(&self) -> bool {
135        self.format.is_hash_seq()
136    }
137
138    /// Get the contents of the ticket, consuming it.
139    pub fn into_parts(self) -> (NodeAddr, Hash, BlobFormat) {
140        let BlobTicket { node, hash, format } = self;
141        (node, hash, format)
142    }
143}
144
145impl Serialize for BlobTicket {
146    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
147        if serializer.is_human_readable() {
148            serializer.serialize_str(&self.to_string())
149        } else {
150            let BlobTicket { node, format, hash } = self;
151            (node, format, hash).serialize(serializer)
152        }
153    }
154}
155
156impl<'de> Deserialize<'de> for BlobTicket {
157    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
158        if deserializer.is_human_readable() {
159            let s = String::deserialize(deserializer)?;
160            Self::from_str(&s).map_err(serde::de::Error::custom)
161        } else {
162            let (peer, format, hash) = Deserialize::deserialize(deserializer)?;
163            Ok(Self::new(peer, hash, format))
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use std::net::SocketAddr;
171
172    use iroh::{PublicKey, SecretKey};
173    use iroh_test::{assert_eq_hex, hexdump::parse_hexdump};
174
175    use super::*;
176
177    fn make_ticket() -> BlobTicket {
178        let hash = Hash::new(b"hi there");
179        let peer = SecretKey::generate(rand::thread_rng()).public();
180        let addr = SocketAddr::from_str("127.0.0.1:1234").unwrap();
181        let relay_url = None;
182        BlobTicket {
183            hash,
184            node: NodeAddr::from_parts(peer, relay_url, [addr]),
185            format: BlobFormat::HashSeq,
186        }
187    }
188
189    #[test]
190    fn test_ticket_postcard() {
191        let ticket = make_ticket();
192        let bytes = postcard::to_stdvec(&ticket).unwrap();
193        let ticket2: BlobTicket = postcard::from_bytes(&bytes).unwrap();
194        assert_eq!(ticket2, ticket);
195    }
196
197    #[test]
198    fn test_ticket_json() {
199        let ticket = make_ticket();
200        let json = serde_json::to_string(&ticket).unwrap();
201        let ticket2: BlobTicket = serde_json::from_str(&json).unwrap();
202        assert_eq!(ticket2, ticket);
203    }
204
205    #[test]
206    fn test_ticket_base32() {
207        let hash =
208            Hash::from_str("0b84d358e4c8be6c38626b2182ff575818ba6bd3f4b90464994be14cb354a072")
209                .unwrap();
210        let node_id =
211            PublicKey::from_str("ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6")
212                .unwrap();
213
214        let ticket = BlobTicket {
215            node: NodeAddr::from_parts(node_id, None, []),
216            format: BlobFormat::Raw,
217            hash,
218        };
219        let encoded = ticket.to_string();
220        let stripped = encoded.strip_prefix("blob").unwrap();
221        let base32 = data_encoding::BASE32_NOPAD
222            .decode(stripped.to_ascii_uppercase().as_bytes())
223            .unwrap();
224        let expected = parse_hexdump("
225            00 # discriminator for variant 0
226            ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6 # node id, 32 bytes, see above
227            00 # relay url
228            00 # number of addresses (0)
229            00 # format (raw)
230            0b84d358e4c8be6c38626b2182ff575818ba6bd3f4b90464994be14cb354a072 # hash, 32 bytes, see above
231        ").unwrap();
232        assert_eq_hex!(base32, expected);
233    }
234}