<div align="center">
    <h2>Ligh𝞽ning</h2>
    <p><strong>Rust QUIC transport layer for Bittensor</strong></p>
    <p>Persistent QUIC connections with sr25519 handshake authentication for validator-miner communication.</p>
    <p>
        <a href="https://crates.io/crates/btlightning"><img src="https://img.shields.io/crates/v/btlightning" alt="crates.io"></a>
        <a href="https://docs.rs/btlightning/latest/btlightning/"><img src="https://img.shields.io/docsrs/btlightning" alt="docs.rs"></a>
        <a href="https://pypi.org/project/btlightning/"><img src="https://img.shields.io/pypi/v/btlightning" alt="PyPI"></a>
    </p>
</div>

## Python

```bash
pip install btlightning
```

```python
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

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

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

```rust,ignore
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:

```rust,ignore
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:

```rust,ignore
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:

```rust,ignore
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`.

| Field | Default | Effect |
|---|---|---|
| `enforce_source_allowlist` | `false` | When `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_validation` | `true` | When `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:

| Mode | accepts/s | handshakes_completed/s |
|---|---:|---:|
| no defenses | 8,420 | 8,420 |
| source allowlist only | 640 | 0 |
| address-validation only | 8,710 | 8,297 |
| both | 638 | 0 |

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

```bash
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/`](benchmarks/).

| | bittensor (dendrite/axon) | lightning |
|---|---|---|
| Protocol | HTTP/1.1 | QUIC |
| Serialization | JSON | MessagePack |
| Transport encryption | None | TLS 1.3 |
| Auth model | Per-request | Per-connection |

| Metric | bittensor | lightning (Python) | lightning (Rust) |
|---|---|---|---|
| Connection setup (p50) | 114.46 ms | 2.68 ms | 0.71 ms |
| Latency p50 (1KB) | 24.01 ms | 0.08 ms | 0.05 ms |
| Latency p99 (1KB) | 28.80 ms | 0.09 ms | 0.07 ms |
| Throughput (1KB) | 41 req/s | 26,539 req/s | 68,650 req/s |
| Wire size (1KB payload) | 2,032 bytes | 1,052 bytes | 1,052 bytes |

| Color | System |
|---|---|
| ![bittensor](https://img.shields.io/badge/-808080?style=flat-square) | bittensor |
| ![lightning-py](https://img.shields.io/badge/-FF8C00?style=flat-square) | lightning (Python) |
| ![lightning](https://img.shields.io/badge/-FFE000?style=flat-square) | lightning (Rust) |

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

### Latency p50

```mermaid
---
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)

```mermaid
---
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]
```

<details>
<summary>Full results (all payload sizes)</summary>

#### Latency (ms)

| Payload | System | p50 | p95 | p99 |
|---|---|---|---|---|
| 256B | bittensor | 24.50 | 28.04 | 35.05 |
| | lightning (py) | 0.08 | 0.19 | 0.23 |
| | lightning (rust) | 0.05 | 0.06 | 0.07 |
| 1KB | bittensor | 24.01 | 25.57 | 28.80 |
| | lightning (py) | 0.08 | 0.09 | 0.09 |
| | lightning (rust) | 0.05 | 0.06 | 0.07 |
| 10KB | bittensor | 24.70 | 25.70 | 26.59 |
| | lightning (py) | 0.23 | 0.24 | 0.26 |
| | lightning (rust) | 0.11 | 0.12 | 0.13 |
| 100KB | bittensor | 26.13 | 27.31 | 29.42 |
| | lightning (py) | 1.88 | 2.25 | 2.37 |
| | lightning (rust) | 0.69 | 0.73 | 0.75 |
| 1MB | bittensor | 40.72 | 43.11 | 48.00 |
| | lightning (py) | 20.55 | 22.48 | 23.31 |
| | lightning (rust) | 7.40 | 12.36 | 17.02 |

#### Throughput (req/s)

| Payload | bittensor | lightning (Python) | lightning (Rust) |
|---|---|---|---|
| 256B | 41 | 35,181 | 83,460 |
| 1KB | 41 | 26,539 | 68,650 |
| 10KB | 40 | 6,729 | 18,686 |
| 100KB | 38 | 875 | 1,819 |
| 1MB | 25 | 88 | 181 |

#### Wire overhead (bytes)

| Payload | bittensor | lightning |
|---|---|---|
| 256B | 1,008 | 284 |
| 1KB | 2,032 | 1,052 |
| 10KB | 14,321 | 10,268 |
| 100KB | 137,202 | 102,430 |
| 1MB | 1,398,771 | 1,048,606 |

</details>
