diff --git a/Cargo.lock b/Cargo.lock index 31e5c5b7c6..8e896c4048 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2249,7 +2249,6 @@ dependencies = [ "proptest", "rand", "rand_core", - "redb", "serde", "serde_json", "serde_test", diff --git a/iroh-base/Cargo.toml b/iroh-base/Cargo.toml index 24ba44ef1a..9a88a52c4f 100644 --- a/iroh-base/Cargo.toml +++ b/iroh-base/Cargo.toml @@ -20,7 +20,6 @@ blake3 = { version = "1.4.5", package = "iroh-blake3", optional = true } data-encoding = { version = "2.3.3", optional = true } hex = "0.4.3" postcard = { version = "1", default-features = false, features = ["alloc", "use-std", "experimental-derive"], optional = true } -redb = { version = "2.0.0", optional = true } serde = { version = "1", features = ["derive"] } thiserror = "2" @@ -46,11 +45,9 @@ serde_json = "1" serde_test = "1" [features] -default = ["hash", "ticket", "relay"] -hash = ["dep:blake3", "dep:data-encoding", "dep:postcard", "dep:derive_more", "base32"] -ticket = ["base32", "key", "hash"] +default = ["ticket", "relay"] +ticket = ["base32", "key"] base32 = ["dep:data-encoding", "dep:postcard"] -redb = ["dep:redb"] key = [ "dep:ed25519-dalek", "dep:once_cell", @@ -65,6 +62,7 @@ key = [ "dep:derive_more", "dep:getrandom", "base32", + "relay", ] wasm = ["getrandom?/js"] relay = ["dep:url", "dep:derive_more"] diff --git a/iroh-base/src/hash.rs b/iroh-base/src/hash.rs deleted file mode 100644 index 04f73e5f11..0000000000 --- a/iroh-base/src/hash.rs +++ /dev/null @@ -1,565 +0,0 @@ -//! The blake3 hash used in Iroh. - -use std::{borrow::Borrow, fmt, str::FromStr}; - -use postcard::experimental::max_size::MaxSize; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; - -use crate::base32::{self, parse_array_hex_or_base32, HexOrBase32ParseError}; - -/// Hash type used throughout. -#[derive(PartialEq, Eq, Copy, Clone, Hash)] -pub struct Hash(blake3::Hash); - -impl fmt::Debug for Hash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Hash").field(&DD(self.to_hex())).finish() - } -} - -struct DD(T); - -impl fmt::Debug for DD { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl Hash { - /// The hash for the empty byte range (`b""`). - pub const EMPTY: Hash = Hash::from_bytes([ - 175, 19, 73, 185, 245, 249, 161, 166, 160, 64, 77, 234, 54, 220, 201, 73, 155, 203, 37, - 201, 173, 193, 18, 183, 204, 154, 147, 202, 228, 31, 50, 98, - ]); - - /// Calculate the hash of the provided bytes. - pub fn new(buf: impl AsRef<[u8]>) -> Self { - let val = blake3::hash(buf.as_ref()); - Hash(val) - } - - /// Bytes of the hash. - pub fn as_bytes(&self) -> &[u8; 32] { - self.0.as_bytes() - } - - /// Create a `Hash` from its raw bytes representation. - pub const fn from_bytes(bytes: [u8; 32]) -> Self { - Self(blake3::Hash::from_bytes(bytes)) - } - - /// Convert the hash to a hex string. - pub fn to_hex(&self) -> String { - self.0.to_hex().to_string() - } - - /// Convert to a base32 string limited to the first 10 bytes for a friendly string - /// representation of the hash. - pub fn fmt_short(&self) -> String { - base32::fmt_short(self.as_bytes()) - } -} - -impl AsRef<[u8]> for Hash { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl Borrow<[u8]> for Hash { - fn borrow(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl Borrow<[u8; 32]> for Hash { - fn borrow(&self) -> &[u8; 32] { - self.0.as_bytes() - } -} - -impl From for blake3::Hash { - fn from(value: Hash) -> Self { - value.0 - } -} - -impl From for Hash { - fn from(value: blake3::Hash) -> Self { - Hash(value) - } -} - -impl From<[u8; 32]> for Hash { - fn from(value: [u8; 32]) -> Self { - Hash(blake3::Hash::from(value)) - } -} - -impl From for [u8; 32] { - fn from(value: Hash) -> Self { - *value.as_bytes() - } -} - -impl From<&[u8; 32]> for Hash { - fn from(value: &[u8; 32]) -> Self { - Hash(blake3::Hash::from(*value)) - } -} - -impl PartialOrd for Hash { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.0.as_bytes().cmp(other.0.as_bytes())) - } -} - -impl Ord for Hash { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.0.as_bytes().cmp(other.0.as_bytes()) - } -} - -impl fmt::Display for Hash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // result will be 52 bytes - let mut res = [b'b'; 52]; - // write the encoded bytes - data_encoding::BASE32_NOPAD.encode_mut(self.as_bytes(), &mut res); - // convert to string, this is guaranteed to succeed - let t = std::str::from_utf8_mut(res.as_mut()).unwrap(); - // hack since data_encoding doesn't have BASE32LOWER_NOPAD as a const - t.make_ascii_lowercase(); - // write the str, no allocations - f.write_str(t) - } -} - -impl FromStr for Hash { - type Err = HexOrBase32ParseError; - - fn from_str(s: &str) -> Result { - parse_array_hex_or_base32(s).map(Hash::from) - } -} - -impl Serialize for Hash { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - if serializer.is_human_readable() { - serializer.serialize_str(self.to_string().as_str()) - } else { - self.0.as_bytes().serialize(serializer) - } - } -} - -impl<'de> Deserialize<'de> for Hash { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - s.parse().map_err(de::Error::custom) - } else { - let data: [u8; 32] = Deserialize::deserialize(deserializer)?; - Ok(Self(blake3::Hash::from(data))) - } - } -} - -impl MaxSize for Hash { - const POSTCARD_MAX_SIZE: usize = 32; -} - -/// A format identifier -#[derive( - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Default, - Debug, - MaxSize, - Hash, - derive_more::Display, -)] -pub enum BlobFormat { - /// Raw blob - #[default] - Raw, - /// A sequence of BLAKE3 hashes - HashSeq, -} - -impl From for u64 { - fn from(value: BlobFormat) -> Self { - match value { - BlobFormat::Raw => 0, - BlobFormat::HashSeq => 1, - } - } -} - -impl BlobFormat { - /// Is raw format - pub const fn is_raw(&self) -> bool { - matches!(self, BlobFormat::Raw) - } - - /// Is hash seq format - pub const fn is_hash_seq(&self) -> bool { - matches!(self, BlobFormat::HashSeq) - } -} - -/// A hash and format pair -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, MaxSize, Hash)] -pub struct HashAndFormat { - /// The hash - pub hash: Hash, - /// The format - pub format: BlobFormat, -} - -#[cfg(feature = "redb")] -mod redb_support { - use postcard::experimental::max_size::MaxSize; - use redb::{Key as RedbKey, Value as RedbValue}; - - use super::{Hash, HashAndFormat}; - - impl RedbValue for Hash { - type SelfType<'a> = Self; - - type AsBytes<'a> = &'a [u8; 32]; - - fn fixed_width() -> Option { - Some(32) - } - - fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> - where - Self: 'a, - { - let contents: &'a [u8; 32] = data.try_into().unwrap(); - (*contents).into() - } - - fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> - where - Self: 'a, - Self: 'b, - { - value.as_bytes() - } - - fn type_name() -> redb::TypeName { - redb::TypeName::new("iroh_base::Hash") - } - } - - impl RedbKey for Hash { - fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { - data1.cmp(data2) - } - } - - impl RedbValue for HashAndFormat { - type SelfType<'a> = Self; - - type AsBytes<'a> = [u8; Self::POSTCARD_MAX_SIZE]; - - fn fixed_width() -> Option { - Some(Self::POSTCARD_MAX_SIZE) - } - - fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> - where - Self: 'a, - { - let t: &'a [u8; Self::POSTCARD_MAX_SIZE] = data.try_into().unwrap(); - postcard::from_bytes(t.as_slice()).unwrap() - } - - fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> - where - Self: 'a, - Self: 'b, - { - let mut res = [0u8; 33]; - postcard::to_slice(&value, &mut res).unwrap(); - res - } - - fn type_name() -> redb::TypeName { - redb::TypeName::new("iroh_base::HashAndFormat") - } - } -} - -impl HashAndFormat { - /// Create a new hash and format pair. - pub fn new(hash: Hash, format: BlobFormat) -> Self { - Self { hash, format } - } - - /// Create a new hash and format pair, using the default (raw) format. - pub fn raw(hash: Hash) -> Self { - Self { - hash, - format: BlobFormat::Raw, - } - } - - /// Create a new hash and format pair, using the collection format. - pub fn hash_seq(hash: Hash) -> Self { - Self { - hash, - format: BlobFormat::HashSeq, - } - } -} - -impl fmt::Display for HashAndFormat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut slice = [0u8; 65]; - hex::encode_to_slice(self.hash.as_bytes(), &mut slice[1..]).unwrap(); - match self.format { - BlobFormat::Raw => { - write!(f, "{}", std::str::from_utf8(&slice[1..]).unwrap()) - } - BlobFormat::HashSeq => { - slice[0] = b's'; - write!(f, "{}", std::str::from_utf8(&slice).unwrap()) - } - } - } -} - -impl FromStr for HashAndFormat { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let s = s.as_bytes(); - let mut hash = [0u8; 32]; - match s.len() { - 64 => { - hex::decode_to_slice(s, &mut hash)?; - Ok(Self::raw(hash.into())) - } - 65 if s[0].to_ascii_lowercase() == b's' => { - hex::decode_to_slice(&s[1..], &mut hash)?; - Ok(Self::hash_seq(hash.into())) - } - _ => anyhow::bail!("invalid hash and format"), - } - } -} - -impl Serialize for HashAndFormat { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - if serializer.is_human_readable() { - serializer.serialize_str(self.to_string().as_str()) - } else { - (self.hash, self.format).serialize(serializer) - } - } -} - -impl<'de> Deserialize<'de> for HashAndFormat { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - s.parse().map_err(de::Error::custom) - } else { - let (hash, format) = <(Hash, BlobFormat)>::deserialize(deserializer)?; - Ok(Self { hash, format }) - } - } -} - -#[cfg(test)] -mod tests { - - use iroh_test::{assert_eq_hex, hexdump::parse_hexdump}; - use serde_test::{assert_tokens, Configure, Token}; - - use super::*; - - #[test] - fn test_display_parse_roundtrip() { - for i in 0..100 { - let hash: Hash = blake3::hash(&[i]).into(); - let text = hash.to_string(); - let hash1 = text.parse::().unwrap(); - assert_eq!(hash, hash1); - - let text = hash.to_hex(); - let hash1 = Hash::from_str(&text).unwrap(); - assert_eq!(hash, hash1); - } - } - - #[test] - fn test_hash() { - let data = b"hello world"; - let hash = Hash::new(data); - - let encoded = hash.to_string(); - assert_eq!(encoded.parse::().unwrap(), hash); - } - - #[test] - fn test_empty_hash() { - let hash = Hash::new(b""); - assert_eq!(hash, Hash::EMPTY); - } - - #[test] - fn hash_wire_format() { - let hash = Hash::from([0xab; 32]); - let serialized = postcard::to_stdvec(&hash).unwrap(); - let expected = parse_hexdump(r" - ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab # hash - ").unwrap(); - assert_eq_hex!(serialized, expected); - } - - #[cfg(feature = "redb")] - #[test] - fn hash_redb() { - use redb::Value as RedbValue; - let bytes: [u8; 32] = (0..32).collect::>().as_slice().try_into().unwrap(); - let hash = Hash::from(bytes); - assert_eq!(::fixed_width(), Some(32)); - assert_eq!( - ::type_name(), - redb::TypeName::new("iroh_base::Hash") - ); - let serialized = ::as_bytes(&hash); - assert_eq!(serialized, &bytes); - let deserialized = ::from_bytes(serialized.as_slice()); - assert_eq!(deserialized, hash); - let expected = parse_hexdump( - r" - 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f - 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f # hash - ", - ) - .unwrap(); - assert_eq_hex!(serialized, expected); - } - - #[cfg(feature = "redb")] - #[test] - fn hash_and_format_redb() { - use redb::Value as RedbValue; - let hash_bytes: [u8; 32] = (0..32).collect::>().as_slice().try_into().unwrap(); - let hash = Hash::from(hash_bytes); - let haf = HashAndFormat::raw(hash); - assert_eq!(::fixed_width(), Some(33)); - assert_eq!( - ::type_name(), - redb::TypeName::new("iroh_base::HashAndFormat") - ); - let serialized = ::as_bytes(&haf); - let mut bytes = [0u8; 33]; - bytes[0..32].copy_from_slice(&hash_bytes); - assert_eq!(serialized, bytes); - let deserialized = ::from_bytes(serialized.as_slice()); - assert_eq!(deserialized, haf); - let expected = parse_hexdump( - r" - 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f - 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f # hash - 00 # format (raw) - ", - ) - .unwrap(); - assert_eq_hex!(serialized, expected); - } - - #[test] - fn test_hash_serde() { - let hash = Hash::new("hello"); - - // Hashes are serialized as 32 tuples - let mut tokens = Vec::new(); - tokens.push(Token::Tuple { len: 32 }); - for byte in hash.as_bytes() { - tokens.push(Token::U8(*byte)); - } - tokens.push(Token::TupleEnd); - assert_eq!(tokens.len(), 34); - - assert_tokens(&hash.compact(), &tokens); - - let tokens = vec![Token::String( - "5khrmpntq2bjexseshc6ldklwnig56gbj23yvbxjbdcwestheahq", - )]; - assert_tokens(&hash.readable(), &tokens); - } - - #[test] - fn test_hash_postcard() { - let hash = Hash::new("hello"); - let ser = postcard::to_stdvec(&hash).unwrap(); - let de = postcard::from_bytes(&ser).unwrap(); - assert_eq!(hash, de); - - assert_eq!(ser.len(), 32); - } - - #[test] - fn test_hash_json() { - let hash = Hash::new("hello"); - let ser = serde_json::to_string(&hash).unwrap(); - let de = serde_json::from_str(&ser).unwrap(); - assert_eq!(hash, de); - // 52 bytes of base32 + 2 quotes - assert_eq!(ser.len(), 54); - } - - #[test] - fn test_hash_and_format_parse() { - let hash = Hash::new("hello"); - - let expected = HashAndFormat::raw(hash); - let actual = expected.to_string().parse::().unwrap(); - assert_eq!(expected, actual); - - let expected = HashAndFormat::hash_seq(hash); - let actual = expected.to_string().parse::().unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn test_hash_and_format_postcard() { - let haf = HashAndFormat::raw(Hash::new("hello")); - let ser = postcard::to_stdvec(&haf).unwrap(); - let de = postcard::from_bytes(&ser).unwrap(); - assert_eq!(haf, de); - } - - #[test] - fn test_hash_and_format_json() { - let haf = HashAndFormat::raw(Hash::new("hello")); - let ser = serde_json::to_string(&haf).unwrap(); - let de = serde_json::from_str(&ser).unwrap(); - assert_eq!(haf, de); - } -} diff --git a/iroh-base/src/lib.rs b/iroh-base/src/lib.rs index 2db5732ab5..ac2b18c7a4 100644 --- a/iroh-base/src/lib.rs +++ b/iroh-base/src/lib.rs @@ -9,8 +9,6 @@ pub mod base32; #[cfg(feature = "ticket")] pub mod ticket; -#[cfg(feature = "hash")] -mod hash; #[cfg(feature = "key")] mod key; #[cfg(feature = "key")] @@ -18,8 +16,6 @@ mod node_addr; #[cfg(feature = "relay")] mod relay_url; -#[cfg(feature = "hash")] -pub use self::hash::{BlobFormat, Hash, HashAndFormat}; #[cfg(feature = "key")] pub use self::key::{ KeyParsingError, NodeId, PublicKey, SecretKey, SharedSecret, Signature, PUBLIC_KEY_LENGTH, diff --git a/iroh-base/src/ticket.rs b/iroh-base/src/ticket.rs index 6a20f3e0bb..76de6936b8 100644 --- a/iroh-base/src/ticket.rs +++ b/iroh-base/src/ticket.rs @@ -4,10 +4,9 @@ use serde::{Deserialize, Serialize}; use crate::{base32, key::NodeId, relay_url::RelayUrl}; -mod blob; mod node; -pub use self::{blob::BlobTicket, node::NodeTicket}; +pub use self::node::NodeTicket; /// A ticket is a serializable object combining information required for an operation. /// diff --git a/iroh-base/src/ticket/blob.rs b/iroh-base/src/ticket/blob.rs deleted file mode 100644 index 73f1514bce..0000000000 --- a/iroh-base/src/ticket/blob.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Tickets for blobs. -use std::str::FromStr; - -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -use super::{Variant0AddrInfo, Variant0NodeAddr}; -use crate::{ - hash::{BlobFormat, Hash}, - node_addr::NodeAddr, - ticket::{self, Ticket}, -}; - -/// A token containing everything to get a file from the provider. -/// -/// It is a single item which can be easily serialized and deserialized. -#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] -#[display("{}", Ticket::serialize(self))] -pub struct BlobTicket { - /// The provider to get a file from. - node: NodeAddr, - /// The format of the blob. - format: BlobFormat, - /// The hash to retrieve. - hash: Hash, -} - -/// Wire format for [`BlobTicket`]. -/// -/// In the future we might have multiple variants (not versions, since they -/// might be both equally valid), so this is a single variant enum to force -/// postcard to add a discriminator. -#[derive(Serialize, Deserialize)] -enum TicketWireFormat { - Variant0(Variant0BlobTicket), -} - -// Legacy -#[derive(Serialize, Deserialize)] -struct Variant0BlobTicket { - node: Variant0NodeAddr, - format: BlobFormat, - hash: Hash, -} - -impl Ticket for BlobTicket { - const KIND: &'static str = "blob"; - - fn to_bytes(&self) -> Vec { - let data = TicketWireFormat::Variant0(Variant0BlobTicket { - node: Variant0NodeAddr { - node_id: self.node.node_id, - info: Variant0AddrInfo { - relay_url: self.node.relay_url.clone(), - direct_addresses: self.node.direct_addresses.clone(), - }, - }, - format: self.format, - hash: self.hash, - }); - postcard::to_stdvec(&data).expect("postcard serialization failed") - } - - fn from_bytes(bytes: &[u8]) -> std::result::Result { - let res: TicketWireFormat = postcard::from_bytes(bytes).map_err(ticket::Error::Postcard)?; - let TicketWireFormat::Variant0(Variant0BlobTicket { node, format, hash }) = res; - Ok(Self { - node: NodeAddr { - node_id: node.node_id, - relay_url: node.info.relay_url, - direct_addresses: node.info.direct_addresses, - }, - format, - hash, - }) - } -} - -impl FromStr for BlobTicket { - type Err = ticket::Error; - - fn from_str(s: &str) -> Result { - Ticket::deserialize(s) - } -} - -impl BlobTicket { - /// Creates a new ticket. - pub fn new(node: NodeAddr, hash: Hash, format: BlobFormat) -> Result { - Ok(Self { hash, format, node }) - } - - /// The hash of the item this ticket can retrieve. - pub fn hash(&self) -> Hash { - self.hash - } - - /// The [`NodeAddr`] of the provider for this ticket. - pub fn node_addr(&self) -> &NodeAddr { - &self.node - } - - /// The [`BlobFormat`] for this ticket. - pub fn format(&self) -> BlobFormat { - self.format - } - - /// 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) -> (NodeAddr, Hash, BlobFormat) { - let BlobTicket { node, hash, format } = self; - (node, hash, format) - } -} - -impl Serialize for BlobTicket { - fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - serializer.serialize_str(&self.to_string()) - } else { - let BlobTicket { node, format, hash } = self; - (node, format, hash).serialize(serializer) - } - } -} - -impl<'de> Deserialize<'de> for BlobTicket { - fn deserialize>(deserializer: D) -> Result { - 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) = Deserialize::deserialize(deserializer)?; - Self::new(peer, hash, format).map_err(serde::de::Error::custom) - } - } -} - -#[cfg(test)] -mod tests { - use std::net::SocketAddr; - - use iroh_test::{assert_eq_hex, hexdump::parse_hexdump}; - - use super::*; - use crate::{ - base32, - key::{PublicKey, SecretKey}, - }; - - fn make_ticket() -> BlobTicket { - let hash = Hash::new(b"hi there"); - let peer = SecretKey::generate().public(); - let addr = SocketAddr::from_str("127.0.0.1:1234").unwrap(); - let relay_url = None; - BlobTicket { - hash, - node: NodeAddr::from_parts(peer, relay_url, [addr]), - format: BlobFormat::HashSeq, - } - } - - #[test] - fn test_ticket_postcard() { - let ticket = make_ticket(); - let bytes = postcard::to_stdvec(&ticket).unwrap(); - let ticket2: BlobTicket = 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: BlobTicket = serde_json::from_str(&json).unwrap(); - assert_eq!(ticket2, ticket); - } - - #[test] - fn test_ticket_base32() { - let hash = - Hash::from_str("0b84d358e4c8be6c38626b2182ff575818ba6bd3f4b90464994be14cb354a072") - .unwrap(); - let node_id = - PublicKey::from_str("ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6") - .unwrap(); - - let ticket = BlobTicket { - node: NodeAddr::from_parts(node_id, None, []), - format: BlobFormat::Raw, - hash, - }; - let base32 = base32::parse_vec(ticket.to_string().strip_prefix("blob").unwrap()).unwrap(); - let expected = parse_hexdump(" - 00 # discriminator for variant 0 - ae58ff8833241ac82d6ff7611046ed67b5072d142c588d0063e942d9a75502b6 # node id, 32 bytes, see above - 00 # relay url - 00 # number of addresses (0) - 00 # format (raw) - 0b84d358e4c8be6c38626b2182ff575818ba6bd3f4b90464994be14cb354a072 # hash, 32 bytes, see above - ").unwrap(); - assert_eq_hex!(base32, expected); - } -}