Skip to content

Irokle

Irokle is a signed Merkle-DAG operation log for invite-only topics. Application events and membership changes are stored as signed operations. The graph of operations can be used to derive current heads, a history of changes, summaries for syncing, and projections.

Features

  • Signed operations: every event or control change is signed by the peer that authored it.
  • Topic membership: topics are not public broadcast channels; typed access is gated by the current signed member set.
  • Deterministic sync: peers exchange summaries, missing operation closures, requests, and signed acknowledgements.
  • Bounded fanout: topic replication is capped by ReplicationPolicy::max_sync_peers so a node does not sync with every member by default.
  • Observability: sync status records expose pending obligations, failure counts, last errors, last success, and per-state counts.
  • Storage choices: MemoryStorage is available by default; FjallStorage is available behind the fjall feature.
  • Iroh integration: the iroh feature syncs over iroh::Endpoint using PeerId/NodeId dialing.

Minimal Example

use irokle::history::HistoryOrder;use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]#[irokle(type_id = "example.chat.message")]structChatEvent{author:String,text:String,}fnmain() -> irokle::Result<()>{let alice = Irokle::builder().build()?;let bob = Irokle::builder().build()?;let alice_topic = alice.create_topic::<ChatEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice_topic.publish(ChatEvent{author:"alice".into(),text:"hello".into(),})?;let bob_summary = bob.sync_summary(alice_topic.id())?;let data_for_bob = alice.plan_sync_data(bob.peer_id(),&bob_summary)?;let(bob_ack, _) = bob.receive_sync_data_from(alice.peer_id(), data_for_bob)?;
alice.apply_sync_ack(&bob_ack)?;let bob_topic = bob.open_topic::<ChatEvent>(alice_topic.id())?;
bob_topic.publish(ChatEvent{author:"bob".into(),text:"reply".into(),})?;for record in bob_topic.history(HistoryOrder::OldestFirst)? {println!("{}: {}", record.event.author, record.event.text);}Ok(())}

This example uses the transport-neutral sync API directly. Iroh examples can use sync_now(peer_id, topic_id) instead.

Topics And Membership

TopicConfig::initial_peers defines the initial signed member set. Topic::add_peer and Topic::remove_peer write membership control operations into the same DAG as application events.

When a node receives a topic for the first time, it can discover it through list_topics() and then open it with open_topic::<E>(topic_id) if its local peer is a current member. A node can reject membership with Irokle::reject_topic(topic_id) or Topic::leave(). Rejection is represented as a signed RemovePeer control operation, so other nodes can observe and sync the decision.

Bounded Replication

ReplicationPolicy::all() means all current topic members are eligible sync targets, but the selected set is capped by max_sync_peers.

use irokle::{ReplicationPolicy,TopicConfig};let config = TopicConfig{replication_policy:ReplicationPolicy::all().with_max_sync_peers(4),
..TopicConfig::default()};

Peer selection is deterministic and combines ring neighbors with hash-ranked fill peers. The goal is bounded epidemic propagation: each node syncs with only a small overlapping subset, and state reaches the rest of the topic through repeated sync rounds.

Iroh Sync

With the iroh feature, Irokle::builder().with_net(endpoint) configures the Irokle sync ALPN automatically. Normal use is NodeId-only:

use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};use tokio::time::{Duration, timeout};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]structMyEvent;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let alice_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;let bob_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;timeout(Duration::from_secs(10), alice_endpoint.online()).await?;timeout(Duration::from_secs(10), bob_endpoint.online()).await?;let alice = Irokle::builder().with_net(alice_endpoint).build()?;let bob = Irokle::builder().with_peer_whitelist([alice.peer_id()]).with_net(bob_endpoint).build()?;let topic = alice.create_topic::<MyEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice.sync_now(bob.peer_id(), topic.id()).await?;Ok(())}

By default, Iroh auto-accept only admits brand-new topics from peers in peer_whitelist. The whitelist starts as Some(empty), so add allowed peers with with_peer_whitelist, add_peer_to_whitelist, add_peers_to_whitelist, or set_peer_whitelist. Set the whitelist to None only when unknown-topic admission should be unrestricted. For production deployments, keep the Irokle sync ALPN dedicated to trusted peers and whitelist topic introducers explicitly.

sync_addr_now(endpoint_addr, topic_id) remains available for explicit one-off manual dialing in local/offline setups. The peer registry API was removed; when discovery is configured, peers are identified by PeerId/Iroh EndpointId.

Iroh runtime behavior is configurable when defaults are not appropriate for the deployment:

use irokle::net::IrohRuntimeConfig;use std::time::Duration;let runtime = IrohRuntimeConfig{connect_timeout:Duration::from_secs(10),sync_io_timeout:Duration::from_secs(10),resync_interval:Duration::from_secs(15),};let node = irokle::Irokle::builder().with_iroh_runtime_config(runtime).with_net(endpoint).build()?;

Use shutdown_iroh().await during orderly shutdown to close the endpoint and abort tracked background accept/resync tasks.

Sync Failures And Status

Iroh-backed builders default to WriteConcern::AsyncReplication unless with_write_concern or with_config sets a different policy. Iroh nodes start a periodic resync loop whenever networking is configured; without_auto_accept() disables inbound auto-accept but does not disable outbound resync. The loop retries outstanding sync obligations and also performs bounded anti-entropy sync with the topic's selected peers. Publish with WriteConcern::AsyncReplication creates obligations for the bounded replication target set and wakes the same sync machinery. If the wake cannot start because no Tokio runtime is active, the obligation remains visible and sync status records the failure.

Applications can inspect sync state:

let statuses = node.sync_status(topic_id)?;let counts = node.sync_state_counts(topic_id)?;

Each SyncPeerStatus includes state, pending_obligations, failed_attempts, successful_attempts, last_attempt_ms, last_success_ms, and last_error.

Disk Recovery

With fjall and iroh, durable recovery means reopening the same Fjall path and reusing the same Iroh SecretKey, because the Iroh key defines the node’s PeerId. Production applications should persist the Iroh secret in their normal secret-management system, restrict filesystem permissions for local key files, and back up the key with the Fjall database path.

See examples/iroh_fjall_recovery.rs for a complete example that creates a topic, closes the endpoint, reopens the database with the same key, lists recovered topics, and reads typed history.

Examples

  • examples/basic.rs: in-memory typed events plus transport-neutral sync planning.
  • examples/rdf.rs: observed-remove RDF projection implemented as application code on top of event history.
  • examples/iroh_chat.rs: NodeId-only Iroh chat sync using discovery.
  • examples/iroh_topic_intro.rs: introduces a peer to a topic, opens it on the receiver, then rejects membership.
  • examples/iroh_fjall_recovery.rs: reopens an Iroh/Fjall node from disk with the same Iroh secret key.

Run examples with features as needed:

cargo run --features iroh --example iroh_chat
cargo run --features iroh --example iroh_topic_intro
cargo run --features 'iroh fjall' --example iroh_fjall_recovery

About

Distributed Merkle DAGs for iroh topic sync

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - arunaengine/irokle: Distributed Merkle DAGs for iroh topic sync · GitHub
Skip to content

Irokle

