Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PoW Epoch Hot-Swap and HostRoutingRecord Publisher

File: kinetic-host/src/heartbeat.rs Crate: kinetic-host | Stage: 13


What Is This?

The most unique code in kinetic-host. Two background tasks run here:

  1. start_dynamic_routing_publisher() — Every 30 seconds, signs and publishes a HostRoutingRecord to the DHT, telling clients “I am still alive and my current ephemeral Peer ID is X.”
  2. start_drand_heartbeat() — Every 3 seconds, fetches the latest Drand beacon, checks if the current PoW key is still valid for this epoch, and if not: aborts the old network loop, mines a new keypair, and restarts the loop with zero downtime.

There is nothing equivalent to this in kinetic-daemon or kinetic-node. This is the host’s core Sybil-resistance mechanism.


How start_dynamic_routing_publisher Works

-> See: kinetic-host/src/heartbeat.rs — Lines 10–53

Why this is needed

The host’s active P2P identity (its libp2p Peer ID) changes every Drand epoch. Clients looking up saif.kin find a PeerId in the DNS zone record — but that Peer ID is the static host key, not the ephemeral one. Without a routing record, clients have no way to find the current ephemeral peer to connect to.

HostRoutingRecord bridges this gap: it maps (static host PeerId) → (current ephemeral PeerId, drand_kyn, signature).


Key conversion chain (Ed25519 format bridging)

-> See: kinetic-host/src/heartbeat.rs — Lines 18–26

libp2p uses its own Keypair type. ed25519_dalek is needed to produce the actual signature. The conversion chain:

  1. publisher_host_key.try_into_ed25519() → libp2p’s internal ed25519::Keypair.
  2. .to_bytes() → raw 64-byte representation.
  3. ed25519_dalek::SigningKey::try_from(&ed_bytes[0..32]) → dalek key using only the 32-byte private key portion.

The dalek signing key is constructed once outside the loop and reused on every interval tick — no redundant key parsing overhead.


The record and signature

-> See: kinetic-host/src/heartbeat.rs — Lines 33–51

#![allow(unused)]
fn main() {
let mut record = HostRoutingRecord {
    host_id: host_peer_id_str.clone(),  // permanent static Peer ID
    current_peer_id: local_peer_id_str.read()...clone(), // current ephemeral Peer ID
    drand_kyn,          // current beacon round number
    signature: vec![],  // filled after signing
};
let signature = dalek_kp.sign(&record.signable_bytes(NETWORK_ID));
record.signature = signature.to_bytes().to_vec();
}

The current_peer_id is read from the Arc<RwLock<String>> shared with the hot-swap heartbeat. After each hot-swap, the heartbeat writes the new Peer ID into this shared string. The next tick of the publisher will automatically pick up the updated ID.


How start_drand_heartbeat Works (The Hot-Swap)

-> See: kinetic-host/src/heartbeat.rs — Lines 61–162

Tick interval and data flow

Runs every 3 seconds. Calls hb_drand.fetch_latest(). If the result is a fresh (non-cached, non-unavailable) kyn, sends it through drand_kyn_tx (the watch channel that all tasks share).


Staggered epoch check

-> See: kinetic-host/src/heartbeat.rs — Lines 86–93

#![allow(unused)]
fn main() {
let current_epoch = kinetic_network::pow::get_staggered_epoch(
    &hb_local_peer_id.to_bytes(),
    kyn.kyn,
);
let needs_validation = match last_verified_epoch {
    Some(epoch) => epoch != current_epoch,
    None => true,
};
}

get_staggered_epoch() computes a per-peer-id staggered epoch from the kyn. “Staggered” means different peers rotate their keys at different kyn offsets — not all simultaneously. This prevents a network-wide reconnection storm when a Drand epoch boundary passes.

last_verified_epoch caches the last epoch we validated against. On epoch change, needs_validation becomes true and we re-check PoW validity.


PoW validity check

-> See: kinetic-host/src/heartbeat.rs — Lines 97–108

#![allow(unused)]
fn main() {
let pow_valid = tokio::task::spawn_blocking(move || {
    kinetic_network::pow::is_valid_sybil_pow(
        &peer_id_clone, kyn_round, POW_DIFFICULTY_BITS,
    )
}).await.unwrap_or(false);
}

PoW validation is also CPU-bound. Offloaded to spawn_blocking. Returns false if the current Peer ID no longer satisfies the PoW constraint for the new epoch.


The hot-swap sequence (zero-downtime restart)

-> See: kinetic-host/src/heartbeat.rs — Lines 109–154

When pow_valid is false:

1. Mine a new keypair (offloaded, CPU-bound):

#![allow(unused)]
fn main() {
let current_local_key = tokio::task::spawn_blocking(move || {
    kinetic_network::pow::mine_sybil_keypair(kyn_round, POW_DIFFICULTY_BITS)
}).await.unwrap_or_else(|_| Keypair::generate_ed25519());
}

If mining panics (extremely unlikely), falls back to a random Ed25519 key so the process keeps running.

2. Update shared Peer ID:

#![allow(unused)]
fn main() {
if let Ok(mut lock) = shared_peer_id.write() {
    *lock = hb_local_peer_id.to_string();
}
}

The routing publisher will pick up the new ID on its next 30-second tick.

3. Abort the old network loop and replace it:

#![allow(unused)]
fn main() {
let mut handle = loop_handle_ref.lock().await;
handle.abort();  // Kill the old libp2p swarm
// Create new NetworkEventLoop with the new key
if let Ok((new_client, new_loop)) = NetworkEventLoop::new(..., current_local_key, ...) {
    hc_client.update_backend(new_client.get_sender(), new_client.stream_control());
    *handle = tokio::spawn(async move { new_loop.run().await; });
}
}

Important

hc_client.update_backend() is the critical step: it replaces the internal sender and stream control inside the NetworkClient that all other tasks (proxy handler, routing publisher) hold references to. This means those tasks don’t need to be restarted — they continue using the same NetworkClient struct, which now routes through the new swarm.


Quick Reference

FunctionIntervalWhat It Does
start_dynamic_routing_publisher30 secondsSigns + publishes HostRoutingRecord to DHT
start_drand_heartbeat3 secondsFetches Drand kyn, checks PoW validity, hot-swaps if expired

Note

Key concept: The hot-swap is zero-downtime because NetworkClient::update_backend() swaps the internal channel sender in-place. No other task needs to be told about the change.

Tip

Staggered epochs: Peers rotate at different kyn offsets (derived from their Peer ID bytes) to avoid a network-wide reconnection storm.