### Basic iroh-gossip Integration with Auto Discovery Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/README.md This example demonstrates setting up an iroh endpoint, initializing gossip, and using the `subscribe_and_join_with_auto_discovery` method with the `RecordPublisher` for decentralized topic bootstrapping. Ensure all necessary imports are present. ```rust use anyhow::Result; use iroh::{Endpoint, SecretKey}; use iroh_gossip::net::Gossip; use ed25519_dalek::SigningKey; // Imports from distributed-topic-tracker use distributed_topic_tracker::{TopicId, AutoDiscoveryGossip, RecordPublisher, Config}; #[tokio::main] async fn main() -> Result<()> { // Generate a new random secret key let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); // Set up endpoint with discovery enabled let endpoint = Endpoint::builder(iroh::endpoint::presets::N0) .secret_key(secret_key.clone()) .bind() .await?; // Initialize gossip let gossip = Gossip::builder().spawn(endpoint.clone()); // Set up protocol router let _router = iroh::protocol::Router::builder(endpoint.clone()) .accept(iroh_gossip::ALPN, gossip.clone()) .spawn(); // Distributed Topic Tracker let topic_id = TopicId::new("my-iroh-gossip-topic".to_string()); let initial_secret = b"my-initial-secret".to_vec(); let record_publisher = RecordPublisher::new( topic_id.clone(), signing_key.clone(), None, initial_secret, Config::default(), ); // Use new `subscribe_and_join_with_auto_discovery` on Gossip let topic = gossip .subscribe_and_join_with_auto_discovery(record_publisher) .await?; println!("[joined topic]"); // Work with the topic (GossipSender/Receiver are clonable) let (_gossip_sender, _gossip_receiver) = topic.split().await?; Ok(()) } ``` -------------------------------- ### Run End-to-End Tests Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/README.md Verify peer discovery across Docker containers. This requires Docker and Docker Compose to be installed. ```bash # Requires Docker and Docker Compose ./test-e2e.sh ``` -------------------------------- ### Send and Receive Gossip Messages Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt After splitting a topic, use `GossipSender` to broadcast messages and `GossipReceiver` to stream incoming events. Both sender and receiver are cloneable. This example demonstrates receiving messages and peer events, and sending a simple text message. ```rust use anyhow::Result; use iroh::{Endpoint, SecretKey}; use iroh_gossip::{api::Event, net::Gossip}; use ed25519_dalek::SigningKey; use distributed_topic_tracker::{AutoDiscoveryGossip, RecordPublisher, TopicId, Config}; #[tokio::main] async fn main() -> Result<()> { let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); let endpoint = Endpoint::builder(iroh::endpoint::presets::N0) .secret_key(secret_key.clone()).bind().await?; let gossip = Gossip::builder().spawn(endpoint.clone()); let _router = iroh::protocol::Router::builder(endpoint.clone()) .accept(iroh_gossip::ALPN, gossip.clone()).spawn(); let record_publisher = RecordPublisher::new( TopicId::new("chat-room".to_string()), signing_key, None, b"shared-secret".to_vec(), Config::default(), ); let (gossip_sender, mut gossip_receiver) = gossip .subscribe_and_join_with_auto_discovery(record_publisher) .await? .split() .await?; // Receive messages in a background task tokio::spawn(async move { while let Ok(event) = gossip_receiver.next().await { match event { Event::Received(msg) => { let sender = &msg.delivered_from.to_string()[..8]; let text = String::from_utf8(msg.content.to_vec()).unwrap_or_default(); println!("Message from {}: {}", sender, text); } Event::NeighborUp(peer) => println!("Peer joined: {}", &peer.to_string()[..8]), _ => {} } } }); // Send a message gossip_sender.broadcast(b"Hello, network!".to_vec()).await?; // Output: Message from : Hello, network! Ok(()) } ``` -------------------------------- ### Dht::get / Dht::put_mutable Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Provides direct, actor-backed access to the DHT for raw mutable record retrieval (`get`) and storage (`put_mutable`). This is intended for custom DHT workflows and internal use by `RecordPublisher`. ```APIDOC ## `Dht::get` / `Dht::put_mutable` — Direct DHT access Low-level actor-backed DHT client for raw mutable record get/put operations. Used internally by `RecordPublisher` but available for custom DHT workflows. ```rust use distributed_topic_tracker::{Dht, DhtConfig, TopicId, signing_keypair, salt}; use tokio_util::sync::CancellationToken; use std::time::Duration; #[tokio::main] async fn main() -> anyhow::Result<()> { let dht_config = DhtConfig::builder() .retries(3) .base_retry_interval(Duration::from_secs(5)) .get_timeout(Duration::from_secs(10)) .put_timeout(Duration::from_secs(10)) .build(); let dht = Dht::new(&dht_config); let topic = TopicId::new("example-topic".to_string()); let unix_minute: u64 = (chrono::Utc::now().timestamp() / 60) as u64; let sign_key = signing_keypair(&topic, unix_minute); let record_salt = salt(&topic, unix_minute).to_vec(); // Retrieve records let items = dht.get( sign_key.verifying_key(), Some(record_salt.clone()), None, // no sequence number filter ).await?; println!("Found {} DHT items", items.len()); // Publish a record (raw bytes, already serialized + encrypted) let data = b"my-raw-record-bytes".to_vec(); dht.put_mutable( mainline::SigningKey::from_bytes(&sign_key.to_bytes()), Some(record_salt), data, i64::MAX, ).await?; Ok(()) } ``` ``` -------------------------------- ### Bootstrapping Pseudocode Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/ARCHITECTURE.md This pseudocode outlines the bootstrapping process, focusing on connecting to topic peers. It includes logic for fetching records, attempting to join peers, and handling cases where no peers are found. ```text loop: if joined(): return sender, receiver minute = first_attempt && check_last_minute_first ? -1 : 0 recs = get_records(unix_minute(minute) - 1) + get_records(unix_minute(minute)) if recs.is_empty(): maybe_publish_this_minute() sleep(no_peers_retry_interval = 1500ms) continue for peer in extract_bootstrap_nodes(recs): if joined(): break join_peer(peer) sleep(per_peer_join_settle_time = 100ms) sleep(join_confirmation_wait_time = 500ms) if joined(): return maybe_publish_this_minute() sleep(discovery_poll_interval = 2000ms) ``` -------------------------------- ### BootstrapConfig::check_older_records_first_on_startup Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Configures the DHT bootstrap process to prioritize checking older records first, which is beneficial for joining long-running clusters. Defaults to false for faster cold-starts. ```APIDOC ## `BootstrapConfig::check_older_records_first_on_startup` — Cold-start vs. existing-cluster tuning Controls which DHT time windows are checked first during bootstrap. Set to `true` to prioritize joining long-running clusters; set to `false` (default) to minimize time-to-bootstrap when multiple nodes start simultaneously. ```rust use std::time::Duration; use distributed_topic_tracker::{Config, BootstrapConfig}; // Optimize for joining an already-running topic (checks minute-1 and minute-2 first) let existing_cluster_config = Config::builder() .bootstrap_config( BootstrapConfig::builder() .check_older_records_first_on_startup(true) .build(), ) .build(); // Optimize for cold-start (2+ nodes launching roughly simultaneously) // Checks current minute and minute-1 (default behavior) let cold_start_config = Config::builder() .bootstrap_config( BootstrapConfig::builder() .check_older_records_first_on_startup(false) // default .no_peers_retry_interval(Duration::from_millis(500)) // faster retry .publish_record_on_startup(true) // advertise immediately .build(), ) .build(); ``` ``` -------------------------------- ### Publishing Procedure: Key Derivation and DHT Query Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/PROTOCOL.md Illustrates the cryptographic key derivation and DHT query process for record discovery during the publishing procedure. Ensure correct topic hash, unix minute, and salt calculation for successful DHT interaction. ```rust keypair_seed = SHA512(topic_hash + unix_minute)[..32] enc_keypair_seed = secret_rotation_function.get_unix_minute_secret(topic_hash, unix_minute, initial_secret_hash) salt = SHA512("salt" + topic_hash + unix_minute)[..32] get_mutable(signing_pubkey, salt) ``` -------------------------------- ### Bootstrap and Join Gossip Topic with Auto Discovery Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Use this method to subscribe to a gossip topic and bootstrap the DHT. It blocks until at least one peer is found or the node is the first to join. Requires setting up an Iroh endpoint and gossip instance. ```rust use anyhow::Result; use ed25519_dalek::SigningKey; use iroh::{Endpoint, SecretKey}; use iroh_gossip::net::Gossip; use distributed_topic_tracker::{AutoDiscoveryGossip, RecordPublisher, TopicId, Config}; #[tokio::main] async fn main() -> Result<()> { let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); let endpoint = Endpoint::builder(iroh::endpoint::presets::N0) .secret_key(secret_key.clone()) .bind() .await?; let gossip = Gossip::builder().spawn(endpoint.clone()); let _router = iroh::protocol::Router::builder(endpoint.clone()) .accept(iroh_gossip::ALPN, gossip.clone()) .spawn(); let record_publisher = RecordPublisher::new( TopicId::new("my-iroh-gossip-topic".to_string()), signing_key, None, b"my-initial-secret".to_vec(), Config::default(), ); // Blocks until joined (or bootstrapped as the first node) let topic = gossip .subscribe_and_join_with_auto_discovery(record_publisher) .await?; println!("[joined topic]"); let (_gossip_sender, _gossip_receiver) = topic.split().await?; Ok(()) } ``` -------------------------------- ### Run Unit Tests Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/README.md Execute the core component tests for the distributed-topic-tracker crate using Cargo. ```bash cargo test ``` -------------------------------- ### Build Full Config with Config::builder Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Use Config::builder() for granular control over all configuration aspects. Defaults are indicated in comments. ```rust use std::time::Duration; use distributed_topic_tracker::{ Config, DhtConfig, BootstrapConfig, PublisherConfig, MergeConfig, BubbleMergeConfig, MessageOverlapMergeConfig, TimeoutConfig, }; let config = Config::builder() .dht_config( DhtConfig::builder() .retries(3) // default: 3 .base_retry_interval(Duration::from_secs(5)) // default: 5s .max_retry_jitter(Duration::from_secs(10)) // default: 10s .get_timeout(Duration::from_secs(10)) // default: 10s .put_timeout(Duration::from_secs(10)) // default: 10s .build(), ) .bootstrap_config( BootstrapConfig::builder() .max_bootstrap_records(5) // default: 5 .publish_record_on_startup(true) // default: true .check_older_records_first_on_startup(false) // default: false .no_peers_retry_interval(Duration::from_millis(1500)) // default: 1500ms .per_peer_join_settle_time(Duration::from_millis(100)) // default: 100ms .join_confirmation_wait_time(Duration::from_millis(500))// default: 500ms .discovery_poll_interval(Duration::from_millis(2000)) // default: 2000ms .build(), ) .max_join_peer_count(4) // default: 4 .publisher_config( PublisherConfig::builder() .initial_delay(Duration::from_secs(10)) // default: 10s .base_interval(Duration::from_secs(10)) // default: 10s .max_jitter(Duration::from_secs(50)) // default: 50s .build(), ) .merge_config( MergeConfig::builder() .bubble_merge( BubbleMergeConfig::builder() .min_neighbors(4) // default: 4 .initial_interval(Duration::from_secs(30)) // default: 30s .base_interval(Duration::from_secs(60)) // default: 60s .max_jitter(Duration::from_secs(120)) // default: 120s .max_join_peers(2) // default: 2 .fail_topic_creation_on_merge_startup_failure(true) // default: true .build(), ) .message_overlap_merge( MessageOverlapMergeConfig::builder() .initial_interval(Duration::from_secs(30)) .base_interval(Duration::from_secs(60)) .max_jitter(Duration::from_secs(120)) .max_join_peers(2) .fail_topic_creation_on_merge_startup_failure(true) .build(), ) .build(), ) .timeouts( TimeoutConfig::builder() .join_peer_timeout(Duration::from_secs(5)) // default: 5s .broadcast_timeout(Duration::from_secs(5)) // default: 5s .broadcast_neighbors_timeout(Duration::from_secs(5)) // default: 5s .build(), ) .build(); ``` -------------------------------- ### Configure Distributed Topic Tracker Parameters Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/README.md Set various timing, retry, and threshold parameters for the DHT, bootstrap, publisher, and merge configurations. This allows fine-grained control over the tracker's behavior. ```rust use std::time::Duration; use distributed_topic_tracker::{ Config, DhtConfig, BootstrapConfig, PublisherConfig, MergeConfig, BubbleMergeConfig, MessageOverlapMergeConfig, TimeoutConfig, }; Config::builder() .dht_config( DhtConfig::builder() .retries(3) .base_retry_interval(Duration::from_secs(5)) .max_retry_jitter(Duration::from_secs(10)) .get_timeout(Duration::from_secs(10)) .put_timeout(Duration::from_secs(10)) .build(), ) .bootstrap_config( BootstrapConfig::builder() .max_bootstrap_records(5) .publish_record_on_startup(true) .check_older_records_first_on_startup(false) .discovery_poll_interval(Duration::from_millis(2000)) .no_peers_retry_interval(Duration::from_millis(1500)) .per_peer_join_settle_time(Duration::from_millis(100)) .join_confirmation_wait_time(Duration::from_millis(500)) .build(), ) .max_join_peer_count(4) .publisher_config( PublisherConfig::builder() .initial_delay(Duration::from_secs(10)) .base_interval(Duration::from_secs(10)) .max_jitter(Duration::from_secs(50)) .build(), ) .merge_config( MergeConfig::builder() .bubble_merge( BubbleMergeConfig::builder() .min_neighbors(4) .initial_interval(Duration::from_secs(30)) .base_interval(Duration::from_secs(60)) .max_jitter(Duration::from_secs(120)) .fail_topic_creation_on_merge_startup_failure(true) .max_join_peers(2) .build(), ) .message_overlap_merge( MessageOverlapMergeConfig::builder() .initial_interval(Duration::from_secs(30)) .base_interval(Duration::from_secs(60)) .max_jitter(Duration::from_secs(120)) .fail_topic_creation_on_merge_startup_failure(true) .max_join_peers(2) .build(), ) .build(), ) .timeouts( TimeoutConfig::builder() .join_peer_timeout(Duration::from_secs(5)) .broadcast_neighbors_timeout(Duration::from_secs(5)) .broadcast_timeout(Duration::from_secs(5)) .build(), ) .build(); ``` -------------------------------- ### Bootstrap Procedure Configuration Parameters Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/PROTOCOL.md Lists default configuration parameters for the bootstrap procedure, including peer counts, timeouts, and intervals. These values can be adjusted via `Config` and `BootstrapConfig` to tune network discovery and connection behavior. ```text max_join_peer_count: 4 max_bootstrap_records: 5 DHT get timeout: 10s No peers retry interval: 1500ms Per-peer join settle time: 100ms Final join confirmation wait: 500ms Discovery poll interval: 2000ms Publish on startup: true Check older records first on startup: false ``` -------------------------------- ### Configure DHT Bootstrap Behavior Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Tune DHT bootstrap by controlling whether older time windows are checked first. Set `check_older_records_first_on_startup` to `true` to prioritize joining existing clusters, or `false` (default) for faster cold-starts. ```rust use std::time::Duration; use distributed_topic_tracker::{Config, BootstrapConfig}; // Optimize for joining an already-running topic (checks minute-1 and minute-2 first) let existing_cluster_config = Config::builder() .bootstrap_config( BootstrapConfig::builder() .check_older_records_first_on_startup(true) .build(), ) .build(); // Optimize for cold-start (2+ nodes launching roughly simultaneously) // Checks current minute and minute-1 (default behavior) let cold_start_config = Config::builder() .bootstrap_config( BootstrapConfig::builder() .check_older_records_first_on_startup(false) // default .no_peers_retry_interval(Duration::from_millis(500)) // faster retry .publish_record_on_startup(true) // advertise immediately .build(), ) .build(); ``` -------------------------------- ### Add Config Parameter to RecordPublisher::new Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/README.md The `RecordPublisher::new` function now requires a `Config` parameter. Use `Config::default()` for standard configurations or customize as needed. ```rust let publisher = RecordPublisher::new(topic, pub_key, signing_key, None, secret); ``` ```rust use distributed_topic_tracker::Config; let publisher = RecordPublisher::new(topic, signing_key, None, secret, Config::default()); ``` ```rust let publisher = RecordPublisher::builder(topic_id, signing_key, initial_secret) .config( Config::builder() .max_join_peer_count(4) .build(), ) .build(); ``` -------------------------------- ### Publishing Procedure: Record Creation and Signing Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/PROTOCOL.md Details the preparation of data structures for a new record, including active peers and recent message hashes, before signing and encryption. The content is serialized and signed using the node's ed25519 key. ```rust active_peers[5] last_message_hashes[5] Record::sign(topic_hash, unix_minute, pub_key, content) ``` -------------------------------- ### Ergonomic RecordPublisher Construction Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/README.md Use `RecordPublisher::builder()` for a more ergonomic way to construct a `RecordPublisher` instance. This method allows setting the topic, signing key, secret, and configuration. ```rust let publisher = RecordPublisher::builder("my-topic", signing_key, b"secret") .secret_rotation(rotation_handle) .config(config) .build(); ``` -------------------------------- ### Build RecordPublisher with Builder API Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Provides an ergonomic builder for constructing a RecordPublisher with fluent configuration of bootstrap and DHT settings. Allows incremental attachment of secret rotation and configuration options. ```rust use distributed_topic_tracker::{ RecordPublisher, TopicId, Config, RotationHandle, DefaultSecretRotation, BootstrapConfig, DhtConfig, }; use ed25519_dalek::SigningKey; use iroh::SecretKey; use std::time::Duration; let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); let config = Config::builder() .bootstrap_config( BootstrapConfig::builder() .max_bootstrap_records(5) .publish_record_on_startup(true) .build(), ) .dht_config( DhtConfig::builder() .retries(3) .get_timeout(Duration::from_secs(10)) .build(), ) .build(); let publisher = RecordPublisher::builder( "my-iroh-gossip-topic", signing_key, b"my-shared-secret", ) .config(config) .secret_rotation(RotationHandle::new(DefaultSecretRotation)) .build(); ``` -------------------------------- ### Construct RecordPublisher Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Creates a publisher for DHT record creation, encryption, and signing. Requires a topic ID, Ed25519 signing key, initial shared secret, and configuration. The publisher provides access to its public key, topic ID, and configuration. ```rust use distributed_topic_tracker::{RecordPublisher, TopicId, Config}; use ed25519_dalek::SigningKey; use iroh::SecretKey; let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); let topic_id = TopicId::new("my-iroh-gossip-topic".to_string()); let initial_secret = b"my-shared-secret".to_vec(); let record_publisher = RecordPublisher::new( topic_id, signing_key, None, // use DefaultSecretRotation initial_secret, Config::default(), ); // Accessors let pub_key = record_publisher.pub_key(); // VerifyingKey let topic = record_publisher.topic_id(); // &TopicId let config = record_publisher.config(); // &Config ``` -------------------------------- ### Key Derivation Flow Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/ARCHITECTURE.md Illustrates the process of deriving signing and encryption keys from topic and minute using SHA512 and Ed25519. ```mermaid flowchart TD T[topic_hash] --> A[SHA512 topic+minute] M[unix_minute] --> A A --> S[signing_keypair seed -> Ed25519] T --> L["salt = SHA512('salt' + topic + minute)[..32]"] M --> L T --> R[secret_rotation topic,minute,initial_secret_hash] M --> R R --> E[encryption_keypair seed -> Ed25519] ``` -------------------------------- ### Add Dependencies to Cargo.toml Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/README.md Include these dependencies in your Cargo.toml file to use the distributed-topic-tracker crate and its related libraries. ```toml [dependencies] anyhow = "1" tokio = "1" ed25519-dalek = "3.0.0-pre.6" iroh = "0.98" iroh-gossip = "0.98" distributed-topic-tracker = "0.3" ``` -------------------------------- ### Low-Level Key Derivation Primitives in Rust Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Derive DHT signing keys, encryption keys, and record salts deterministically for a given topic and time slot. Useful for custom DHT interactions or testing purposes. ```rust use distributed_topic_tracker::{ TopicId, RotationHandle, DefaultSecretRotation, signing_keypair, encryption_keypair, salt, }; use sha2::Digest; let topic = TopicId::new("my-topic".to_string()); let unix_minute: u64 = (chrono::Utc::now().timestamp() / 60) as u64; // Deterministic DHT routing key (public, same for all nodes in topic+minute) let sign_key = signing_keypair(&topic, unix_minute); let dht_routing_pub_key = sign_key.verifying_key(); // Per-minute salt for DHT mutable record slot let record_salt: [u8; 32] = salt(&topic, unix_minute); // Encryption keypair (private, derived from shared secret) let rotation = RotationHandle::new(DefaultSecretRotation); let mut initial_secret_hash_bytes = sha2::Sha512::new(); initial_secret_hash_bytes.update(b"my-shared-secret"); let initial_secret_hash: [u8; 32] = initial_secret_hash_bytes.finalize()[..32] .try_into().unwrap(); let enc_key = encryption_keypair(&topic, &rotation, initial_secret_hash, unix_minute); println!("DHT pub key (routing): {:?}", &dht_routing_pub_key.as_bytes()[..8]); println!("Salt: {:?}", &record_salt[..8]); // DHT pub key (routing): [...] // Salt: [...] ``` -------------------------------- ### Manually Sign, Verify, and Encrypt DHT Records Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Use this low-level API for custom protocols or testing when internal handling by RecordPublisher is not sufficient. Ensure all necessary keys and topic information are correctly generated. ```rust use distributed_topic_tracker::{ TopicId, RotationHandle, DefaultSecretRotation, Record, EncryptedRecord, signing_keypair, encryption_keypair, salt, }; use sha2::Digest; use ed25519_dalek::SigningKey; use getrandom::rand_core::OsRng; let topic = TopicId::new("custom-topic".to_string()); let unix_minute: u64 = (chrono::Utc::now().timestamp() / 60) as u64; // Each node signs with its own key let node_signing_key = SigningKey::generate(&mut OsRng); // Custom content (any serde-serializable type) #[derive(serde::Serialize, serde::Deserialize)] struct MyContent { value: u32 } let record = Record::sign( topic.hash(), unix_minute, MyContent { value: 42 }, &node_signing_key, ).unwrap(); // Verify signature, topic, and timestamp record.verify(&topic.hash(), unix_minute).unwrap(); // Encrypt for DHT storage let rotation = RotationHandle::new(DefaultSecretRotation); let mut h = sha2::Sha512::new(); h.update(b"shared-secret"); let secret_hash: [u8; 32] = h.finalize()[..32].try_into().unwrap(); let enc_key = encryption_keypair(&topic, &rotation, secret_hash, unix_minute); let encrypted: EncryptedRecord = record.encrypt(&enc_key); let bytes: Vec = encrypted.to_bytes().unwrap(); // Deserialize and decrypt let enc2 = EncryptedRecord::from_bytes(bytes).unwrap(); let decrypted = enc2.decrypt(&enc_key).unwrap(); assert_eq!(decrypted.topic(), topic.hash()); assert_eq!(decrypted.unix_minute(), unix_minute); let content: MyContent = decrypted.content().unwrap(); assert_eq!(content.value, 42); ``` -------------------------------- ### RecordPublisher::new Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Constructs a new `RecordPublisher` responsible for creating, encrypting, signing, and publishing records to the DHT. It requires a topic ID, a signing key, and an initial shared secret. ```APIDOC ## RecordPublisher::new — Construct a record publisher Creates a publisher that handles DHT record creation, encryption, signing, and publication. Takes a topic ID, an Ed25519 signing key, an optional custom secret rotation handle, an initial shared secret (used to derive per-minute encryption keys), and a `Config`. ### Method Associated function (constructor) ### Parameters - **topic_id**: `TopicId` - The identifier for the topic. - **signing_key**: `ed25519_dalek::SigningKey` - The Ed25519 key used for signing records. - **secret_rotation**: `Option` - An optional handle for managing secret rotation. If `None`, `DefaultSecretRotation` is used. - **initial_secret**: `Vec` - The initial shared secret used for deriving per-minute encryption keys. - **config**: `Config` - Configuration settings for the record publisher. ### Returns - `RecordPublisher` - A new instance of the record publisher. ### Examples ```rust use distributed_topic_tracker::{RecordPublisher, TopicId, Config}; use ed25519_dalek::SigningKey; use iroh::SecretKey; let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); let topic_id = TopicId::new("my-iroh-gossip-topic".to_string()); let initial_secret = b"my-shared-secret".to_vec(); let record_publisher = RecordPublisher::new( topic_id, signing_key, None, // use DefaultSecretRotation initial_secret, Config::default(), ); // Accessors let pub_key = record_publisher.pub_key(); // VerifyingKey let topic = record_publisher.topic_id(); // &TopicId let config = record_publisher.config(); // &Config ``` ``` -------------------------------- ### RecordPublisher::builder Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Provides a fluent builder API for constructing a `RecordPublisher` with customizable configuration options, including bootstrap settings and DHT parameters. ```APIDOC ## RecordPublisher::builder — Ergonomic builder for `RecordPublisher` Provides a fluent builder API for constructing a `RecordPublisher`, allowing optional secret rotation and configuration to be attached incrementally. ### Method Associated function (builder pattern) ### Parameters - **topic_id_str**: `&str` - The string representation of the topic ID. - **signing_key**: `ed25519_dalek::SigningKey` - The Ed25519 key used for signing records. - **shared_secret**: `&[u8]` - The shared secret used for deriving encryption keys. ### Builder Methods - `.config(config: Config)`: Sets the `Config` for the publisher. - `.secret_rotation(rotation_handle: RotationHandle)`: Sets a custom secret rotation handle. - `.build()`: Constructs and returns the `RecordPublisher` instance. ### Examples ```rust use distributed_topic_tracker::{ RecordPublisher, TopicId, Config, RotationHandle, DefaultSecretRotation, BootstrapConfig, DhtConfig, }; use ed25519_dalek::SigningKey; use iroh::SecretKey; use std::time::Duration; let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); let config = Config::builder() .bootstrap_config( BootstrapConfig::builder() .max_bootstrap_records(5) .publish_record_on_startup(true) .build(), ) .dht_config( DhtConfig::builder() .retries(3) .get_timeout(Duration::from_secs(10)) .build(), ) .build(); let publisher = RecordPublisher::builder( "my-iroh-gossip-topic", signing_key, b"my-shared-secret", ) .config(config) .secret_rotation(RotationHandle::new(DefaultSecretRotation)) .build(); ``` ``` -------------------------------- ### Record::sign / Record::verify / Record::encrypt Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Provides low-level control over the creation, verification, and encryption of DHT records. This is typically handled internally but is exposed for custom protocols or testing scenarios. ```APIDOC ## `Record::sign` / `Record::verify` / `Record::encrypt` — Manual record lifecycle Low-level API for creating, verifying, and encrypting DHT records directly. Normally handled internally by `RecordPublisher`, but exposed for custom protocols or testing. ```rust use distributed_topic_tracker::{ TopicId, RotationHandle, DefaultSecretRotation, Record, EncryptedRecord, signing_keypair, encryption_keypair, salt, }; use sha2::Digest; use ed25519_dalek::SigningKey; use getrandom::rand_core::OsRng; let topic = TopicId::new("custom-topic".to_string()); let unix_minute: u64 = (chrono::Utc::now().timestamp() / 60) as u64; // Each node signs with its own key let node_signing_key = SigningKey::generate(&mut OsRng); // Custom content (any serde-serializable type) #[derive(serde::Serialize, serde::Deserialize)] struct MyContent { value: u32 } let record = Record::sign( topic.hash(), unix_minute, MyContent { value: 42 }, &node_signing_key, ).unwrap(); // Verify signature, topic, and timestamp record.verify(&topic.hash(), unix_minute).unwrap(); // Encrypt for DHT storage let rotation = RotationHandle::new(DefaultSecretRotation); let mut h = sha2::Sha512::new(); h.update(b"shared-secret"); let secret_hash: [u8; 32] = h.finalize()[..32].try_into().unwrap(); let enc_key = encryption_keypair(&topic, &rotation, secret_hash, unix_minute); let encrypted: EncryptedRecord = record.encrypt(&enc_key); let bytes: Vec = encrypted.to_bytes().unwrap(); // Deserialize and decrypt let enc2 = EncryptedRecord::from_bytes(bytes).unwrap(); let decrypted = enc2.decrypt(&enc_key).unwrap(); assert_eq!(decrypted.topic(), topic.hash()); assert_eq!(decrypted.unix_minute(), unix_minute); let content: MyContent = decrypted.content().unwrap(); assert_eq!(content.value, 42); ``` ``` -------------------------------- ### Remove MAX_BOOTSTRAP_RECORDS, use BootstrapConfig Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/README.md The `MAX_BOOTSTRAP_RECORDS` constant has been removed. Configure the per-minute record cap using `BootstrapConfig::max_bootstrap_records` within the `Config` object. ```rust use distributed_topic_tracker::MAX_BOOTSTRAP_RECORDS; // was 100 ``` ```rust let config = Config::builder() .bootstrap_config( BootstrapConfig::builder() .max_bootstrap_records(10) .build() ) .build(); ``` -------------------------------- ### Publisher Actor Loop Pseudocode Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/ARCHITECTURE.md This pseudocode describes the publisher actor's loop, responsible for publishing participation records to the DHT. It includes logic for rate-limiting based on existing records and resetting the publishing interval with jitter. ```text // Publisher actor loop (interval: base_interval + random jitter) on tick: records = get_records(unix_minute(0)) if records.len >= max_bootstrap_records(5): return rec = make_record(neighbors(<=5), last_hashes(<=5)) enc = encrypt(sign(rec)) publish(enc) reset_ticker(base_interval + random(0, max_jitter)) ``` -------------------------------- ### Publishing Procedure: DHT Put with Retries Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/PROTOCOL.md Shows the process of publishing a signed and encrypted record to the DHT using `Dht::put_mutable()`. This operation includes retry logic with jittered intervals to prevent synchronized access patterns and handle network issues. ```rust Dht::put_mutable() ``` -------------------------------- ### AutoDiscoveryGossip::subscribe_and_join_with_auto_discovery Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Extension method to subscribe to a gossip topic and join it using auto-discovery. It runs the DHT bootstrap loop until at least one peer is found or the node is the first, then spawns background publisher and merge actors. Returns a Topic handle. ```APIDOC ## `AutoDiscoveryGossip::subscribe_and_join_with_auto_discovery` — Bootstrap and join a gossip topic Extension method added to `iroh_gossip::net::Gossip` by this library. Subscribes to the topic, runs the DHT bootstrap loop until at least one peer is found (or the node is the first), then spawns the background publisher and merge actors. Returns a `Topic` handle. ### Method `subscribe_and_join_with_auto_discovery` ### Parameters - `record_publisher`: An instance of `RecordPublisher` containing topic information and configuration. ### Returns A `Topic` handle which can be used to split into sender and receiver. ### Example ```rust use anyhow::Result; use ed25519_dalek::SigningKey; use iroh::{Endpoint, SecretKey}; use iroh_gossip::net::Gossip; use distributed_topic_tracker::{AutoDiscoveryGossip, RecordPublisher, TopicId, Config}; #[tokio::main] async fn main() -> Result<()> { let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); let endpoint = Endpoint::builder(iroh::endpoint::presets::N0) .secret_key(secret_key.clone()) .bind() .await?; let gossip = Gossip::builder().spawn(endpoint.clone()); let _router = iroh::protocol::Router::builder(endpoint.clone()) .accept(iroh_gossip::ALPN, gossip.clone()) .spawn(); let record_publisher = RecordPublisher::new( TopicId::new("my-iroh-gossip-topic".to_string()), signing_key, None, b"my-initial-secret".to_vec(), Config::default(), ); // Blocks until joined (or bootstrapped as the first node) let topic = gossip .subscribe_and_join_with_auto_discovery(record_publisher) .await?; println!("[joined topic]"); let (_gossip_sender, _gossip_receiver) = topic.split().await?; Ok(()) } ``` ``` -------------------------------- ### Disable Publisher with max_bootstrap_records Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Setting `max_bootstrap_records` to 0 automatically disables the publisher. A warning is logged if the publisher is explicitly enabled but this condition is met. ```rust // Setting max_bootstrap_records to 0 auto-disables the publisher let publish_disabled_config = Config::builder() .bootstrap_config( BootstrapConfig::builder() .max_bootstrap_records(0) .build(), ) .build(); // Warning: Publisher is enabled via PublisherConfig::Enabled(_) but max_bootstrap_records is 0... ``` -------------------------------- ### Create TopicId from String Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Create a stable topic identifier by hashing a string with SHA-512. Supports conversion from various string and byte types. The original string is not stored, only the 32-byte hash. ```rust use distributed_topic_tracker::TopicId; use std::str::FromStr; // From a string let topic = TopicId::new("my-chat-room".to_string()); // Via From<&str> let topic: TopicId = "my-chat-room".into(); // Via FromStr let topic: TopicId = "my-chat-room".parse().unwrap(); // From a pre-computed 32-byte hash let hash: [u8; 32] = [0u8; 32]; let topic = TopicId::from_hash(&hash); // Access the underlying hash bytes let hash_bytes: [u8; 32] = topic.hash(); println!("Topic hash: {:?}", &hash_bytes[..8]); // Topic hash: [...] ``` -------------------------------- ### Record Data Model Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/ARCHITECTURE.md Defines the structure of a record containing topic hash, timestamp, publisher key, content, and signature. ```mermaid classDiagram class Record { +topic: [u8;32] +unix_minute: u64 +pub_key: [u8;32] +content: GossipRecordContent +signature: [u8;64] } class GossipRecordContent { +active_peers: [[u8;32];5] +last_message_hashes: [[u8;32];5] } class EncryptedRecord { +encrypted_record: Vec +encrypted_decryption_key: Vec } Record --* GossipRecordContent : content deserializes to ``` -------------------------------- ### Create EncryptedRecord Structure Source: https://github.com/rustonbsd/distributed-topic-tracker/blob/main/PROTOCOL.md Defines the structure for an encrypted record, combining the encrypted data with the encrypted one-time decryption key. ```rust EncryptedRecord { encrypted_record: encrypted_record, encrypted_decryption_key: encrypted_decryption_key, } ``` -------------------------------- ### Direct DHT Access for Mutable Records Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Utilize the low-level `Dht::get` and `Dht::put_mutable` for raw DHT operations. Configure retry mechanisms and timeouts for robust interaction. This is suitable for custom DHT workflows. ```rust use distributed_topic_tracker::{Dht, DhtConfig, TopicId, signing_keypair, salt}; use tokio_util::sync::CancellationToken; use std::time::Duration; #[tokio::main] async fn main() -> anyhow::Result<()> { let dht_config = DhtConfig::builder() .retries(3) .base_retry_interval(Duration::from_secs(5)) .get_timeout(Duration::from_secs(10)) .put_timeout(Duration::from_secs(10)) .build(); let dht = Dht::new(&dht_config); let topic = TopicId::new("example-topic".to_string()); let unix_minute: u64 = (chrono::Utc::now().timestamp() / 60) as u64; let sign_key = signing_keypair(&topic, unix_minute); let record_salt = salt(&topic, unix_minute).to_vec(); // Retrieve records let items = dht.get( sign_key.verifying_key(), Some(record_salt.clone()), None, // no sequence number filter ).await?; println!("Found {} DHT items", items.len()); // Publish a record (raw bytes, already serialized + encrypted) let data = b"my-raw-record-bytes".to_vec(); dht.put_mutable( mainline::SigningKey::from_bytes(&sign_key.to_bytes()), Some(record_salt), data, i64::MAX, ).await?; Ok(()) } ``` -------------------------------- ### TopicId::new Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Creates a stable topic identifier by hashing an arbitrary string or byte slice using SHA-512. The resulting 32-byte hash uniquely represents the topic. ```APIDOC ## TopicId::new — Create a topic identifier from a string Hashes an arbitrary string (or bytes) with SHA-512 and uses the first 32 bytes as the stable topic identifier. Supports conversion from `&str`, `String`, `Vec`, and `FromStr`. Only the 32-byte hash is stored; the original string is not retained. ### Method Associated function (constructor-like) ### Parameters - **input**: `impl Into>` - The string or byte slice to hash. ### Returns - `TopicId` - A new topic identifier based on the SHA-512 hash. ### Examples ```rust use distributed_topic_tracker::TopicId; use std::str::FromStr; // From a string let topic = TopicId::new("my-chat-room".to_string()); // Via From<&str> let topic: TopicId = "my-chat-room".into(); // Via FromStr let topic: TopicId = "my-chat-room".parse().unwrap(); // From a pre-computed 32-byte hash let hash: [u8; 32] = [0u8; 32]; let topic = TopicId::from_hash(&hash); // Access the underlying hash bytes let hash_bytes: [u8; 32] = topic.hash(); println!("Topic hash: {:?}", &hash_bytes[..8]); ``` ``` -------------------------------- ### GossipSender::broadcast / GossipReceiver::next Source: https://context7.com/rustonbsd/distributed-topic-tracker/llms.txt Methods for sending and receiving gossip messages after splitting a `Topic`. `GossipSender` broadcasts byte messages, and `GossipReceiver` streams incoming `Event`s. Both are cheaply clonable. ```APIDOC ## `GossipSender::broadcast` / `GossipReceiver::next` — Send and receive gossip messages After splitting a `Topic` via `.split()`, `GossipSender` broadcasts byte messages to all topic peers and `GossipReceiver` streams incoming `Event`s. Both types are cheaply clonable. ### Methods - `broadcast(message: Vec) -> Result<()>`: Sends a byte message to all peers in the topic. - `next() -> Result`: Streams incoming `Event`s from the topic. ### Parameters - `broadcast`: Takes a `Vec` representing the message content. - `next`: No parameters. ### Returns - `broadcast`: Returns `Ok(())` on success. - `next`: Returns an `Event` enum variant containing received messages or peer events. ### Example ```rust use anyhow::Result; use iroh::{Endpoint, SecretKey}; use iroh_gossip::{api::Event, net::Gossip}; use ed25519_dalek::SigningKey; use distributed_topic_tracker::{AutoDiscoveryGossip, RecordPublisher, TopicId, Config}; #[tokio::main] async fn main() -> Result<()> { let secret_key = SecretKey::generate(); let signing_key = SigningKey::from_bytes(&secret_key.to_bytes()); let endpoint = Endpoint::builder(iroh::endpoint::presets::N0) .secret_key(secret_key.clone()).bind().await?; let gossip = Gossip::builder().spawn(endpoint.clone()); let _router = iroh::protocol::Router::builder(endpoint.clone()) .accept(iroh_gossip::ALPN, gossip.clone()).spawn(); let record_publisher = RecordPublisher::new( TopicId::new("chat-room".to_string()), signing_key, None, b"shared-secret".to_vec(), Config::default(), ); let (gossip_sender, mut gossip_receiver) = gossip .subscribe_and_join_with_auto_discovery(record_publisher) .await? .split() .await?; // Receive messages in a background task tokio::spawn(async move { while let Ok(event) = gossip_receiver.next().await { match event { Event::Received(msg) => { let sender = &msg.delivered_from.to_string()[..8]; let text = String::from_utf8(msg.content.to_vec()).unwrap_or_default(); println!("Message from {}: {}", sender, text); } Event::NeighborUp(peer) => println!("Peer joined: {}", &peer.to_string()[..8]), _ => {{}} } } }); // Send a message gossip_sender.broadcast(b"Hello, network!".to_vec()).await?; // Output: Message from : Hello, network! Ok(()) } ``` ```