Irokle is a signed Merkle-DAG operation log for invite-only topics. Application events and membership changes are stored as signed operations. The graph of operations can be used to derive current heads, a history of changes, summaries for syncing, and projections.

Features

  • Signed operations: every event or control change is signed by the peer that authored it.
  • Topic membership: topics are not public broadcast channels; typed access is gated by the current signed member set.
  • Deterministic sync: peers exchange summaries, missing operation closures, requests, and signed acknowledgements.
  • Bounded fanout: topic replication is capped by ReplicationPolicy::max_sync_peers so a node does not sync with every member by default.
  • Observability: sync status records expose pending obligations, failure counts, last errors, last success, and per-state counts.
  • Storage choices: MemoryStorage is available by default; FjallStorage is available behind the fjall feature.
  • Iroh integration: the iroh feature syncs over iroh::Endpoint using PeerId/NodeId dialing.

Minimal Example

use irokle::history::HistoryOrder;use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]#[irokle(type_id = "example.chat.message")]structChatEvent{author:String,text:String,}fnmain() -> irokle::Result<()>{let alice = Irokle::builder().build()?;let bob = Irokle::builder().build()?;let alice_topic = alice.create_topic::<ChatEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice_topic.publish(ChatEvent{author:"alice".into(),text:"hello".into(),})?;let bob_summary = bob.sync_summary(alice_topic.id())?;let data_for_bob = alice.plan_sync_data(bob.peer_id(),&bob_summary)?;let(bob_ack, _) = bob.receive_sync_data_from(alice.peer_id(), data_for_bob)?;
alice.apply_sync_ack(&bob_ack)?;let bob_topic = bob.open_topic::<ChatEvent>(alice_topic.id())?;
bob_topic.publish(ChatEvent{author:"bob".into(),text:"reply".into(),})?;for record in bob_topic.history(HistoryOrder::OldestFirst)? {println!("{}: {}", record.event.author, record.event.text);}Ok(())}

This example uses the transport-neutral sync API directly. Iroh examples can use sync_now(peer_id, topic_id) instead.

Topics And Membership

TopicConfig::initial_peers defines the initial signed member set. Topic::add_peer and Topic::remove_peer write membership control operations into the same DAG as application events.

When a node receives a topic for the first time, it can discover it through list_topics() and then open it with open_topic::<E>(topic_id) if its local peer is a current member. A node can reject membership with Irokle::reject_topic(topic_id) or Topic::leave(). Rejection is represented as a signed RemovePeer control operation, so other nodes can observe and sync the decision.

Bounded Replication

ReplicationPolicy::all() means all current topic members are eligible sync targets, but the selected set is capped by max_sync_peers.

use irokle::{ReplicationPolicy,TopicConfig};let config = TopicConfig{replication_policy:ReplicationPolicy::all().with_max_sync_peers(4),
..TopicConfig::default()};

Peer selection is deterministic and combines ring neighbors with hash-ranked fill peers. The goal is bounded epidemic propagation: each node syncs with only a small overlapping subset, and state reaches the rest of the topic through repeated sync rounds.

Iroh Sync

With the iroh feature, Irokle::builder().with_net(endpoint) configures the Irokle sync ALPN automatically. Normal use is NodeId-only:

use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};use tokio::time::{Duration, timeout};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]structMyEvent;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let alice_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;let bob_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;timeout(Duration::from_secs(10), alice_endpoint.online()).await?;timeout(Duration::from_secs(10), bob_endpoint.online()).await?;let alice = Irokle::builder().with_net(alice_endpoint).build()?;let bob = Irokle::builder().with_peer_whitelist([alice.peer_id()]).with_net(bob_endpoint).build()?;let topic = alice.create_topic::<MyEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice.sync_now(bob.peer_id(), topic.id()).await?;Ok(())}

By default, Iroh auto-accept only admits brand-new topics from peers in peer_whitelist. The whitelist starts as Some(empty), so add allowed peers with with_peer_whitelist, add_peer_to_whitelist, add_peers_to_whitelist, or set_peer_whitelist. Set the whitelist to None only when unknown-topic admission should be unrestricted. For production deployments, keep the Irokle sync ALPN dedicated to trusted peers and whitelist topic introducers explicitly.

sync_addr_now(endpoint_addr, topic_id) remains available for explicit one-off manual dialing in local/offline setups. The peer registry API was removed; when discovery is configured, peers are identified by PeerId/Iroh EndpointId.

Iroh runtime behavior is configurable when defaults are not appropriate for the deployment:

use irokle::net::IrohRuntimeConfig;use std::time::Duration;let runtime = IrohRuntimeConfig{connect_timeout:Duration::from_secs(10),sync_io_timeout:Duration::from_secs(10),resync_interval:Duration::from_secs(15),};let node = irokle::Irokle::builder().with_iroh_runtime_config(runtime).with_net(endpoint).build()?;

Use shutdown_iroh().await during orderly shutdown to close the endpoint and abort tracked background accept/resync tasks.

Sync Failures And Status

Iroh-backed builders default to WriteConcern::AsyncReplication unless with_write_concern or with_config sets a different policy. Iroh nodes start a periodic resync loop whenever networking is configured; without_auto_accept() disables inbound auto-accept but does not disable outbound resync. The loop retries outstanding sync obligations and also performs bounded anti-entropy sync with the topic's selected peers. Publish with WriteConcern::AsyncReplication creates obligations for the bounded replication target set and wakes the same sync machinery. If the wake cannot start because no Tokio runtime is active, the obligation remains visible and sync status records the failure.

Applications can inspect sync state:

let statuses = node.sync_status(topic_id)?;let counts = node.sync_state_counts(topic_id)?;

Each SyncPeerStatus includes state, pending_obligations, failed_attempts, successful_attempts, last_attempt_ms, last_success_ms, and last_error.

Disk Recovery

With fjall and iroh, durable recovery means reopening the same Fjall path and reusing the same Iroh SecretKey, because the Iroh key defines the node’s PeerId. Production applications should persist the Iroh secret in their normal secret-management system, restrict filesystem permissions for local key files, and back up the key with the Fjall database path.

See examples/iroh_fjall_recovery.rs for a complete example that creates a topic, closes the endpoint, reopens the database with the same key, lists recovered topics, and reads typed history.

Examples

  • examples/basic.rs: in-memory typed events plus transport-neutral sync planning.
  • examples/rdf.rs: observed-remove RDF projection implemented as application code on top of event history.
  • examples/iroh_chat.rs: NodeId-only Iroh chat sync using discovery.
  • examples/iroh_topic_intro.rs: introduces a peer to a topic, opens it on the receiver, then rejects membership.
  • examples/iroh_fjall_recovery.rs: reopens an Iroh/Fjall node from disk with the same Iroh secret key.

Run examples with features as needed:

cargo run --features iroh --example iroh_chat
cargo run --features iroh --example iroh_topic_intro
cargo run --features 'iroh fjall' --example iroh_fjall_recovery

About

Distributed Merkle DAGs for iroh topic sync

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - arunaengine/irokle: Distributed Merkle DAGs for iroh topic sync · GitHub
Skip to content

Irokle

