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