Skip to main content

Crate btlightning

Crate btlightning 

Source
Expand description

Ligh𝞽ning

Rust QUIC transport layer for Bittensor

Persistent QUIC connections with sr25519 handshake authentication for validator-miner communication.

crates.io docs.rs PyPI

§Python

pip install btlightning
from btlightning import Lightning

client = Lightning(wallet_hotkey="5GrwvaEF...")
client.set_python_signer(my_signer_callback)
client.initialize_connections([
    {"hotkey": "5FHneW46...", "ip": "192.168.1.1", "port": 8443}
])
response = client.query_axon(
    {"hotkey": "5FHneW46...", "ip": "192.168.1.1", "port": 8443},
    {"synapse_type": "MyQuery", "data": {"key": "value"}}
)

§Rust

[dependencies]
btlightning = { version = "0.1", features = ["subtensor"] }

With the subtensor feature, the client discovers miners from the metagraph and keeps connections in sync automatically:

use btlightning::{LightningClient, LightningClientConfig, Sr25519Signer, MetagraphMonitorConfig};

let config = LightningClientConfig {
    metagraph: Some(MetagraphMonitorConfig::finney(YOUR_NETUID)),
    ..Default::default()
};
let mut client = LightningClient::with_config("5GrwvaEF...".into(), config)?;
client.set_signer(Box::new(Sr25519Signer::from_seed(seed)));
client.initialize_connections(vec![]).await?;

Without subtensor, pass miner addresses directly:

use btlightning::{LightningClient, Sr25519Signer, QuicAxonInfo};

let mut client = LightningClient::new("5GrwvaEF...".into());
client.set_signer(Box::new(Sr25519Signer::from_seed(seed)));
client.initialize_connections(vec![
    QuicAxonInfo::new("5FHneW46...".into(), "192.168.1.1".into(), 8443, 4)
]).await?;

Query a single miner by axon info and deserialize the response:

use btlightning::QuicRequest;
use serde::{Serialize, Deserialize};

#[derive(Serialize)]
struct MyQuery { prompt: String }

#[derive(Deserialize)]
struct MyResult { answer: String }

let request = QuicRequest::from_typed("MyQuery", &MyQuery { prompt: "hello".into() })?;
let response = client.query_axon(axon_info, request).await?.into_result()?;
let result: MyResult = response.deserialize_data()?;

Fan out to every QUIC miner on the subnet concurrently:

use std::sync::Arc;
use tokio::task::JoinSet;

let client = Arc::new(client);
let miners = metagraph.quic_miners();
let mut tasks = JoinSet::new();
for miner in miners {
    let req = QuicRequest::from_typed("MyQuery", &MyQuery { prompt: "hello".into() })?;
    let client = Arc::clone(&client);
    tasks.spawn(async move {
        (miner.hotkey.clone(), client.query_axon(miner, req).await)
    });
}
while let Some(Ok((hotkey, result))) = tasks.join_next().await {
    match result {
        Ok(resp) => { /* handle response from hotkey */ }
        Err(e) => { /* miner failed */ }
    }
}

§Flood defense

The server applies two filters at the quinn::Incoming boundary, before any per-connection state is allocated. Both are operator-configurable on LightningServerConfig.

FieldDefaultEffect
enforce_source_allowlistfalseWhen true, drops any connection whose source IP is not in the cached allowlist. Backed by the SourceAddressResolver trait; refresh interval source_allowlist_refresh_secs (default 300s). Drops use Incoming::ignore() so no response packet is emitted, eliminating reflection-amplification surface.
require_address_validationtrueWhen true, unvalidated peers are answered with a QUIC Retry packet via Incoming::retry(), forcing a token round-trip before connection state is allocated. Defeats spoofed-source Initial floods.

Observability:

  • info! Initial source-address allowlist resolution: N allowed IPs on startup
  • info! Refreshed source-address allowlist: N allowed IPs per refresh
  • warn! source-address allowlist is empty under enforce_source_allowlist=true; ALL connections will be silently dropped if the resolver returns empty or errors while enforcement is on
  • info! QUIC address validation enabled -- all clients must complete a Retry round-trip on startup when require_address_validation is on
  • LightningServer::get_allowed_source_count() returns the size of the cached allowlist for embedding in operator dashboards

Pre-flight numbers, measured locally with a quinn-driven flood at concurrency 64 over 10s:

Modeaccepts/shandshakes_completed/s
no defenses8,4208,420
source allowlist only6400
address-validation only8,7108,297
both6380

Address validation alone provides limited mitigation against attackers using real reachable source IPs (the Retry round-trip completes); it is decisive against spoofed-source floods. Source allowlist enforcement reduces application-layer cost on flood traffic to zero. Both together provide defense in depth across the metagraph cold-start and resolver-failure windows.

This filter does not reduce inbound bandwidth or kernel UDP buffer pressure. Operators on saturated uplinks must combine these with upstream scrubbing or a kernel-level allowlist (e.g. nftables).

§Build from source

cargo build -p btlightning
maturin develop --manifest-path crates/btlightning-py/Cargo.toml

§Performance

Benchmarked on Apple Silicon (M-series), macOS, loopback networking. Echo handler returns input unchanged. Connection setup includes first request-response round trip. Lightning authenticates once at connection time and amortizes over all subsequent requests; bittensor signs and verifies every request. Bittensor payloads are base64-encoded (JSON has no binary type), adding ~33% wire overhead beyond the nominal payload size. Source: benchmarks/.