Irokle is a signed Merkle-DAG operation log for invite-only topics. Application events and membership changes are stored as signed operations. The graph of operations can be used to derive current heads, a history of changes, summaries for syncing, and projections.

Features

  • Signed operations: every event or control change is signed by the peer that authored it.
  • Topic membership: topics are not public broadcast channels; typed access is gated by the current signed member set.
  • Deterministic sync: peers exchange summaries, missing operation closures, requests, and signed acknowledgements.
  • Bounded fanout: topic replication is capped by ReplicationPolicy::max_sync_peers so a node does not sync with every member by default.
  • Observability: sync status records expose pending obligations, failure counts, last errors, last success, and per-state counts.
  • Storage choices: MemoryStorage is available by default; FjallStorage is available behind the fjall feature.
  • Iroh integration: the iroh feature syncs over iroh::Endpoint using PeerId/NodeId dialing.

Minimal Example

use irokle::history::HistoryOrder;use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]#[irokle(type_id = "example.chat.message")]structChatEvent{author:String,text:String,}fnmain() -> irokle::Result<()>{let alice = Irokle::builder().build()?;let bob = Irokle::builder().build()?;let alice_topic = alice.create_topic::<ChatEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice_topic.publish(ChatEvent{author:"alice".into(),text:"hello".into(),})?;let bob_summary = bob.sync_summary(alice_topic.id())?;let data_for_bob = alice.plan_sync_data(bob.peer_id(),&bob_summary)?;let(bob_ack, _) = bob.receive_sync_data_from(alice.peer_id(), data_for_bob)?;
alice.apply_sync_ack(&bob_ack)?;let bob_topic = bob.open_topic::<ChatEvent>(alice_topic.id())?;
bob_topic.publish(ChatEvent{author:"bob".into(),text:"reply".into(),})?;for record in bob_topic.history(HistoryOrder::OldestFirst)? {println!("{}: {}", record.event.author, record.event.text);}Ok(())}

This example uses the transport-neutral sync API directly. Iroh examples can use sync_now(peer_id, topic_id) instead.

Topics And Membership

TopicConfig::initial_peers defines the initial signed member set. Topic::add_peer and Topic::remove_peer write membership control operations into the same DAG as application events.

When a node receives a topic for the first time, it can discover it through list_topics() and then open it with open_topic::<E>(topic_id) if its local peer is a current member. A node can reject membership with Irokle::reject_topic(topic_id) or Topic::leave(). Rejection is represented as a signed RemovePeer control operation, so other nodes can observe and sync the decision.

Bounded Replication

ReplicationPolicy::all() means all current topic members are eligible sync targets, but the selected set is capped by max_sync_peers.

use irokle::{ReplicationPolicy,TopicConfig};let config = TopicConfig{replication_policy:ReplicationPolicy::all().with_max_sync_peers(4),
..TopicConfig::default()};

Peer selection is deterministic and combines ring neighbors with hash-ranked fill peers. The goal is bounded epidemic propagation: each node syncs with only a small overlapping subset, and state reaches the rest of the topic through repeated sync rounds.

Iroh Sync

With the iroh feature, Irokle::builder().with_net(endpoint) configures the Irokle sync ALPN automatically. Normal use is NodeId-only:

use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};use tokio::time::{Duration, timeout};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]structMyEvent;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let alice_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;let bob_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;timeout(Duration::from_secs(10), alice_endpoint.online()).await?;timeout(Duration::from_secs(10), bob_endpoint.online()).await?;let alice = Irokle::builder().with_net(alice_endpoint).build()?;let bob = Irokle::builder().with_peer_whitelist([alice.peer_id()]).with_net(bob_endpoint).build()?;let topic = alice.create_topic::<MyEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice.sync_now(bob.peer_id(), topic.id()).await?;Ok(())}

By default, Iroh auto-accept only admits brand-new topics from peers in peer_whitelist. The whitelist starts as Some(empty), so add allowed peers with with_peer_whitelist, add_peer_to_whitelist, add_peers_to_whitelist, or set_peer_whitelist. Set the whitelist to None only when unknown-topic admission should be unrestricted. For production deployments, keep the Irokle sync ALPN dedicated to trusted peers and whitelist topic introducers explicitly.

sync_addr_now(endpoint_addr, topic_id) remains available for explicit one-off manual dialing in local/offline setups. The peer registry API was removed; when discovery is configured, peers are identified by PeerId/Iroh EndpointId.

Iroh runtime behavior is configurable when defaults are not appropriate for the deployment:

use irokle::net::IrohRuntimeConfig;use std::time::Duration;let runtime = IrohRuntimeConfig{connect_timeout:Duration::from_secs(10),sync_io_timeout:Duration::from_secs(10),resync_interval:Duration::from_secs(15),};let node = irokle::Irokle::builder().with_iroh_runtime_config(runtime).with_net(endpoint).build()?;

Use shutdown_iroh().await during orderly shutdown to close the endpoint and abort tracked background accept/resync tasks.

Sync Failures And Status

Iroh-backed builders default to WriteConcern::AsyncReplication unless with_write_concern or with_config sets a different policy. Iroh nodes start a periodic resync loop whenever networking is configured; without_auto_accept() disables inbound auto-accept but does not disable outbound resync. The loop retries outstanding sync obligations and also performs bounded anti-entropy sync with the topic's selected peers. Publish with WriteConcern::AsyncReplication creates obligations for the bounded replication target set and wakes the same sync machinery. If the wake cannot start because no Tokio runtime is active, the obligation remains visible and sync status records the failure.

Applications can inspect sync state:

let statuses = node.sync_status(topic_id)?;let counts = node.sync_state_counts(topic_id)?;

Each SyncPeerStatus includes state, pending_obligations, failed_attempts, successful_attempts, last_attempt_ms, last_success_ms, and last_error.

Disk Recovery

With fjall and iroh, durable recovery means reopening the same Fjall path and reusing the same Iroh SecretKey, because the Iroh key defines the node’s PeerId. Production applications should persist the Iroh secret in their normal secret-management system, restrict filesystem permissions for local key files, and back up the key with the Fjall database path.

See examples/iroh_fjall_recovery.rs for a complete example that creates a topic, closes the endpoint, reopens the database with the same key, lists recovered topics, and reads typed history.

Examples

  • examples/basic.rs: in-memory typed events plus transport-neutral sync planning.
  • examples/rdf.rs: observed-remove RDF projection implemented as application code on top of event history.
  • examples/iroh_chat.rs: NodeId-only Iroh chat sync using discovery.
  • examples/iroh_topic_intro.rs: introduces a peer to a topic, opens it on the receiver, then rejects membership.
  • examples/iroh_fjall_recovery.rs: reopens an Iroh/Fjall node from disk with the same Iroh secret key.

Run examples with features as needed:

cargo run --features iroh --example iroh_chat
cargo run --features iroh --example iroh_topic_intro
cargo run --features 'iroh fjall' --example iroh_fjall_recovery

About

Distributed Merkle DAGs for iroh topic sync

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - arunaengine/irokle: Distributed Merkle DAGs for iroh topic sync · GitHub
Skip to content

Irokle

