Skip to main content

hashiverse_lib/tools/
types.rs

1//! # Core 32-byte newtypes and cryptographic primitive wrappers
2//!
3//! The "nouns" of the hashiverse protocol. Every one of these is a transparent newtype
4//! over a fixed-size byte array or an underlying crypto library type, so that
5//! function signatures throughout the crate can encode their intent in the type system
6//! rather than relying on correctly-named `[u8; 32]` parameters.
7//!
8//! ## Addressing
9//!
10//! - [`Id`] — 32-byte opaque identifier used pervasively (peer IDs, client IDs, post IDs,
11//!   bucket location IDs, Kademlia keys). Ordered by natural byte comparison so it plays
12//!   well with XOR-distance routing.
13//! - [`struct@Hash`] — 32-byte Blake3 digest. Structurally equivalent to `Id` but
14//!   semantically distinct — "a hash of something" vs "a handle to something".
15//!
16//! ## Cryptography
17//!
18//! - [`SignatureKey`] / [`VerificationKey`] — Ed25519 secret and public halves.
19//! - [`VerificationKeyBytes`] — the wire-format serialisation of a public key.
20//! - [`Signature`] — a 64-byte Ed25519 signature.
21//! - [`PQCommitmentBytes`] — the 32-byte post-quantum commitment (see
22//!   [`crate::tools::keys_post_quantum`]).
23//!
24//! ## Proof-of-work
25//!
26//! - [`Pow`] — a leading-zero-bit count, effectively a difficulty level.
27//! - [`Salt`] — the random bytes searched over during PoW.
28//!
29//! Every type exposes `from_hex_str` / `to_hex_str`, `from_slice`, and `from_buf`
30//! constructors so they can be produced from URLs, wire bytes, or `bytes::Buf` streams
31//! with consistent error handling.
32
33use crate::tools::time::{DurationMillis, TimeMillis};
34use crate::tools::{hashing, tools};
35use bytes::Buf;
36use ed25519_dalek::pkcs8::{EncodePrivateKey, SecretDocument};
37use serde::{Deserialize, Serialize};
38use serde_with::{hex::Hex, serde_as};
39use std::fmt;
40
41fn from_buf<const N: usize>(buf: &mut impl Buf, field: &str) -> anyhow::Result<[u8; N]> {
42    anyhow::ensure!(buf.remaining() >= N, "Buffer too short for {}: need {}, have {}", field, N, buf.remaining());
43    let mut arr = [0u8; N];
44    buf.copy_to_slice(&mut arr);
45    Ok(arr)
46}
47
48pub const ID_BYTES: usize = 32;
49
50/// A 32-byte opaque identifier used pervasively throughout the protocol.
51///
52/// `Id` is the common currency of addressing in hashiverse: peer IDs, client IDs, post IDs,
53/// hashtag IDs, bucket location IDs, and Kademlia keys are all `Id` values. The 32-byte size
54/// matches the output of Blake3 and the other cryptographic hashes in the PoW chain, so IDs
55/// are typically derived by hashing some canonical content (see [`Id::from_hash`] and
56/// [`Id::from_hashtag_str`]) — though they can also be random ([`Id::random`]).
57///
58/// XOR distance between IDs drives the Kademlia DHT routing logic in [`crate::client::peer_tracker`],
59/// so `Id` also implements `Ord` to allow placement in sorted structures.
60///
61/// `Display` and `Debug` print only the first four bytes as hex followed by `...` to keep logs
62/// readable; use [`Id::to_hex_str`] when you need the full value.
63#[serde_as]
64#[derive(Ord, PartialOrd, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
65pub struct Id(#[serde_as(as = "Hex")] pub [u8; ID_BYTES]);
66
67impl Id {
68    pub fn zero() -> Self {
69        let bytes = [0; ID_BYTES];
70        Self(bytes)
71    }
72
73    pub fn is_zero(&self) -> bool {
74        tools::are_all_zeros(&self.0)
75    }
76
77    pub fn random() -> Self {
78        let mut bytes = [0; ID_BYTES];
79        tools::random_fill_bytes(&mut bytes);
80        Self(bytes)
81    }
82
83    pub fn from_hex_str(str: &str) -> anyhow::Result<Self> {
84        tools::from_hex_str::<Self, ID_BYTES>(str, Self)
85    }
86
87    pub fn to_hex_str(&self) -> String {
88        hex::encode(self.0)
89    }
90
91    pub fn as_bytes(&self) -> &[u8; ID_BYTES] {
92        &self.0
93    }
94
95    pub fn from_slice(bytes: &[u8]) -> anyhow::Result<Self> {
96        let arr: [u8; ID_BYTES] = bytes.try_into().map_err(|_| anyhow::anyhow!("Invalid id length: expected {}, got {}", ID_BYTES, bytes.len()))?;
97        Ok(Self(arr))
98    }
99
100    pub fn from_buf(buf: &mut impl Buf, field: &str) -> anyhow::Result<Self> {
101        Ok(Self(from_buf::<ID_BYTES>(buf, field)?))
102    }
103
104    pub fn from_hash(hash: Hash) -> anyhow::Result<Id> {
105        if hash.len() != 32 {
106            anyhow::bail!("Invalid Hash length: expected 32 bytes, got {} bytes", hash.len());
107        }
108
109        let id = Id(hash.to_bytes());
110        Ok(id)
111    }
112
113    pub fn from_hashtag_str(hashtag_str: &str) -> anyhow::Result<Id> {
114        let lowercase_str = hashtag_str.to_lowercase();
115        let str_stripped = lowercase_str.strip_prefix('#').unwrap_or(&lowercase_str);
116        let hash = hashing::hash(str_stripped.as_bytes());
117        Id::from_hash(hash)
118    }
119}
120
121impl AsRef<[u8]> for Id {
122    fn as_ref(&self) -> &[u8] {
123        &self.0
124    }
125}
126
127impl fmt::Display for Id {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(f, "{}...", hex::encode(&self.0[0..4]))
130    }
131}
132
133impl fmt::Debug for Id {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        fmt::Display::fmt(self, f)
136    }
137}
138
139pub const SALT_BYTES: usize = 8;
140
141/// An 8-byte nonce used to vary the input of a proof-of-work search.
142///
143/// PoW in hashiverse is computed by hashing a fixed "data hash" concatenated with a `Salt`
144/// and counting the leading zero bits of the result (see [`crate::tools::pow`]). The worker
145/// keeps re-randomizing the `Salt` to explore the hash space until it finds one whose hash
146/// satisfies the required [`Pow`] difficulty. The salt that succeeds is transmitted alongside
147/// the payload so verifiers can re-compute the same hash without re-searching.
148///
149/// `Salt` is deliberately small (8 bytes) because it is only a search-space tag — it does not
150/// need to be unpredictable to an adversary, only varied across attempts.
151#[serde_as]
152#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
153pub struct Salt(#[serde_as(as = "Hex")] pub [u8; SALT_BYTES]);
154
155impl Salt {
156    pub fn zero() -> Self {
157        Self([0; SALT_BYTES])
158    }
159
160    pub fn random() -> Self {
161        let mut bytes = [0; SALT_BYTES];
162        tools::random_fill_bytes(&mut bytes);
163        Self(bytes)
164    }
165
166    pub fn from_slice(bytes: &[u8]) -> anyhow::Result<Self> {
167        let arr: [u8; SALT_BYTES] = bytes.try_into().map_err(|_| anyhow::anyhow!("Invalid salt length: expected {}, got {}", SALT_BYTES, bytes.len()))?;
168        Ok(Self(arr))
169    }
170
171    pub fn from_buf(buf: &mut impl Buf, field: &str) -> anyhow::Result<Self> {
172        Ok(Self(from_buf::<SALT_BYTES>(buf, field)?))
173    }
174
175    pub fn randomize(&mut self) {
176        tools::random_fill_bytes(&mut self.0);
177    }
178}
179
180impl AsRef<[u8]> for Salt {
181    fn as_ref(&self) -> &[u8] {
182        &self.0
183    }
184}
185
186impl std::ops::Deref for Salt {
187    type Target = [u8; SALT_BYTES];
188
189    fn deref(&self) -> &Self::Target {
190        &self.0
191    }
192}
193
194impl fmt::Display for Salt {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        let all_zero = self.0.iter().all(|&b| b == 0);
197        match all_zero {
198            true => write!(f, "0"),
199            false => write!(f, "..."),
200        }
201    }
202}
203
204impl fmt::Debug for Salt {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        fmt::Display::fmt(self, f)
207    }
208}
209
210pub const SIGNATURE_BYTES: usize = 64;
211
212/// A 64-byte Ed25519 signature.
213///
214/// Every authenticated artefact in the protocol — RPC responses, peer announcements, encoded
215/// posts, post-bundle headers, meta posts — carries a `Signature` produced by the author's
216/// [`SignatureKey`] over a canonical byte representation of the artefact. Verification is
217/// performed with the matching [`VerificationKey`] (typically obtained from a [`Peer`]'s
218/// identity). The concrete signing algorithm is Ed25519 via `ed25519-dalek`; post-quantum
219/// signatures (ML-DSA, FN-DSA) are layered on top via the PQ commitment on [`ClientId`]
220/// rather than replacing this type.
221#[serde_as]
222#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
223pub struct Signature(#[serde_as(as = "Hex")] pub [u8; SIGNATURE_BYTES]);
224
225impl Signature {
226    pub fn as_bytes(&self) -> &[u8; SIGNATURE_BYTES] {
227        &self.0
228    }
229
230    pub fn from_bytes_exact(bytes: [u8; SIGNATURE_BYTES]) -> Self {
231        Self(bytes)
232    }
233
234    pub fn from_hex_str(str: &str) -> anyhow::Result<Self> {
235        tools::from_hex_str::<Self, SIGNATURE_BYTES>(str, Self)
236    }
237
238    pub fn to_hex_str(&self) -> String {
239        hex::encode(self.0)
240    }
241
242
243    pub fn from_slice(bytes: &[u8]) -> anyhow::Result<Self> {
244        let arr: [u8; SIGNATURE_BYTES] = bytes.try_into().map_err(|_| anyhow::anyhow!("Invalid signature length: expected {}, got {}", SIGNATURE_BYTES, bytes.len()))?;
245        Ok(Self(arr))
246    }
247
248    pub fn zero() -> Self {
249        Self([0; SIGNATURE_BYTES])
250    }
251
252    pub fn random() -> Self {
253        let mut bytes = [0; SIGNATURE_BYTES];
254        tools::random_fill_bytes(&mut bytes);
255        Self(bytes)
256    }
257
258}
259
260impl fmt::Display for Signature {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        write!(f, "{}...", hex::encode(&self.0[0..4]))
263    }
264}
265
266impl fmt::Debug for Signature {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        fmt::Display::fmt(self, f)
269    }
270}
271
272impl std::ops::Deref for Signature {
273    type Target = [u8; SIGNATURE_BYTES];
274
275    fn deref(&self) -> &Self::Target {
276        &self.0
277    }
278}
279
280// Implement From for easy conversion
281impl From<[u8; SIGNATURE_BYTES]> for Signature {
282    fn from(bytes: [u8; SIGNATURE_BYTES]) -> Self {
283        Self(bytes)
284    }
285}
286
287impl From<Signature> for [u8; SIGNATURE_BYTES] {
288    fn from(sig: Signature) -> Self {
289        sig.0
290    }
291}
292
293/// A private Ed25519 signing key held by a single client identity.
294///
295/// `SignatureKey` is the secret half of the identity established by [`ClientId`]: it is what
296/// a [`crate::client::key_locker::KeyLocker`] guards on behalf of the logged-in user and uses
297/// to sign outbound posts, RPC responses, and peer announcements. Never serialize or leak this
298/// value across trust boundaries. The matching public half is obtained via
299/// [`SignatureKey::verification_key`] and is published freely as part of the client's identity.
300#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
301pub struct SignatureKey(pub ed25519_dalek::SigningKey);
302
303impl SignatureKey {
304    pub fn from_bytes(bytes: &[u8; SIGNATURE_KEY_BYTES]) -> anyhow::Result<Self> {
305        Ok(Self(ed25519_dalek::SigningKey::from_bytes(bytes)))
306    }
307
308    pub fn verification_key(&self) -> VerificationKey {
309        VerificationKey(self.0.verifying_key())
310    }
311
312    pub fn to_pkcs8_der(&self) -> anyhow::Result<SecretDocument> {
313        Ok(self.0.to_pkcs8_der()?)
314    }
315}
316
317impl AsRef<[u8]> for SignatureKey {
318    fn as_ref(&self) -> &[u8] {
319        self.0.as_bytes()
320    }
321}
322
323#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
324pub struct VerificationKey(pub ed25519_dalek::VerifyingKey);
325
326impl VerificationKey {
327    pub fn from_bytes_raw(bytes: &[u8; VERIFICATION_KEY_BYTES]) -> anyhow::Result<Self> {
328        Ok(Self(ed25519_dalek::VerifyingKey::from_bytes(bytes)?))
329    }
330
331    pub fn from_bytes(bytes: &VerificationKeyBytes) -> anyhow::Result<Self> {
332        Ok(Self(ed25519_dalek::VerifyingKey::from_bytes(&bytes.0)?))
333    }
334
335    pub fn to_verification_key_bytes(&self) -> VerificationKeyBytes {
336        VerificationKeyBytes(self.0.to_bytes())
337    }
338
339    pub fn to_hex(&self) -> String {
340        let vkb = self.to_verification_key_bytes();
341        hex::encode(vkb)
342    }
343
344    pub fn from_hex(hex_str: &str) -> anyhow::Result<Self> {
345        let bytes = hex::decode(hex_str)?;
346        if bytes.len() != VERIFICATION_KEY_BYTES {
347            anyhow::bail!("Invalid hex string length for VerificationKeyBytes");
348        }
349
350        Ok(Self(ed25519_dalek::VerifyingKey::from_bytes(<&[u8; 32]>::try_from(bytes.as_slice())?)?))
351    }
352}
353
354impl AsRef<[u8]> for VerificationKey {
355    fn as_ref(&self) -> &[u8] {
356        self.0.as_bytes()
357    }
358}
359
360
361pub const SIGNATURE_KEY_BYTES: usize = 32;
362#[serde_as]
363#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
364pub struct SignatureKeyBytes(#[serde_as(as = "Hex")] pub [u8; SIGNATURE_KEY_BYTES]);
365
366pub const VERIFICATION_KEY_BYTES: usize = 32;
367
368#[serde_as]
369#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Debug)]
370pub struct VerificationKeyBytes(#[serde_as(as = "Hex")] pub [u8; VERIFICATION_KEY_BYTES]);
371
372impl VerificationKeyBytes {
373    pub fn zero() -> Self {
374        Self([0; VERIFICATION_KEY_BYTES])
375    }
376    pub fn to_verification_key(&self) -> anyhow::Result<VerificationKey> {
377        Ok(VerificationKey(ed25519_dalek::VerifyingKey::from_bytes(&self.0).map_err(|_| anyhow::anyhow!("invalid verification key bytes"))?))
378    }
379    pub fn to_hex(&self) -> String {
380        hex::encode(self.0)
381    }
382    pub fn from_hex_str(str: &str) -> anyhow::Result<Self> {
383        tools::from_hex_str::<Self, VERIFICATION_KEY_BYTES>(str, Self)
384    }
385}
386
387impl AsRef<[u8]> for VerificationKeyBytes {
388    fn as_ref(&self) -> &[u8] {
389        &self.0
390    }
391}
392
393pub const PQ_COMMITMENT_BYTES: usize = 32;
394#[serde_as]
395#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Debug)]
396pub struct PQCommitmentBytes(#[serde_as(as = "Hex")] pub [u8; PQ_COMMITMENT_BYTES]);
397
398impl PQCommitmentBytes {
399
400    pub fn to_hex(&self) -> String {
401        hex::encode(self.0)
402    }
403
404    pub fn from_hex_str(str: &str) -> anyhow::Result<Self> {
405        tools::from_hex_str::<Self, PQ_COMMITMENT_BYTES>(str, Self)
406    }
407
408
409    pub fn zero() -> Self {
410        Self([0; PQ_COMMITMENT_BYTES])
411    }
412
413    pub fn from_slice(bytes: &[u8]) -> anyhow::Result<Self> {
414        let arr: [u8; PQ_COMMITMENT_BYTES] = bytes.try_into().map_err(|_| anyhow::anyhow!("Invalid PQCommitmentBytes length: expected {}, got {}", PQ_COMMITMENT_BYTES, bytes.len()))?;
415        Ok(Self(arr))
416    }
417}
418
419impl AsRef<[u8]> for PQCommitmentBytes {
420    fn as_ref(&self) -> &[u8] {
421        &self.0
422    }
423}
424
425
426impl PQCommitmentBytes {
427    pub fn from(pq_commitment_falcon: [u8; 16], pq_commitment_dilithium: [u8; 16]) -> Self {
428        let mut combined = [0u8; 32];
429        combined[..16].copy_from_slice(&pq_commitment_falcon);
430        combined[16..].copy_from_slice(&pq_commitment_dilithium);
431        PQCommitmentBytes(combined)
432    }
433}
434
435pub const HASH_BYTES: usize = 32;
436
437/// A 32-byte cryptographic hash, the standard hash output throughout the protocol.
438///
439/// For general-purpose hashing (content addressing, ID derivation, deduplication) hashiverse
440/// uses Blake3 via [`crate::tools::hashing`]. For proof-of-work, a longer chain spanning
441/// Blake2, Blake3, SHA2, SHA3, Whirlpool, Groestl, and Skein is used instead to resist
442/// ASIC-style shortcuts — see [`crate::tools::pow`]. Either way the 32-byte `Hash` is the
443/// canonical output. [`Id`] is a sibling newtype over the same byte length and conversion
444/// between them is cheap via [`Id::from_hash`].
445///
446/// `Display` and `Debug` deliberately hide the bytes (printing `"..."` or `"0"`) to avoid
447/// noisy logs; use the slice accessors or hex helpers when the raw value is needed.
448#[serde_as]
449#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
450pub struct Hash(#[serde_as(as = "Hex")] pub [u8; HASH_BYTES]);
451
452impl Hash {
453    pub fn zero() -> Self {
454        Self([0; HASH_BYTES])
455    }
456
457    pub fn random() -> Self {
458        let mut bytes = [0; HASH_BYTES];
459        tools::random_fill_bytes(&mut bytes);
460        Self(bytes)
461    }
462
463    pub fn to_bytes(&self) -> [u8; HASH_BYTES] {
464        self.0
465    }
466
467    pub fn as_bytes(&self) -> &[u8; HASH_BYTES] {
468        &self.0
469    }
470
471    pub fn from_slice(bytes: &[u8]) -> anyhow::Result<Self> {
472        if bytes.len() != HASH_BYTES {
473            anyhow::bail!("incorrect byte count: expected {}, got {}", HASH_BYTES, bytes.len())
474        }
475
476        let mut arr = [0u8; HASH_BYTES];
477        arr.copy_from_slice(bytes);
478        Ok(Self(arr))
479    }
480
481    pub fn randomize(&mut self) {
482        tools::random_fill_bytes(&mut self.0);
483    }
484}
485
486impl AsRef<[u8]> for Hash {
487    fn as_ref(&self) -> &[u8] {
488        &self.0
489    }
490}
491
492impl std::ops::Deref for Hash {
493    type Target = [u8; HASH_BYTES];
494
495    fn deref(&self) -> &Self::Target {
496        &self.0
497    }
498}
499
500impl fmt::Display for Hash {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        let all_zero = self.0.iter().all(|&b| b == 0);
503        match all_zero {
504            true => write!(f, "0"),
505            false => write!(f, "..."),
506        }
507    }
508}
509
510impl fmt::Debug for Hash {
511    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512        fmt::Display::fmt(self, f)
513    }
514}
515
516impl From<[u8; HASH_BYTES]> for Hash {
517    fn from(bytes: [u8; HASH_BYTES]) -> Hash {
518        Hash(bytes)
519    }
520}
521
522impl From<&[u8; HASH_BYTES]> for Hash {
523    fn from(bytes: &[u8; HASH_BYTES]) -> Hash {
524        Hash(*bytes)
525    }
526}
527
528#[derive(Debug, PartialEq, Clone)]
529pub struct BucketKey {
530    pub base_id: Id,
531    pub timestamp: TimeMillis,
532    pub granularity: DurationMillis,
533    pub location_id: Id,
534}
535
536/// A proof-of-work difficulty level, expressed as the number of leading zero bits required
537/// on the PoW hash output.
538///
539/// PoW is embedded throughout the protocol: in every RPC packet (see
540/// [`crate::protocol::rpc`]), on peer announcements (see [`crate::protocol::peer`]), and on
541/// reporting / feedback. Encoding difficulty as a single byte keeps packets compact and keeps
542/// comparison cheap — `Ord` on `Pow` is the natural "harder or easier" ordering. Values are
543/// produced by the chained-hash measurement in [`crate::tools::pow::pow_measure_from_data_hash`]
544/// and searched for via [`ParallelPowGenerator`].
545#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
546#[serde(transparent)]
547pub struct Pow(pub u8);
548
549impl Pow {
550    pub const fn new(val: u8) -> Self {
551        Self(val)
552    }
553
554    pub fn as_u8(self) -> u8 {
555        self.0
556    }
557}
558
559impl std::fmt::Display for Pow {
560    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
561        write!(f, "{}", self.0)
562    }
563}
564
565impl From<u8> for Pow {
566    fn from(val: u8) -> Self {
567        Self(val)
568    }
569}
570
571impl From<Pow> for u8 {
572    fn from(pow: Pow) -> u8 {
573        pow.0
574    }
575}
576
577#[cfg(test)]
578mod tests {
579    use crate::tools::keys::Keys;
580    use crate::tools::types::{Id, Salt, VerificationKey};
581
582    #[tokio::test]
583    async fn salt_display_zero_test() {
584        let salt = Salt::zero();
585        assert_eq!(format!("{}", salt), "0");
586    }
587
588    #[tokio::test]
589    async fn salt_display_nonzero_test() {
590        let salt = Salt::random();
591        assert_eq!(format!("{}", salt), "...");
592    }
593
594    #[tokio::test]
595    async fn test_hash_to_base_id() -> anyhow::Result<()> {
596        let hash = "walking";
597        let hash1 = "#walking";
598        let hash2 = "WaLkINg";
599        let hash3 = "#WAlkING";
600        let hash4 = "TALKING";
601
602        let id = Id::from_hashtag_str(hash)?;
603        let id1 = Id::from_hashtag_str(hash1)?;
604        let id2 = Id::from_hashtag_str(hash2)?;
605        let id3 = Id::from_hashtag_str(hash3)?;
606        let id4 = Id::from_hashtag_str(hash4)?;
607
608        assert_eq!(id, id1, "Hash to id should be the same");
609        assert_eq!(id, id2, "Hash to id should be the same");
610        assert_eq!(id, id3, "Hash to id should be the same");
611        assert_ne!(id, id4, "Hash to id should not be the same");
612        Ok(())
613    }
614
615    #[tokio::test]
616    async fn test_id_to_str_reversible() -> anyhow::Result<()> {
617        let id = Id::random();
618        let id_str = id.to_hex_str();
619        let id_from_str = Id::from_hex_str(&id_str)?;
620        assert_eq!(id, id_from_str, "Id to string and string to id should be reversible");
621        Ok(())
622    }
623
624    #[tokio::test]
625    async fn test_verification_key() -> anyhow::Result<()> {
626        let keys = Keys::from_phrase("this is a test")?;
627        let hex = keys.verification_key.to_hex();
628        let vfk = VerificationKey::from_hex(&hex)?;
629        assert_eq!(keys.verification_key, vfk, "Verification key to hex and hex to verification key should be reversible");
630        Ok(())
631    }
632}