bittensor (dendrite/axon)lightning
ProtocolHTTP/1.1QUIC
SerializationJSONMessagePack
Transport encryptionNoneTLS 1.3
Auth modelPer-requestPer-connection
Metricbittensorlightning (Python)lightning (Rust)
Connection setup (p50)114.46 ms2.68 ms0.71 ms
Latency p50 (1KB)24.01 ms0.08 ms0.05 ms
Latency p99 (1KB)28.80 ms0.09 ms0.07 ms
Throughput (1KB)41 req/s26,539 req/s68,650 req/s
Wire size (1KB payload)2,032 bytes1,052 bytes1,052 bytes
ColorSystem
bittensorbittensor
lightning-pylightning (Python)
lightninglightning (Rust)

[!NOTE] Charts use log10 scale. Refer to the tables below for raw values.

§Latency p50

---
config:
    xyChart:
        plotReservedSpacePercent: 60
    themeVariables:
        xyChart:
            backgroundColor: "#000000"
            plotColorPalette: "#808080, #FF8C00, #FFE000"
            titleColor: "#ffffff"
            xAxisLabelColor: "#aaaaaa"
            xAxisTitleColor: "#cccccc"
            xAxisTickColor: "#666666"
            xAxisLineColor: "#444444"
            yAxisLabelColor: "#aaaaaa"
            yAxisTitleColor: "#cccccc"
            yAxisTickColor: "#666666"
            yAxisLineColor: "#444444"
---
xychart-beta
    x-axis ["256B", "1KB", "10KB", "100KB", "1MB"]
    y-axis "log10(us)" 0 --> 5
    bar "bittensor" [4.39, 4.38, 4.39, 4.42, 4.61]
    bar "lightning (Python)" [1.90, 1.90, 2.36, 3.27, 4.31]
    bar "lightning (Rust)" [1.70, 1.70, 2.04, 2.84, 3.87]

§Throughput (req/s)

---
config:
    xyChart:
        plotReservedSpacePercent: 60
    themeVariables:
        xyChart:
            backgroundColor: "#000000"
            plotColorPalette: "#FFE000, #FF8C00, #808080"
            titleColor: "#ffffff"
            xAxisLabelColor: "#aaaaaa"
            xAxisTitleColor: "#cccccc"
            xAxisTickColor: "#666666"
            xAxisLineColor: "#444444"
            yAxisLabelColor: "#aaaaaa"
            yAxisTitleColor: "#cccccc"
            yAxisTickColor: "#666666"
            yAxisLineColor: "#444444"
---
xychart-beta
    x-axis ["256B", "1KB", "10KB", "100KB", "1MB"]
    y-axis "log10(req/s)" 0 --> 5
    bar "lightning (Rust)" [4.92, 4.84, 4.27, 3.26, 2.26]
    bar "lightning (Python)" [4.55, 4.42, 3.83, 2.94, 1.94]
    bar "bittensor" [1.61, 1.61, 1.60, 1.57, 1.40]
Full results (all payload sizes)
§Latency (ms)
PayloadSystemp50p95p99
256Bbittensor24.5028.0435.05
lightning (py)0.080.190.23
lightning (rust)0.050.060.07
1KBbittensor24.0125.5728.80
lightning (py)0.080.090.09
lightning (rust)0.050.060.07
10KBbittensor24.7025.7026.59
lightning (py)0.230.240.26
lightning (rust)0.110.120.13
100KBbittensor26.1327.3129.42
lightning (py)1.882.252.37
lightning (rust)0.690.730.75
1MBbittensor40.7243.1148.00
lightning (py)20.5522.4823.31
lightning (rust)7.4012.3617.02
§Throughput (req/s)
Payloadbittensorlightning (Python)lightning (Rust)
256B4135,18183,460
1KB4126,53968,650
10KB406,72918,686
100KB388751,819
1MB2588181
§Wire overhead (bytes)
Payloadbittensorlightning
256B1,008284
1KB2,0321,052
10KB14,32110,268
100KB137,202102,430
1MB1,398,7711,048,606

Re-exports§

pub use client::LightningClient;
pub use client::LightningClientConfig;
pub use client::LightningClientConfigBuilder;
pub use client::StreamingResponse;
pub use error::LightningError;
pub use error::Result;
pub use metagraph::is_valid_ip;
pub use metagraph::Metagraph;
pub use metagraph::MetagraphMonitorConfig;
pub use metagraph::NeuronInfo;
pub use metagraph::FINNEY_ENDPOINT;
pub use metagraph::TESTNET_ENDPOINT;
pub use server::typed_async_handler;
pub use server::typed_handler;
pub use server::AsyncSynapseHandler;
pub use server::HandshakeObserver;
pub use server::LightningServer;
pub use server::LightningServerConfig;
pub use server::LightningServerConfigBuilder;
pub use server::SourceAddressResolver;
pub use server::SourceAllowlist;
pub use server::StreamingSynapseHandler;
pub use server::SynapseHandler;
pub use server::ValidatorPermitResolver;
pub use signing::BtWalletSigner;
pub use signing::CallbackSigner;
pub use signing::Signer;
pub use signing::Sr25519Signer;
pub use types::parse_frame_header;
pub use types::HandshakeRequest;
pub use types::HandshakeResponse;
pub use types::MessageType;
pub use types::PeerAddr;
pub use types::QuicAxonInfo;
pub use types::QuicRequest;
pub use types::QuicResponse;
pub use types::SynapsePacket;
pub use types::SynapseResponse;
pub use types::DEFAULT_MAX_FRAME_PAYLOAD;

Modules§

client
error
metagraph
server
signing
types
util