Irokle is a signed Merkle-DAG operation log for invite-only topics. Application events and membership changes are stored as signed operations. The graph of operations can be used to derive current heads, a history of changes, summaries for syncing, and projections.

Features

  • Signed operations: every event or control change is signed by the peer that authored it.
  • Topic membership: topics are not public broadcast channels; typed access is gated by the current signed member set.
  • Deterministic sync: peers exchange summaries, missing operation closures, requests, and signed acknowledgements.
  • Bounded fanout: topic replication is capped by ReplicationPolicy::max_sync_peers so a node does not sync with every member by default.
  • Observability: sync status records expose pending obligations, failure counts, last errors, last success, and per-state counts.
  • Storage choices: MemoryStorage is available by default; FjallStorage is available behind the fjall feature.
  • Iroh integration: the iroh feature syncs over iroh::Endpoint using PeerId/NodeId dialing.

Minimal Example

use irokle::history::HistoryOrder;use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]#[irokle(type_id = "example.chat.message")]structChatEvent{author:String,text:String,}fnmain() -> irokle::Result<()>{let alice = Irokle::builder().build()?;let bob = Irokle::builder().build()?;let alice_topic = alice.create_topic::<ChatEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice_topic.publish(ChatEvent{author:"alice".into(),text:"hello".into(),})?;let bob_summary = bob.sync_summary(alice_topic.id())?;let data_for_bob = alice.plan_sync_data(bob.peer_id(),&bob_summary)?;let(bob_ack, _) = bob.receive_sync_data_from(alice.peer_id(), data_for_bob)?;
alice.apply_sync_ack(&bob_ack)?;let bob_topic = bob.open_topic::<ChatEvent>(alice_topic.id())?;
bob_topic.publish(ChatEvent{author:"bob".into(),text:"reply".into(),})?;for record in bob_topic.history(HistoryOrder::OldestFirst)? {println!("{}: {}", record.event.author, record.event.text);}Ok(())}

This example uses the transport-neutral sync API directly. Iroh examples can use sync_now(peer_id, topic_id) instead.

Topics And Membership

TopicConfig::initial_peers defines the initial signed member set. Topic::add_peer and Topic::remove_peer write membership control operations into the same DAG as application events.

When a node receives a topic for the first time, it can discover it through list_topics() and then open it with open_topic::<E>(topic_id) if its local peer is a current member. A node can reject membership with Irokle::reject_topic(topic_id) or Topic::leave(). Rejection is represented as a signed RemovePeer control operation, so other nodes can observe and sync the decision.

Bounded Replication

ReplicationPolicy::all() means all current topic members are eligible sync targets, but the selected set is capped by max_sync_peers.

use irokle::{ReplicationPolicy,TopicConfig};let config = TopicConfig{replication_policy:ReplicationPolicy::all().with_max_sync_peers(4),
..TopicConfig::default()};

Peer selection is deterministic and combines ring neighbors with hash-ranked fill peers. The goal is bounded epidemic propagation: each node syncs with only a small overlapping subset, and state reaches the rest of the topic through repeated sync rounds.

Iroh Sync

With the iroh feature, Irokle::builder().with_net(endpoint) configures the Irokle sync ALPN automatically. Normal use is NodeId-only:

use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};use tokio::time::{Duration, timeout};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]structMyEvent;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let alice_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;let bob_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;timeout(Duration::from_secs(10), alice_endpoint.online()).await?;timeout(Duration::from_secs(10), bob_endpoint.online()).await?;let alice = Irokle::builder().with_net(alice_endpoint).build()?;let bob = Irokle::builder().with_peer_whitelist([alice.peer_id()]).with_net(bob_endpoint).build()?;let topic = alice.create_topic::<MyEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice.sync_now(bob.peer_id(), topic.id()).await?;Ok(())}

By default, Iroh auto-accept only admits brand-new topics from peers in peer_whitelist. The whitelist starts as Some(empty), so add allowed peers with with_peer_whitelist, add_peer_to_whitelist, add_peers_to_whitelist, or set_peer_whitelist. Set the whitelist to None only when unknown-topic admission should be unrestricted. For production deployments, keep the Irokle sync ALPN dedicated to trusted peers and whitelist topic introducers explicitly.

sync_addr_now(endpoint_addr, topic_id) remains available for explicit one-off manual dialing in local/offline setups. The peer registry API was removed; when discovery is configured, peers are identified by PeerId/Iroh EndpointId.

Iroh runtime behavior is configurable when defaults are not appropriate for the deployment:

use irokle::net::IrohRuntimeConfig;use std::time::Duration;let runtime = IrohRuntimeConfig{connect_timeout:Duration::from_secs(10),sync_io_timeout:Duration::from_secs(10),resync_interval:Duration::from_secs(15),};let node = irokle::Irokle::builder().with_iroh_runtime_config(runtime).with_net(endpoint).build()?;

Use shutdown_iroh().await during orderly shutdown to close the endpoint and abort tracked background accept/resync tasks.

Sync Failures And Status

Iroh-backed builders default to WriteConcern::AsyncReplication unless with_write_concern or with_config sets a different policy. Iroh nodes start a periodic resync loop whenever networking is configured; without_auto_accept() disables inbound auto-accept but does not disable outbound resync. The loop retries outstanding sync obligations and also performs bounded anti-entropy sync with the topic's selected peers. Publish with WriteConcern::AsyncReplication creates obligations for the bounded replication target set and wakes the same sync machinery. If the wake cannot start because no Tokio runtime is active, the obligation remains visible and sync status records the failure.

Applications can inspect sync state:

let statuses = node.sync_status(topic_id)?;let counts = node.sync_state_counts(topic_id)?;

Each SyncPeerStatus includes state, pending_obligations, failed_attempts, successful_attempts, last_attempt_ms, last_success_ms, and last_error.

Disk Recovery

With fjall and iroh, durable recovery means reopening the same Fjall path and reusing the same Iroh SecretKey, because the Iroh key defines the node’s PeerId. Production applications should persist the Iroh secret in their normal secret-management system, restrict filesystem permissions for local key files, and back up the key with the Fjall database path.

See examples/iroh_fjall_recovery.rs for a complete example that creates a topic, closes the endpoint, reopens the database with the same key, lists recovered topics, and reads typed history.

Examples

  • examples/basic.rs: in-memory typed events plus transport-neutral sync planning.
  • examples/rdf.rs: observed-remove RDF projection implemented as application code on top of event history.
  • examples/iroh_chat.rs: NodeId-only Iroh chat sync using discovery.
  • examples/iroh_topic_intro.rs: introduces a peer to a topic, opens it on the receiver, then rejects membership.
  • examples/iroh_fjall_recovery.rs: reopens an Iroh/Fjall node from disk with the same Iroh secret key.

Run examples with features as needed:

cargo run --features iroh --example iroh_chat
cargo run --features iroh --example iroh_topic_intro
cargo run --features 'iroh fjall' --example iroh_fjall_recovery

About

Distributed Merkle DAGs for iroh topic sync

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - arunaengine/irokle: Distributed Merkle DAGs for iroh topic sync · GitHub
Skip to content

Irokle

Irokle is a signed Merkle-DAG operation log for invite-only topics. Application events and membership changes are stored as signed operations. The graph of operations can be used to derive current heads, a history of changes, summaries for syncing, and projections.

