Hippius Hermes Documentation
Hermes is a Bittensor cross-subnet Machine-to-Machine (M2M) communication protocol. It enables trustless, high-bandwidth communication between any two nodes across any Bittensor subnet using Hippius infrastructure.
Dual-Layer Architecture
Low-latency QUIC signaling via iroh with direct UDP hole-punching.
No relays.
Large file transfer via the Hippius Sync-Engine API (Coming Soon!). Control messages carry metadata and keys, not the data itself.
Installation
Python
pip install hippius-hermes
Rust
[dependencies]
hippius-hermes-core = "0.2"
Node Registration
Before a Hermes Node can send or receive messages on the network, its offline Iroh Ed25519
SecretKey must be cryptographically associated with your main Substrate
Wallet in the AccountProfile pallet on the Hippius chain.
We provide two automated CLI registration methods: API (Feeless) and Chain (Direct).
1. Generate an identity
First, generate your local node secret key:
cargo run --bin hippius-hermes-cli -- keygen --out-path hermes.key
2. Register your Node
Configure your hermes_config.json with your SS58 wallet address and the path to
the key you just generated. Then run the interactive registration command:
# Default (API Method - No Substrate Tokens Required!)
cargo run --bin hippius-hermes-cli -- --config hermes_config.json register --ss58 5Grwv... --username my_node
# Direct Chain Method (Requires Substrate Tokens for Gas)
cargo run --bin hippius-hermes-cli -- --config hermes_config.json register --ss58 5Grwv... --method chain
Cryptographic Ownership Verification
Regardless of the method chosen, the CLI will prompt you locally for your Substrate Wallet Mnemonic.
Enter Substrate Wallet Mnemonic (hidden): *****
Security Note: If you use the default API method, your mnemonic is never transmitted. It is exclusively used locally by the Rust Keyring to generate a mathematical Signature over your Iroh NodeId. This signature is then POSTed to the Hippius API, which transparently pays the Extrinsic Gas fees on your behalf.
Configuration
Every Hermes node requires a configuration that identifies the node and controls
its network behavior. The subnet_ids field is the key to ALPN-based
subnet whitelisting.
Config Fields
| Field | Type | Required | Description |
|---|---|---|---|
node_secret_key_path |
string | Yes | Path to the 32-byte Ed25519 secret key file for Iroh node identity |
ss58_address |
string | Yes | Your node's SS58 address on the Bittensor network |
api_token |
string | Yes | Hippius Sync-Engine API token for data plane access (Coming Soon) |
storage_directory |
string | Yes | Local directory for the offline message queue database |
rpc_url |
string | No | Hippius chain RPC endpoint. Default: wss://rpc.hippius.network:443 |
subnet_ids |
array of u16 | No | List of Bittensor subnet netuids to whitelist. Default: []
(cross-subnet only) |
s3 |
object | No | Optional native S3 credentials block containing bucket,
access_key, and secret_key to override the Arion HTTP API.
|
enable_firewall |
boolean | No | If true, Hermes will aggressively drop all incoming connections from
SS58 addresses not injected via client.set_firewall_whitelist([...]).
Default: false
|
pullweights_api_key |
string | No | Optional PullWeights API key for pushing/pulling ML models via the
pullweights.com registry. Default: null
|
enable_queue |
boolean | No | Enable persistent sled message queue. Only needed for listeners.
Default: false |
encryption_key_path |
string | No | Path to 32-byte X25519 secret key file for E2E encrypted messaging.
Default: null |
skip_identity_verification |
boolean | No | Skip on-chain identity verification for incoming P2P data.
Default: false |
JSON Config File
{
"node_secret_key_path": "/etc/hermes/iroh.key",
"ss58_address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"api_token": "sk-your-hippius-token-here",
"storage_directory": ".hermes_data",
"rpc_url": "wss://rpc.hippius.network:443",
"subnet_ids": [42, 69],
"s3": {
"bucket": "hippius-arion",
"access_key": "YOUR_KEY",
"secret_key": "YOUR_SECRET"
},
"enable_firewall": true,
"pullweights_api_key": "sk-pw-your-key-here",
"encryption_key_path": "./hermes_encryption.key"
}
The subnet_ids field is optional. If omitted, the node only accepts the
universal cross-subnet protocol hippius-hermes/1. When specified, the node
additionally accepts per-subnet protocols for each listed netuid.
ALPN Protocol Negotiation
Hermes uses Application-Layer Protocol Negotiation (ALPN) to scope and filter QUIC connections at the TLS level. ALPN is a TLS extension that lets the client and server agree on an application protocol during the TLS handshake — before any application data is exchanged.
Protocol Format
| Protocol | ALPN String | Scope | Always Registered |
|---|---|---|---|
| Cross-subnet | hippius-hermes/1 |
Universal — any node can communicate | Yes |
| Per-subnet | hippius-hermes/subnet/<netuid> |
Scoped to a specific Bittensor subnet | Only if listed in subnet_ids |
| Data plane (P2P) | hippius-hermes/data/1 |
Direct peer-to-peer file streaming | Yes |
How It Works
At startup, the node registers ALPNs with the Iroh QUIC endpoint based on
subnet_ids. The cross-subnet ALPN hippius-hermes/1 is
always registered automatically.
When a sender connects, they specify which ALPN to use. The receiver's TLS config automatically rejects connections that don't match any registered ALPN.
After the TLS handshake, the listener performs a defense-in-depth check by verifying the negotiated ALPN against its registered set before processing any message.
Cross-Subnet Protocol
The cross-subnet ALPN hippius-hermes/1 is the universal protocol that every
Hermes node accepts regardless of configuration. It enables communication between
nodes on different Bittensor subnets.
ALPN: hippius-hermes/1
Sender (Subnet 42) ──── hippius-hermes/1 ────> Receiver (Subnet 69)
Cross-subnet OK
Use this when your message isn't scoped to a specific subnet — for example, when a validator on subnet 42 needs to coordinate with a miner on subnet 69.
subnet_ids config, the node always accepts
hippius-hermes/1 connections. This ensures every node is reachable for
cross-subnet communication.
Per-Subnet Protocols
Per-subnet ALPNs let nodes scope their communication to a specific Bittensor subnet.
When a node registers subnet_ids: [42], it additionally accepts connections
using the ALPN hippius-hermes/subnet/42.
ALPN: hippius-hermes/subnet/42
Sender (Subnet 42) ── hippius-hermes/subnet/42 ──> Receiver (Subnet 42)
Same subnet OK
Sender (Subnet 69) ── hippius-hermes/subnet/42 ──> Receiver (Subnet 42)
Receiver accepts (42 is registered)
Sender (Subnet 69) ── hippius-hermes/subnet/69 ──> Receiver (Subnet 42)
TLS REJECTED (69 not in receiver's subnet_ids)
This means a receiver with subnet_ids: [42] will accept connections
on hippius-hermes/subnet/42 (and the universal hippius-hermes/1), but
will reject connections on hippius-hermes/subnet/69 at the TLS level.
Multi-Subnet Whitelisting
Nodes can participate in multiple subnets simultaneously by listing multiple netuids
in the subnet_ids array. Each netuid adds a corresponding ALPN protocol
to the node's accepted set.
How It Works
Given this configuration:
{
"subnet_ids": [1, 42, 69]
}
The node registers 5 ALPNs:
| # | ALPN | Source |
|---|---|---|
| 1 | hippius-hermes/1 |
Always registered (cross-subnet control) |
| 2 | hippius-hermes/data/1 |
Always registered (direct P2P data) |
| 3 | hippius-hermes/subnet/1 |
From subnet_ids |
| 4 | hippius-hermes/subnet/42 |
From subnet_ids |
| 5 | hippius-hermes/subnet/69 |
From subnet_ids |
Connection Matrix
With multi-subnet whitelisting, the node accepts incoming connections on any of its registered ALPNs:
Node A: subnet_ids = [42, 69]
Accepts: hippius-hermes/1, hippius-hermes/data/1, hippius-hermes/subnet/42, hippius-hermes/subnet/69
Node B: subnet_ids = [42, 100]
Accepts: hippius-hermes/1, hippius-hermes/data/1, hippius-hermes/subnet/42, hippius-hermes/subnet/100
Communication paths:
A -> B via hippius-hermes/1 OK (both accept cross-subnet)
A -> B via hippius-hermes/subnet/42 OK (B has 42 registered)
A -> B via hippius-hermes/subnet/69 FAIL (B does NOT have 69 registered)
A -> B via hippius-hermes/subnet/100 OK (B has 100 registered)
B -> A via hippius-hermes/subnet/100 FAIL (A does NOT have 100 registered)
B -> A via hippius-hermes/subnet/42 OK (A has 42 registered)
hippius-hermes/1
guarantees a fallback path always exists.
Typical Deployment Patterns
Subnet-Only Miner
"subnet_ids": [42]
Accepts traffic from subnet 42 and cross-subnet. Rejects per-subnet traffic from any other subnet.
Multi-Subnet Validator
"subnet_ids": [1, 42, 69, 100]
Participates in 4 subnets simultaneously. Accepts per-subnet traffic from all of them.
Cross-Subnet Relay
"subnet_ids": []
Only accepts the universal hippius-hermes/1 protocol. Acts as a cross-subnet
bridge.
Full-Network Node
"subnet_ids": [1, 2, 3, ..., 255]
Registers ALPNs for every subnet. Accepts all per-subnet and cross-subnet traffic.
Dynamic API Firewall
By default, Hippius Hermes accepts incoming connections from any node that correctly formats the ALPN handshake. To protect validators and miners against volumetric connection DDoS from unauthorized identities, you can enable the O(1) Memory Firewall.
enable_firewall: true is configured, the inner Rust QUIC engine intercepts TLS
handshakes. It resolves the incoming NodeId to its SS58 address. If the SS58 is not
in the whitelist, the connection is instantly killed before any streams are allocated
or async memory is reserved, completely eliminating memory-exhaustion vectors.
Python Whitelist Injection
Validators generally sync the Bittensor Metagraph every 12 seconds. Whenever the metagraph updates, you dynamically inject the list of valid miner SS58 hotkeys into the Rust engine's active memory:
# Inside your Validator sync loop:
valid_hotkeys = metagraph.hotkeys # List of string SS58 addresses
# Instantaneously updates the Rust QUIC Firewall rules
client.set_firewall_whitelist(valid_hotkeys)
Global ACL (Access Control List)
In addition to the subnet firewall, Hermes provides a Nebula-inspired global ACL that applies to all ALPNs (data, cross-subnet, subnet). The ACL has two layers:
Nodes on the blocklist are always rejected, regardless of allowlist status. Blocklist wins.
If the allowlist is non-empty, only listed nodes pass. If the allowlist is empty, all non-blocked nodes are allowed (open policy).
Python Example
# Block specific bad actors
resolved = await client.set_acl_blocklist(["5BadActor1...", "5BadActor2..."])
print(f"Blocked {resolved} nodes")
# Restrict to known-good nodes only
resolved = await client.set_acl_allowlist(metagraph.hotkeys)
print(f"Allowlisted {resolved} nodes")
set_acl_allowlist() and set_acl_blocklist()
return the number of successfully resolved SS58 addresses.
Direct P2P File Transfers
Hermes supports direct peer-to-peer file streaming over QUIC. Files are pushed
directly from sender to receiver using
the hippius-hermes/data/1 ALPN protocol.
How It Works
Sender resolves the receiver's NodeId from their SS58 address and connects on
hippius-hermes/data/1.
Sender writes a length-prefixed JSON header followed by raw file bytes (64KB chunks) over a QUIC bi-stream.
Receiver verifies sender identity, streams bytes to disk, and fires the data callback with file metadata.
Python Example
# Sender: push file directly to peer
filename = await client.send_file_unencrypted(dest_ss58, "./gradients.bin")
# Receiver: register data callback
def on_file(sender_ss58, filename, local_path, file_size):
print(f"Received {filename} ({file_size} bytes) from {sender_ss58}")
print(f"Saved to: {local_path}")
client.start_listener(on_message, on_data=on_file)
Rust Example
// Sender: push file directly to peer
let filename = client
.send_file_unencrypted(dest_ss58, "./gradients.bin", None)
.await?;
// Receiver: register data callback
client.spawn_listener(
|msg| println!("Control: {}", msg.action),
Some(|sender, filename, path, size| {
println!("Received {} ({} bytes) at {}", filename, size, path);
}),
);
End-to-End Encryption (E2EE)
Hermes nodes support secure payload delivery using the TweetNaCl crypto_box standard
(X25519, XSalsa20, Poly1305).
hermes_encryption.key), and the public key is broadcasted to the decentralized
Hippius blockchain
inside the AccountProfile pallet. Senders can query this on-chain state to encrypt
a payload
that ONLY the target Hermes node can decrypt!
How It Works
During hippius-hermes-cli register, a static X25519 keypair is generated.
The private key is saved locally as hermes_encryption.key. The public key
is written to the AccountProfile pallet on the Hippius blockchain.
The sender queries the target's SS58 address on-chain to retrieve their static X25519
public key from the AccountProfile storage.
The sender generates a throwaway ephemeral X25519 keypair for Perfect Forward Secrecy. A shared secret is derived from the ephemeral private key and the target's static public key.
The payload is encrypted using XSalsa20-Poly1305 with the shared secret. The ciphertext and the ephemeral public key are transmitted to the receiver.
The receiver reconstructs the shared secret using their static private key and the sender's ephemeral public key, then decrypts the ciphertext.
Native Hermes E2EE
The Hermes SDK handles E2EE natively — no manual crypto required. The client automatically resolves the recipient's on-chain encryption key and applies NaCl SealedBox encryption.
Python
from hermes import Config, HermesClient
import asyncio
async def main():
config = Config.from_file("hermes_config.json")
client = await HermesClient.create(config)
# Send an E2E encrypted message (NaCl SealedBox)
await client.send_message_encrypted(
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"encrypted_message",
b"top secret payload data"
)
asyncio.run(main())
CLI
hippius-hermes-cli --config hermes_config.json send-encrypted \
--dest-ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
--action "encrypted_message" \
--message "top secret payload data"
Python E2EE Sender (Standalone)
Note: Standard Rust/C++ Hermes nodes natively automate this entire on-chain key
lookup and handshake process over QUIC under the hood! However, if you are writing an external
application (like a Python web backend) that needs to send an encrypted payload to a Hermes node,
you can use the substrate-interface and PyNaCl libraries to securely
encrypt a payload directly from the blockchain state.
from nacl.public import PrivateKey, PublicKey, Box
from nacl.encoding import HexEncoder
from substrateinterface import SubstrateInterface
# 1. Query target's encryption key from the Hippius blockchain
substrate = SubstrateInterface(url="wss://rpc.hippius.network:443")
bob_ss58 = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
result = substrate.query("AccountProfile", "AccountProfiles", [bob_ss58])
bob_pub_hex = result.value['encryption_key'].replace("0x", "")
# 2. Generate ephemeral X25519 keypair for Perfect Forward Secrecy
ephemeral_private = PrivateKey.generate()
ephemeral_public = ephemeral_private.public_key
# 3. Encrypt with TweetNaCl crypto_box (X25519 + XSalsa20-Poly1305)
bob_pub = PublicKey(bob_pub_hex.encode("utf-8"), encoder=HexEncoder)
sender_box = Box(ephemeral_private, bob_pub)
message = b"Secure model weights for subnet 42"
encrypted = sender_box.encrypt(message) # Nonce auto-prepended
# 4. Transmit encrypted payload + ephemeral public key to Bob
# Bob needs: encrypted (bytes), ephemeral_public (bytes)
print(f"Encrypted {len(encrypted)} bytes — only Bob can decrypt")
Python E2EE Receiver
The receiving Hermes node decrypts using its static private key and the sender's ephemeral public key:
from nacl.public import PrivateKey, PublicKey, Box
from nacl.encoding import RawEncoder
# 1. Bob loads his static private key from disk (generated during registration)
with open("hermes_encryption.key", "rb") as f:
bob_private = PrivateKey(f.read(), encoder=RawEncoder)
# 2. Reconstruct the Box using Bob's private key + Alice's ephemeral public key
alice_ephemeral_pub = PublicKey(received_ephemeral_bytes, encoder=RawEncoder)
bob_box = Box(bob_private, alice_ephemeral_pub)
# 3. Decrypt — XSalsa20-Poly1305 authenticates and decrypts in one step
plaintext = bob_box.decrypt(received_ciphertext)
print(f"Decrypted: {plaintext.decode('utf-8')}")
Full Roundtrip Demo
A complete self-contained example that generates keys and demonstrates the full encrypt → transmit → decrypt lifecycle:
from nacl.public import PrivateKey, Box
from nacl.encoding import HexEncoder
# === Setup: Bob registers his static key on-chain ===
bob_static_private = PrivateKey.generate()
bob_static_public = bob_static_private.public_key
print(f"Bob registered: {bob_static_public.encode(encoder=HexEncoder).decode()}")
# === Alice encrypts a message for Bob ===
alice_ephemeral = PrivateKey.generate()
alice_box = Box(alice_ephemeral, bob_static_public)
encrypted = alice_box.encrypt(b"Secure P2P delivery verified.")
print(f"Alice encrypted: {len(encrypted)} bytes")
# === Bob decrypts using his static key + Alice's ephemeral public ===
bob_box = Box(bob_static_private, alice_ephemeral.public_key)
plaintext = bob_box.decrypt(encrypted)
print(f"Bob decrypted: '{plaintext.decode()}'")
# Output: Bob decrypted: 'Secure P2P delivery verified.'
Direct P2P vs Sync-Engine
Hermes offers two data transfer modes. Choose based on your use case:
| Feature | Direct P2P | Sync-Engine |
|---|---|---|
| Transport | QUIC bi-stream (hippius-hermes/data/1) |
HTTP upload/download via Arion |
| Intermediary | None — pure P2P | Hippius Sync-Engine server |
| Receiver must be online | Yes — at time of send | No — stored until retrieved |
| Best for | Real-time streaming: gradients, tensors, live data | Large persistent payloads: model weights, datasets |
| API (Python) | send_file_unencrypted() |
send_file_via_s3() |
| Receiving | Automatic via on_data callback |
Explicit download_file_http() |
S3 Pre-Signed Downloads
For transmitting incredibly large multi-gigabyte models (e.g. LLM checkpoints), the Hippius Sync-Engine HTTP API can become a minor bottleneck because it streams data through the Arion servers for billing verification.
Hermes solves this by allowing Sender nodes to pre-sign native Amazon S3 GET queries perfectly offline.
The Sender node (configured with S3 API keys) generates an S3 Pre-Signed URL for the file valid for exactly 24 hours, entirely locally without making any network calls to AWS.
The Sender injects this raw Pre-Signed URL into the
hermes.message(dest, payload) control packet. It transmits to the receiver
instantly over the encrypted Iroh Control Plane.
The Receiver intercepts the control message payload, extracts the URL, and invokes the
Rust download_file_http async method to pull the 10GB payload straight from
the cloud bucket to disk at maximum line speed (10+ Gbps).
PullWeights Model Registry
Hippius Hermes natively integrates with the PullWeights enterprise model registry, allowing seamless pushing and pulling of machine learning models via the CLI.
hermes_config.json under the pullweights_api_key field.
Pushing a Model
You can push a local model direct to the PullWeights registry via the CLI:
hippius-hermes-cli push-model \
--org my-org \
--model my-llm \
--file-path ./model.safetensors
Pulling a Model
You can instantly pull specific tags down to your local directory (defaulting to the current working
directory). Unauthenticated pulling is supported for public models, but private models utilize your
pullweights_api_key.
hippius-hermes-cli pull-model \
--org my-org \
--model my-llm \
--tag v1.0.0 \
--download-dir /mnt/models
Python API
Config
from hermes import Config
# From keyword arguments
config = Config(
node_secret_key_path="/etc/hermes/iroh.key",
ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
api_token="sk-your-token",
storage_directory=".hermes_data",
rpc_url=None, # Optional, defaults to wss://rpc.hippius.network:443
subnet_ids=[42, 69], # Optional, defaults to []
s3_bucket="my-models", # Optional S3 Bypass
s3_access_key="AKIA...",
s3_secret_key="AWS...",
enable_firewall=True, # Optional O(1) TLS connection dropping
pullweights_api_key="sk-...", # Optional PullWeights token
enable_queue=True, # Optional persistent sled message queue
encryption_key_path="./hermes_encryption.key" # Optional E2E encryption key
)
# From JSON file
config = Config.from_file("hermes_config.json")
HermesClient
from hermes import Config, HermesClient
# Create client (async)
client = await HermesClient.create(config)
# Upload to S3 and send Pre-Signed URL to peer (No Sync-Engine)
await client.send_file_via_s3(dest_ss58, "./model.bin")
# Download from a Pre-Signed URL
await client.download_file_http(presigned_url, "./model.bin")
# Update the memory firewall (drops unauthorized TLS at O(1))
resolved = await client.set_firewall_whitelist(["5Grwv...", "5FHne..."])
# Global ACL: blocklist / allowlist (applies to all ALPNs)
await client.set_acl_blocklist(["5BadActor..."])
await client.set_acl_allowlist(["5Trusted1...", "5Trusted2..."])
# Start listener (callback receives action, sender_ss58, payload_bytes)
def on_message(action, sender_ss58, payload):
print(f"From {sender_ss58}: {action}")
client.start_listener(on_message)
# Direct P2P file transfer
filename = await client.send_file_unencrypted(dest_ss58, file_path)
# Start listener with data callback for direct P2P
def on_file(sender_ss58, filename, local_path, file_size):
print(f"Got {filename} from {sender_ss58}")
client.start_listener(on_message, on_data=on_file)
# Send E2E encrypted message (NaCl SealedBox)
await client.send_message_encrypted(dest_ss58, "encrypted_message", b"secret payload")
# Start background retry worker (requires enable_queue=True in config)
client.start_retry_worker()
Rust API
Config
use hippius_hermes_core::Config;
use std::path::PathBuf;
let config = Config {
node_secret_key_path: PathBuf::from("/etc/hermes/iroh.key"),
ss58_address: "5GrwvaEF5zXb26...".to_string(),
api_token: "sk-your-token".to_string(),
storage_directory: PathBuf::from(".hermes_data"),
rpc_url: "wss://rpc.hippius.network:443".to_string(),
subnet_ids: vec![42, 69],
enable_firewall: true,
s3: Some(hippius_hermes_core::config::S3Config {
bucket: "my-models".into(),
access_key: "AKIA...".into(),
secret_key: "AWS...".into(),
}),
pullweights_api_key: Some("sk-...".to_string()),
skip_identity_verification: false,
enable_queue: false,
encryption_key_path: Some(PathBuf::from("./hermes_encryption.key")),
};
Client
use hippius_hermes_core::Client;
let client = Client::new(config).await?;
// Upload to S3 and send Pre-Signed URL to peer
client.send_file_via_s3("5Dest...", "./model.bin", None).await?;
// Download from a Pre-Signed URL
client.download_file_http("https://...", "./model.bin").await?;
// Update the memory firewall (drops unauthorized TLS at O(1))
let resolved = client.set_firewall_whitelist(vec!["5Grwv...".into(), "5FHne...".into()]).await;
// Global ACL
client.set_acl_blocklist(vec!["5Bad...".into()]).await;
client.set_acl_allowlist(vec!["5Good...".into()]).await;
// Start listener (control callback + optional data callback)
client.spawn_listener(
|msg| println!("From {}: {}", msg.sender_ss58, msg.action),
None::<fn(String, String, String, u64)>,
);
// Direct P2P file transfer
let filename = client
.send_file_unencrypted(dest_ss58, file_path, None)
.await?;
// Start listener with data callback
client.spawn_listener(
|msg| println!("Control: {}", msg.action),
Some(|sender: String, filename: String, path: String, size: u64| {
println!("File: {} from {}", filename, sender);
}),
);
// Send E2E encrypted message (NaCl SealedBox)
client.send_message_encrypted("5Dest...", "encrypted_message", b"secret payload".to_vec(), None).await?;
// Start background retry worker (requires enable_queue in config)
client.spawn_retry_worker();
ALPN Utilities
use hippius_hermes_core::network::node::{
CROSS_SUBNET_ALPN, subnet_alpn, build_alpn_list
};
// Cross-subnet constant
assert_eq!(CROSS_SUBNET_ALPN, b"hippius-hermes/1");
// Build per-subnet ALPN
let alpn = subnet_alpn(42);
assert_eq!(alpn, b"hippius-hermes/subnet/42");
// Build full ALPN list from config
let alpns = build_alpn_list(&[42, 69]);
// Returns: ["hippius-hermes/1", "hippius-hermes/data/1", "hippius-hermes/subnet/42", "hippius-hermes/subnet/69"]
CLI Reference
Hermes provides a native Rust CLI for generating deterministic keys, running a listener daemon, and testing direct P2P transfers from the terminal.
1. Key Generation
Generate a secure, random Iroh secret key (NodeId) for your Hermes installation:
cargo run --bin hippius-hermes-cli -- keygen \
--out-path examples/iroh.key
2. Backup / Reveal NodeId
Read an existing Iroh secret key from disk and print its corresponding public NodeId. Send this NodeId to peers so they can resolve your address offline.
cargo run --bin hippius-hermes-cli -- backup-key \
--key-path examples/iroh.key
3. Start Listener
Start the Hermes daemon to listen for cross-subnet control messages and direct P2P file transfers:
cargo run --bin hippius-hermes-cli -- --config examples/hermes_config.json listen
4. Send File Directly
Push a file directly to a peer over QUIC (bypassing the Sync-Engine). For offline testing without the node tracker chain, explicitly provide the receiver's NodeId.
cargo run --bin hippius-hermes-cli -- --config examples/sender_config.json send-direct \
--dest-ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
--peer-node-id <RECEIVER_NODE_ID> \
--file-path ./model.bin
5. Send Encrypted Message
Send an E2E encrypted message using NaCl SealedBox. The recipient's encryption key is resolved from the on-chain AccountProfile pallet.
cargo run --bin hippius-hermes-cli -- --config examples/sender_config.json send-encrypted \
--dest-ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
--message "top secret payload"
6. Send via S3
Upload a file to S3, generate a pre-signed URL, and send it to the peer over QUIC.
cargo run --bin hippius-hermes-cli -- --config examples/sender_config.json send-via-s3 \
--dest-ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY \
--file-path ./large_model.bin
7. Upload to S3
Upload a file directly to your configured S3 bucket without sending it to a peer.
cargo run --bin hippius-hermes-cli -- --config examples/sender_config.json upload-s3 \
--file-path ./model.bin
8. Download from URL
Download a file from a pre-signed URL (or any HTTP URL) to a local path.
cargo run --bin hippius-hermes-cli -- download-url \
--url "https://s3.example.com/model.bin?X-Amz-..." \
--dest-path ./model.bin
9. Register Node
Register your node's Iroh NodeId and encryption public key on the Hippius blockchain.
cargo run --bin hippius-hermes-cli -- --config examples/hermes_config.json register \
--ss58 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
10. Push Model
Push a local model directory to the PullWeights registry.
cargo run --bin hippius-hermes-cli -- --config examples/hermes_config.json push-model \
--org my-org \
--model my-llm \
--file-path ./model.safetensors
11. Pull Model
Pull a model from the PullWeights registry to a local directory.
cargo run --bin hippius-hermes-cli -- pull-model \
--org my-org \
--model my-llm \
--tag v1.0.0 \
--download-dir /mnt/models
Example: Single Subnet Node
A miner that only participates in subnet 42:
import asyncio
from hermes import Config, HermesClient
async def main():
config = Config(
node_secret_key_path="/etc/hermes/iroh.key",
ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
api_token="sk-your-token",
storage_directory=".hermes_data",
subnet_ids=[42] # Only accept subnet-42 and cross-subnet traffic
)
client = await HermesClient.create(config)
# Node now accepts: hippius-hermes/1 + hippius-hermes/subnet/42
def on_message(action, sender_ss58, payload):
print(f"[Subnet 42] {sender_ss58}: {action}")
client.start_listener(on_message)
client.start_retry_worker()
# Keep alive
while True:
await asyncio.sleep(3600)
if __name__ == "__main__":
asyncio.run(main())
Example: Multi-Subnet Validator
A validator participating in subnets 1, 42, and 69 simultaneously. Incoming connections on any of these subnet-specific protocols are accepted.
import asyncio
from hermes import Config, HermesClient
async def main():
config = Config(
node_secret_key_path="/etc/hermes/iroh.key",
ss58_address="5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
api_token="sk-validator-token",
storage_directory="/var/hermes",
subnet_ids=[1, 42, 69] # Accept traffic from all three subnets
)
client = await HermesClient.create(config)
# Node accepts: hippius-hermes/1, hippius-hermes/subnet/1, hippius-hermes/subnet/42, hippius-hermes/subnet/69
def handle_message(action, sender_ss58, payload):
print(f"[Multi-Subnet Validator] {action} from {sender_ss58}")
# Route based on action or parse payload metadata
if action == "process_data_unencrypted_store":
import json
meta = json.loads(payload)
print(f" HCFS Hash: {meta['hash']}")
client.start_listener(handle_message)
client.start_retry_worker()
while True:
await asyncio.sleep(3600)
if __name__ == "__main__":
asyncio.run(main())
Example: Cross-Subnet Communication
Two nodes on different subnets communicating via the universal hippius-hermes/1
protocol. This works regardless of subnet_ids configuration.
import asyncio
from hermes import Config, HermesClient
async def main():
# This node is on subnet 42 but needs to talk to subnet 69
config = Config(
node_secret_key_path="/etc/hermes/iroh.key",
ss58_address="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
api_token="sk-your-token",
storage_directory=".hermes_data",
subnet_ids=[42]
)
client = await HermesClient.create(config)
# send_file_unencrypted uses hippius-hermes/data/1 (direct P2P QUIC).
# The receiver accepts hippius-hermes/1 regardless of their subnet_ids config.
dest_ss58 = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
filename = await client.send_file_unencrypted(
dest_ss58,
"./gradients.safetensors"
)
print(f"Sent cross-subnet! File: {filename}")
if __name__ == "__main__":
asyncio.run(main())
Example: Rust Multi-Subnet Node
use hippius_hermes_core::{Client, Config};
use std::path::PathBuf;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = Config::from_file("hermes_config.json")?;
// Or construct manually:
// let config = Config {
// node_secret_key_path: PathBuf::from("/etc/hermes/iroh.key"),
// ss58_address: "5GrwvaEF...".to_string(),
// ...
// };
let client = Client::new(config).await?;
// Node accepts: hippius-hermes/1, hippius-hermes/subnet/42, hippius-hermes/subnet/69
client.spawn_listener(
|msg| println!("[Hermes] {} from {}", msg.action, msg.sender_ss58),
None::<fn(String, String, String, u64)>,
);
client.spawn_retry_worker();
// Keep alive
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
}
}