lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 5f136f7a29d00ab53676d7f3cd7b9f144a97326d
parent ea16d393a4fbeca4c1b1cedcebe991e1e50244ec
Author: triesap <tyson@radroots.org>
Date:   Mon, 25 May 2026 04:53:27 +0000

local_events: add relay set fingerprint

Diffstat:
Mcrates/local_events/src/lib.rs | 2++
Acrates/local_events/src/relay_set.rs | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/local_events/tests/relay_set.rs | 36++++++++++++++++++++++++++++++++++++
3 files changed, 87 insertions(+), 0 deletions(-)

diff --git a/crates/local_events/src/lib.rs b/crates/local_events/src/lib.rs @@ -4,6 +4,7 @@ mod error; mod migrations; mod models; mod order_work; +mod relay_set; mod store; pub use error::LocalEventsError; @@ -21,4 +22,5 @@ pub use order_work::{ validate_supported_buyer_order_request_local_work_payload, validate_unsupported_buyer_order_request_local_work_payload, }; +pub use relay_set::{CANONICAL_RELAY_SET_FINGERPRINT_VERSION, canonical_relay_set_fingerprint}; pub use store::LocalEventsStore; diff --git a/crates/local_events/src/relay_set.rs b/crates/local_events/src/relay_set.rs @@ -0,0 +1,49 @@ +#![forbid(unsafe_code)] + +use std::collections::BTreeSet; + +const FNV_1A_64_OFFSET_BASIS: u64 = 0xcbf29ce484222325; +const FNV_1A_64_PRIME: u64 = 0x100000001b3; + +pub const CANONICAL_RELAY_SET_FINGERPRINT_VERSION: &str = "radroots-local-events-relay-set-v1"; + +/// Returns the canonical shared local-event relay-set fingerprint. +/// +/// Relay URLs are trimmed, blank entries are discarded, duplicates are removed, +/// and the remaining set is sorted before hashing. +pub fn canonical_relay_set_fingerprint<I, S>(relay_urls: I) -> Option<String> +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + let relays = relay_urls + .into_iter() + .filter_map(|relay_url| { + let relay_url = relay_url.as_ref().trim(); + (!relay_url.is_empty()).then(|| relay_url.to_owned()) + }) + .collect::<BTreeSet<_>>(); + + if relays.is_empty() { + return None; + } + + let mut hash = FNV_1A_64_OFFSET_BASIS; + for relay in relays { + update_hash(&mut hash, relay.len().to_string().as_bytes()); + update_hash(&mut hash, &[0]); + update_hash(&mut hash, relay.as_bytes()); + update_hash(&mut hash, &[0]); + } + + Some(format!( + "{CANONICAL_RELAY_SET_FINGERPRINT_VERSION}:{hash:016x}" + )) +} + +fn update_hash(hash: &mut u64, bytes: &[u8]) { + for byte in bytes { + *hash ^= u64::from(*byte); + *hash = hash.wrapping_mul(FNV_1A_64_PRIME); + } +} diff --git a/crates/local_events/tests/relay_set.rs b/crates/local_events/tests/relay_set.rs @@ -0,0 +1,36 @@ +use radroots_local_events::{ + CANONICAL_RELAY_SET_FINGERPRINT_VERSION, canonical_relay_set_fingerprint, +}; + +#[test] +fn relay_set_fingerprint_trims_sorts_and_dedupes() { + let first = canonical_relay_set_fingerprint([ + " wss://relay-b.example ", + "wss://relay-a.example", + "wss://relay-b.example", + ]) + .expect("fingerprint"); + let second = + canonical_relay_set_fingerprint(["wss://relay-a.example", "wss://relay-b.example"]) + .expect("fingerprint"); + + assert_eq!(first, second); + assert!(first.starts_with(CANONICAL_RELAY_SET_FINGERPRINT_VERSION)); +} + +#[test] +fn relay_set_fingerprint_rejects_empty_entries() { + let fingerprint = canonical_relay_set_fingerprint([" ", "", "\t"]); + + assert_eq!(fingerprint, None); +} + +#[test] +fn relay_set_fingerprint_changes_when_relay_set_changes() { + let first = canonical_relay_set_fingerprint(["wss://relay-a.example"]).expect("fingerprint"); + let second = + canonical_relay_set_fingerprint(["wss://relay-a.example", "wss://relay-b.example"]) + .expect("fingerprint"); + + assert_ne!(first, second); +}