Features

  • Signed operations: every event or control change is signed by the peer that authored it.
  • Topic membership: topics are not public broadcast channels; typed access is gated by the current signed member set.
  • Deterministic sync: peers exchange summaries, missing operation closures, requests, and signed acknowledgements.
  • Bounded fanout: topic replication is capped by ReplicationPolicy::max_sync_peers so a node does not sync with every member by default.
  • Observability: sync status records expose pending obligations, failure counts, last errors, last success, and per-state counts.
  • Storage choices: MemoryStorage is available by default; FjallStorage is available behind the fjall feature.
  • Iroh integration: the iroh feature syncs over iroh::Endpoint using PeerId/NodeId dialing.

Minimal Example

use irokle::history::HistoryOrder;use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]#[irokle(type_id = "example.chat.message")]structChatEvent{author:String,text:String,}fnmain() -> irokle::Result<()>{let alice = Irokle::builder().build()?;let bob = Irokle::builder().build()?;let alice_topic = alice.create_topic::<ChatEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice_topic.publish(ChatEvent{author:"alice".into(),text:"hello".into(),})?;let bob_summary = bob.sync_summary(alice_topic.id())?;let data_for_bob = alice.plan_sync_data(bob.peer_id(),&bob_summary)?;let(bob_ack, _) = bob.receive_sync_data_from(alice.peer_id(), data_for_bob)?;
alice.apply_sync_ack(&bob_ack)?;let bob_topic = bob.open_topic::<ChatEvent>(alice_topic.id())?;
bob_topic.publish(ChatEvent{author:"bob".into(),text:"reply".into(),})?;for record in bob_topic.history(HistoryOrder::OldestFirst)? {println!("{}: {}", record.event.author, record.event.text);}Ok(())}

This example uses the transport-neutral sync API directly. Iroh examples can use sync_now(peer_id, topic_id) instead.

Topics And Membership

TopicConfig::initial_peers defines the initial signed member set. Topic::add_peer and Topic::remove_peer write membership control operations into the same DAG as application events.

When a node receives a topic for the first time, it can discover it through list_topics() and then open it with open_topic::<E>(topic_id) if its local peer is a current member. A node can reject membership with Irokle::reject_topic(topic_id) or Topic::leave(). Rejection is represented as a signed RemovePeer control operation, so other nodes can observe and sync the decision.

Bounded Replication

ReplicationPolicy::all() means all current topic members are eligible sync targets, but the selected set is capped by max_sync_peers.

use irokle::{ReplicationPolicy,TopicConfig};let config = TopicConfig{replication_policy:ReplicationPolicy::all().with_max_sync_peers(4),
..TopicConfig::default()};

Peer selection is deterministic and combines ring neighbors with hash-ranked fill peers. The goal is bounded epidemic propagation: each node syncs with only a small overlapping subset, and state reaches the rest of the topic through repeated sync rounds.

Iroh Sync

With the iroh feature, Irokle::builder().with_net(endpoint) configures the Irokle sync ALPN automatically. Normal use is NodeId-only:

use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};use tokio::time::{Duration, timeout};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]structMyEvent;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let alice_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;let bob_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;timeout(Duration::from_secs(10), alice_endpoint.online()).await?;timeout(Duration::from_secs(10), bob_endpoint.online()).await?;let alice = Irokle::builder().with_net(alice_endpoint).build()?;let bob = Irokle::builder().with_peer_whitelist([alice.peer_id()]).with_net(bob_endpoint).build()?;let topic = alice.create_topic::<MyEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice.sync_now(bob.peer_id(), topic.id()).await?;Ok(())}

By default, Iroh auto-accept only admits brand-new topics from peers in peer_whitelist. The whitelist starts as Some(empty), so add allowed peers with with_peer_whitelist, add_peer_to_whitelist, add_peers_to_whitelist, or set_peer_whitelist. Set the whitelist to None only when unknown-topic admission should be unrestricted. For production deployments, keep the Irokle sync ALPN dedicated to trusted peers and whitelist topic introducers explicitly.

sync_addr_now(endpoint_addr, topic_id) remains available for explicit one-off manual dialing in local/offline setups. The peer registry API was removed; when discovery is configured, peers are identified by PeerId/Iroh EndpointId.

Iroh runtime behavior is configurable when defaults are not appropriate for the deployment:

use irokle::net::IrohRuntimeConfig;use std::time::Duration;let runtime = IrohRuntimeConfig{connect_timeout:Duration::from_secs(10),sync_io_timeout:Duration::from_secs(10),resync_interval:Duration::from_secs(15),};let node = irokle::Irokle::builder().with_iroh_runtime_config(runtime).with_net(endpoint).build()?;

Use shutdown_iroh().await during orderly shutdown to close the endpoint and abort tracked background accept/resync tasks.

Sync Failures And Status

Iroh-backed builders default to WriteConcern::AsyncReplication unless with_write_concern or with_config sets a different policy. Iroh nodes start a periodic resync loop whenever networking is configured; without_auto_accept() disables inbound auto-accept but does not disable outbound resync. The loop retries outstanding sync obligations and also performs bounded anti-entropy sync with the topic's selected peers. Publish with WriteConcern::AsyncReplication creates obligations for the bounded replication target set and wakes the same sync machinery. If the wake cannot start because no Tokio runtime is active, the obligation remains visible and sync status records the failure.

Applications can inspect sync state:

let statuses = node.sync_status(topic_id)?;let counts = node.sync_state_counts(topic_id)?;

Each SyncPeerStatus includes state, pending_obligations, failed_attempts, successful_attempts, last_attempt_ms, last_success_ms, and last_error.

Disk Recovery

With fjall and iroh, durable recovery means reopening the same Fjall path and reusing the same Iroh SecretKey, because the Iroh key defines the node’s PeerId. Production applications should persist the Iroh secret in their normal secret-management system, restrict filesystem permissions for local key files, and back up the key with the Fjall database path.

See examples/iroh_fjall_recovery.rs for a complete example that creates a topic, closes the endpoint, reopens the database with the same key, lists recovered topics, and reads typed history.

Examples

  • examples/basic.rs: in-memory typed events plus transport-neutral sync planning.
  • examples/rdf.rs: observed-remove RDF projection implemented as application code on top of event history.
  • examples/iroh_chat.rs: NodeId-only Iroh chat sync using discovery.
  • examples/iroh_topic_intro.rs: introduces a peer to a topic, opens it on the receiver, then rejects membership.
  • examples/iroh_fjall_recovery.rs: reopens an Iroh/Fjall node from disk with the same Iroh secret key.

Run examples with features as needed:

cargo run --features iroh --example iroh_chat
cargo run --features iroh --example iroh_topic_intro
cargo run --features 'iroh fjall' --example iroh_fjall_recovery

About

Distributed Merkle DAGs for iroh topic sync

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - arunaengine/irokle: Distributed Merkle DAGs for iroh topic sync · GitHub
Skip to content

Irokle

Irokle is a signed Merkle-DAG operation log for invite-only topics. Application events and membership changes are stored as signed operations. The graph of operations can be used to derive current heads, a history of changes, summaries for syncing, and projections.

Features

  • Signed operations: every event or control change is signed by the peer that authored it.
  • Topic membership: topics are not public broadcast channels; typed access is gated by the current signed member set.
  • Deterministic sync: peers exchange summaries, missing operation closures, requests, and signed acknowledgements.
  • Bounded fanout: topic replication is capped by ReplicationPolicy::max_sync_peers so a node does not sync with every member by default.
  • Observability: sync status records expose pending obligations, failure counts, last errors, last success, and per-state counts.
  • Storage choices: MemoryStorage is available by default; FjallStorage is available behind the fjall feature.
  • Iroh integration: the iroh feature syncs over iroh::Endpoint using PeerId/NodeId dialing.

Minimal Example

use irokle::history::HistoryOrder;use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]#[irokle(type_id = "example.chat.message")]structChatEvent{author:String,text:String,}fnmain() -> irokle::Result<()>{let alice = Irokle::builder().build()?;let bob = Irokle::builder().build()?;let alice_topic = alice.create_topic::<ChatEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice_topic.publish(ChatEvent{author:"alice".into(),text:"hello".into(),})?;let bob_summary = bob.sync_summary(alice_topic.id())?;let data_for_bob = alice.plan_sync_data(bob.peer_id(),&bob_summary)?;let(bob_ack, _) = bob.receive_sync_data_from(alice.peer_id(), data_for_bob)?;
alice.apply_sync_ack(&bob_ack)?;let bob_topic = bob.open_topic::<ChatEvent>(alice_topic.id())?;
bob_topic.publish(ChatEvent{author:"bob".into(),text:"reply".into(),})?;for record in bob_topic.history(HistoryOrder::OldestFirst)? {println!("{}: {}", record.event.author, record.event.text);}Ok(())}

This example uses the transport-neutral sync API directly. Iroh examples can use sync_now(peer_id, topic_id) instead.

Topics And Membership

TopicConfig::initial_peers defines the initial signed member set. Topic::add_peer and Topic::remove_peer write membership control operations into the same DAG as application events.

When a node receives a topic for the first time, it can discover it through list_topics() and then open it with open_topic::<E>(topic_id) if its local peer is a current member. A node can reject membership with Irokle::reject_topic(topic_id) or Topic::leave(). Rejection is represented as a signed RemovePeer control operation, so other nodes can observe and sync the decision.

Bounded Replication

ReplicationPolicy::all() means all current topic members are eligible sync targets, but the selected set is capped by max_sync_peers.

use irokle::{ReplicationPolicy,TopicConfig};let config = TopicConfig{replication_policy:ReplicationPolicy::all().with_max_sync_peers(4),
..TopicConfig::default()};

Peer selection is deterministic and combines ring neighbors with hash-ranked fill peers. The goal is bounded epidemic propagation: each node syncs with only a small overlapping subset, and state reaches the rest of the topic through repeated sync rounds.

Iroh Sync

With the iroh feature, Irokle::builder().with_net(endpoint) configures the Irokle sync ALPN automatically. Normal use is NodeId-only:

use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};use tokio::time::{Duration, timeout};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]structMyEvent;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let alice_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;let bob_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;timeout(Duration::from_secs(10), alice_endpoint.online()).await?;timeout(Duration::from_secs(10), bob_endpoint.online()).await?;let alice = Irokle::builder().with_net(alice_endpoint).build()?;let bob = Irokle::builder().with_peer_whitelist([alice.peer_id()]).with_net(bob_endpoint).build()?;let topic = alice.create_topic::<MyEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice.sync_now(bob.peer_id(), topic.id()).await?;Ok(())}

By default, Iroh auto-accept only admits brand-new topics from peers in peer_whitelist. The whitelist starts as Some(empty), so add allowed peers with with_peer_whitelist, add_peer_to_whitelist, add_peers_to_whitelist, or set_peer_whitelist. Set the whitelist to None only when unknown-topic admission should be unrestricted. For production deployments, keep the Irokle sync ALPN dedicated to trusted peers and whitelist topic introducers explicitly.

sync_addr_now(endpoint_addr, topic_id) remains available for explicit one-off manual dialing in local/offline setups. The peer registry API was removed; when discovery is configured, peers are identified by PeerId/Iroh EndpointId.

Iroh runtime behavior is configurable when defaults are not appropriate for the deployment:

use irokle::net::IrohRuntimeConfig;use std::time::Duration;let runtime = IrohRuntimeConfig{connect_timeout:Duration::from_secs(10),sync_io_timeout:Duration::from_secs(10),resync_interval:Duration::from_secs(15),};let node = irokle::Irokle::builder().with_iroh_runtime_config(runtime).with_net(endpoint).build()?;

Use shutdown_iroh().await during orderly shutdown to close the endpoint and abort tracked background accept/resync tasks.

Sync Failures And Status

Iroh-backed builders default to WriteConcern::AsyncReplication unless with_write_concern or with_config sets a different policy. Iroh nodes start a periodic resync loop whenever networking is configured; without_auto_accept() disables inbound auto-accept but does not disable outbound resync. The loop retries outstanding sync obligations and also performs bounded anti-entropy sync with the topic's selected peers. Publish with WriteConcern::AsyncReplication creates obligations for the bounded replication target set and wakes the same sync machinery. If the wake cannot start because no Tokio runtime is active, the obligation remains visible and sync status records the failure.

Applications can inspect sync state:

let statuses = node.sync_status(topic_id)?;let counts = node.sync_state_counts(topic_id)?;

Each SyncPeerStatus includes state, pending_obligations, failed_attempts, successful_attempts, last_attempt_ms, last_success_ms, and last_error.

Disk Recovery

With fjall and iroh, durable recovery means reopening the same Fjall path and reusing the same Iroh SecretKey, because the Iroh key defines the node’s PeerId. Production applications should persist the Iroh secret in their normal secret-management system, restrict filesystem permissions for local key files, and back up the key with the Fjall database path.

See examples/iroh_fjall_recovery.rs for a complete example that creates a topic, closes the endpoint, reopens the database with the same key, lists recovered topics, and reads typed history.

Examples

  • examples/basic.rs: in-memory typed events plus transport-neutral sync planning.
  • examples/rdf.rs: observed-remove RDF projection implemented as application code on top of event history.
  • examples/iroh_chat.rs: NodeId-only Iroh chat sync using discovery.
  • examples/iroh_topic_intro.rs: introduces a peer to a topic, opens it on the receiver, then rejects membership.
  • examples/iroh_fjall_recovery.rs: reopens an Iroh/Fjall node from disk with the same Iroh secret key.

Run examples with features as needed:

cargo run --features iroh --example iroh_chat
cargo run --features iroh --example iroh_topic_intro
cargo run --features 'iroh fjall' --example iroh_fjall_recovery

About

Distributed Merkle DAGs for iroh topic sync

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - arunaengine/irokle: Distributed Merkle DAGs for iroh topic sync · GitHub
Skip to content

Irokle

Irokle is a signed Merkle-DAG operation log for invite-only topics. Application events and membership changes are stored as signed operations. The graph of operations can be used to derive current heads, a history of changes, summaries for syncing, and projections.

Features

  • Signed operations: every event or control change is signed by the peer that authored it.
  • Topic membership: topics are not public broadcast channels; typed access is gated by the current signed member set.
  • Deterministic sync: peers exchange summaries, missing operation closures, requests, and signed acknowledgements.
  • Bounded fanout: topic replication is capped by ReplicationPolicy::max_sync_peers so a node does not sync with every member by default.
  • Observability: sync status records expose pending obligations, failure counts, last errors, last success, and per-state counts.
  • Storage choices: MemoryStorage is available by default; FjallStorage is available behind the fjall feature.
  • Iroh integration: the iroh feature syncs over iroh::Endpoint using PeerId/NodeId dialing.

Minimal Example

use irokle::history::HistoryOrder;use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]#[irokle(type_id = "example.chat.message")]structChatEvent{author:String,text:String,}fnmain() -> irokle::Result<()>{let alice = Irokle::builder().build()?;let bob = Irokle::builder().build()?;let alice_topic = alice.create_topic::<ChatEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice_topic.publish(ChatEvent{author:"alice".into(),text:"hello".into(),})?;let bob_summary = bob.sync_summary(alice_topic.id())?;let data_for_bob = alice.plan_sync_data(bob.peer_id(),&bob_summary)?;let(bob_ack, _) = bob.receive_sync_data_from(alice.peer_id(), data_for_bob)?;
alice.apply_sync_ack(&bob_ack)?;let bob_topic = bob.open_topic::<ChatEvent>(alice_topic.id())?;
bob_topic.publish(ChatEvent{author:"bob".into(),text:"reply".into(),})?;for record in bob_topic.history(HistoryOrder::OldestFirst)? {println!("{}: {}", record.event.author, record.event.text);}Ok(())}

This example uses the transport-neutral sync API directly. Iroh examples can use sync_now(peer_id, topic_id) instead.

Topics And Membership

TopicConfig::initial_peers defines the initial signed member set. Topic::add_peer and Topic::remove_peer write membership control operations into the same DAG as application events.

When a node receives a topic for the first time, it can discover it through list_topics() and then open it with open_topic::<E>(topic_id) if its local peer is a current member. A node can reject membership with Irokle::reject_topic(topic_id) or Topic::leave(). Rejection is represented as a signed RemovePeer control operation, so other nodes can observe and sync the decision.

Bounded Replication

ReplicationPolicy::all() means all current topic members are eligible sync targets, but the selected set is capped by max_sync_peers.

use irokle::{ReplicationPolicy,TopicConfig};let config = TopicConfig{replication_policy:ReplicationPolicy::all().with_max_sync_peers(4),
..TopicConfig::default()};

Peer selection is deterministic and combines ring neighbors with hash-ranked fill peers. The goal is bounded epidemic propagation: each node syncs with only a small overlapping subset, and state reaches the rest of the topic through repeated sync rounds.

Iroh Sync

With the iroh feature, Irokle::builder().with_net(endpoint) configures the Irokle sync ALPN automatically. Normal use is NodeId-only:

use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};use tokio::time::{Duration, timeout};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]structMyEvent;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let alice_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;let bob_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;timeout(Duration::from_secs(10), alice_endpoint.online()).await?;timeout(Duration::from_secs(10), bob_endpoint.online()).await?;let alice = Irokle::builder().with_net(alice_endpoint).build()?;let bob = Irokle::builder().with_peer_whitelist([alice.peer_id()]).with_net(bob_endpoint).build()?;let topic = alice.create_topic::<MyEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice.sync_now(bob.peer_id(), topic.id()).await?;Ok(())}

By default, Iroh auto-accept only admits brand-new topics from peers in peer_whitelist. The whitelist starts as Some(empty), so add allowed peers with with_peer_whitelist, add_peer_to_whitelist, add_peers_to_whitelist, or set_peer_whitelist. Set the whitelist to None only when unknown-topic admission should be unrestricted. For production deployments, keep the Irokle sync ALPN dedicated to trusted peers and whitelist topic introducers explicitly.

sync_addr_now(endpoint_addr, topic_id) remains available for explicit one-off manual dialing in local/offline setups. The peer registry API was removed; when discovery is configured, peers are identified by PeerId/Iroh EndpointId.

Iroh runtime behavior is configurable when defaults are not appropriate for the deployment:

use irokle::net::IrohRuntimeConfig;use std::time::Duration;let runtime = IrohRuntimeConfig{connect_timeout:Duration::from_secs(10),sync_io_timeout:Duration::from_secs(10),resync_interval:Duration::from_secs(15),};let node = irokle::Irokle::builder().with_iroh_runtime_config(runtime).with_net(endpoint).build()?;

Use shutdown_iroh().await during orderly shutdown to close the endpoint and abort tracked background accept/resync tasks.

Sync Failures And Status

Iroh-backed builders default to WriteConcern::AsyncReplication unless with_write_concern or with_config sets a different policy. Iroh nodes start a periodic resync loop whenever networking is configured; without_auto_accept() disables inbound auto-accept but does not disable outbound resync. The loop retries outstanding sync obligations and also performs bounded anti-entropy sync with the topic's selected peers. Publish with WriteConcern::AsyncReplication creates obligations for the bounded replication target set and wakes the same sync machinery. If the wake cannot start because no Tokio runtime is active, the obligation remains visible and sync status records the failure.

Applications can inspect sync state:

let statuses = node.sync_status(topic_id)?;let counts = node.sync_state_counts(topic_id)?;

Each SyncPeerStatus includes state, pending_obligations, failed_attempts, successful_attempts, last_attempt_ms, last_success_ms, and last_error.

Disk Recovery

With fjall and iroh, durable recovery means reopening the same Fjall path and reusing the same Iroh SecretKey, because the Iroh key defines the node’s PeerId. Production applications should persist the Iroh secret in their normal secret-management system, restrict filesystem permissions for local key files, and back up the key with the Fjall database path.

See examples/iroh_fjall_recovery.rs for a complete example that creates a topic, closes the endpoint, reopens the database with the same key, lists recovered topics, and reads typed history.

Examples

  • examples/basic.rs: in-memory typed events plus transport-neutral sync planning.
  • examples/rdf.rs: observed-remove RDF projection implemented as application code on top of event history.
  • examples/iroh_chat.rs: NodeId-only Iroh chat sync using discovery.
  • examples/iroh_topic_intro.rs: introduces a peer to a topic, opens it on the receiver, then rejects membership.
  • examples/iroh_fjall_recovery.rs: reopens an Iroh/Fjall node from disk with the same Iroh secret key.

Run examples with features as needed:

cargo run --features iroh --example iroh_chat
cargo run --features iroh --example iroh_topic_intro
cargo run --features 'iroh fjall' --example iroh_fjall_recovery

About

Distributed Merkle DAGs for iroh topic sync

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - arunaengine/irokle: Distributed Merkle DAGs for iroh topic sync · GitHub
Skip to content

Irokle

Irokle is a signed Merkle-DAG operation log for invite-only topics. Application events and membership changes are stored as signed operations. The graph of operations can be used to derive current heads, a history of changes, summaries for syncing, and projections.

Features

  • Signed operations: every event or control change is signed by the peer that authored it.
  • Topic membership: topics are not public broadcast channels; typed access is gated by the current signed member set.
  • Deterministic sync: peers exchange summaries, missing operation closures, requests, and signed acknowledgements.
  • Bounded fanout: topic replication is capped by ReplicationPolicy::max_sync_peers so a node does not sync with every member by default.
  • Observability: sync status records expose pending obligations, failure counts, last errors, last success, and per-state counts.
  • Storage choices: MemoryStorage is available by default; FjallStorage is available behind the fjall feature.
  • Iroh integration: the iroh feature syncs over iroh::Endpoint using PeerId/NodeId dialing.

Minimal Example

use irokle::history::HistoryOrder;use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]#[irokle(type_id = "example.chat.message")]structChatEvent{author:String,text:String,}fnmain() -> irokle::Result<()>{let alice = Irokle::builder().build()?;let bob = Irokle::builder().build()?;let alice_topic = alice.create_topic::<ChatEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice_topic.publish(ChatEvent{author:"alice".into(),text:"hello".into(),})?;let bob_summary = bob.sync_summary(alice_topic.id())?;let data_for_bob = alice.plan_sync_data(bob.peer_id(),&bob_summary)?;let(bob_ack, _) = bob.receive_sync_data_from(alice.peer_id(), data_for_bob)?;
alice.apply_sync_ack(&bob_ack)?;let bob_topic = bob.open_topic::<ChatEvent>(alice_topic.id())?;
bob_topic.publish(ChatEvent{author:"bob".into(),text:"reply".into(),})?;for record in bob_topic.history(HistoryOrder::OldestFirst)? {println!("{}: {}", record.event.author, record.event.text);}Ok(())}

This example uses the transport-neutral sync API directly. Iroh examples can use sync_now(peer_id, topic_id) instead.

Topics And Membership

TopicConfig::initial_peers defines the initial signed member set. Topic::add_peer and Topic::remove_peer write membership control operations into the same DAG as application events.

When a node receives a topic for the first time, it can discover it through list_topics() and then open it with open_topic::<E>(topic_id) if its local peer is a current member. A node can reject membership with Irokle::reject_topic(topic_id) or Topic::leave(). Rejection is represented as a signed RemovePeer control operation, so other nodes can observe and sync the decision.

Bounded Replication

ReplicationPolicy::all() means all current topic members are eligible sync targets, but the selected set is capped by max_sync_peers.

use irokle::{ReplicationPolicy,TopicConfig};let config = TopicConfig{replication_policy:ReplicationPolicy::all().with_max_sync_peers(4),
..TopicConfig::default()};

Peer selection is deterministic and combines ring neighbors with hash-ranked fill peers. The goal is bounded epidemic propagation: each node syncs with only a small overlapping subset, and state reaches the rest of the topic through repeated sync rounds.

Iroh Sync

With the iroh feature, Irokle::builder().with_net(endpoint) configures the Irokle sync ALPN automatically. Normal use is NodeId-only:

use irokle::{Irokle,TopicConfig};use serde::{Deserialize,Serialize};use tokio::time::{Duration, timeout};#[derive(Clone,Debug, irokle::Event,Deserialize,Serialize)]structMyEvent;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let alice_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;let bob_endpoint = iroh::Endpoint::builder(iroh::endpoint::presets::N0).bind().await?;timeout(Duration::from_secs(10), alice_endpoint.online()).await?;timeout(Duration::from_secs(10), bob_endpoint.online()).await?;let alice = Irokle::builder().with_net(alice_endpoint).build()?;let bob = Irokle::builder().with_peer_whitelist([alice.peer_id()]).with_net(bob_endpoint).build()?;let topic = alice.create_topic::<MyEvent>(TopicConfig{initial_peers:[bob.peer_id()].into(),
..TopicConfig::default()})?;
alice.sync_now(bob.peer_id(), topic.id()).await?;Ok(())}

By default, Iroh auto-accept only admits brand-new topics from peers in peer_whitelist. The whitelist starts as Some(empty), so add allowed peers with with_peer_whitelist, add_peer_to_whitelist, add_peers_to_whitelist, or set_peer_whitelist. Set the whitelist to None only when unknown-topic admission should be unrestricted. For production deployments, keep the Irokle sync ALPN dedicated to trusted peers and whitelist topic introducers explicitly.

sync_addr_now(endpoint_addr, topic_id) remains available for explicit one-off manual dialing in local/offline setups. The peer registry API was removed; when discovery is configured, peers are identified by PeerId/Iroh EndpointId.

Iroh runtime behavior is configurable when defaults are not appropriate for the deployment:

use irokle::net::IrohRuntimeConfig;use std::time::Duration;let runtime = IrohRuntimeConfig{connect_timeout:Duration::from_secs(10),sync_io_timeout:Duration::from_secs(10),resync_interval:Duration::from_secs(15),};let node = irokle::Irokle::builder().with_iroh_runtime_config(runtime).with_net(endpoint).build()?;

Use shutdown_iroh().await during orderly shutdown to close the endpoint and abort tracked background accept/resync tasks.

Sync Failures And Status

Iroh-backed builders default to WriteConcern::AsyncReplication unless with_write_concern or with_config sets a different policy. Iroh nodes start a periodic resync loop whenever networking is configured; without_auto_accept() disables inbound auto-accept but does not disable outbound resync. The loop retries outstanding sync obligations and also performs bounded anti-entropy sync with the topic's selected peers. Publish with WriteConcern::AsyncReplication creates obligations for the bounded replication target set and wakes the same sync machinery. If the wake cannot start because no Tokio runtime is active, the obligation remains visible and sync status records the failure.

Applications can inspect sync state:

let statuses = node.sync_status(topic_id)?;let counts = node.sync_state_counts(topic_id)?;

Each SyncPeerStatus includes state, pending_obligations, failed_attempts, successful_attempts, last_attempt_ms, last_success_ms, and last_error.

Disk Recovery

With fjall and iroh, durable recovery means reopening the same Fjall path and reusing the same Iroh SecretKey, because the Iroh key defines the node’s PeerId. Production applications should persist the Iroh secret in their normal secret-management system, restrict filesystem permissions for local key files, and back up the key with the Fjall database path.

See examples/iroh_fjall_recovery.rs for a complete example that creates a topic, closes the endpoint, reopens the database with the same key, lists recovered topics, and reads typed history.

Examples

  • examples/basic.rs: in-memory typed events plus transport-neutral sync planning.
  • examples/rdf.rs: observed-remove RDF projection implemented as application code on top of event history.
  • examples/iroh_chat.rs: NodeId-only Iroh chat sync using discovery.
  • examples/iroh_topic_intro.rs: introduces a peer to a topic, opens it on the receiver, then rejects membership.
  • examples/iroh_fjall_recovery.rs: reopens an Iroh/Fjall node from disk with the same Iroh secret key.

Run examples with features as needed:

cargo run --features iroh --example iroh_chat
cargo run --features iroh --example iroh_topic_intro
cargo run --features 'iroh fjall' --example iroh_fjall_recovery

About

Distributed Merkle DAGs for iroh topic sync

Resources

Code of conduct

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages