Skip to content
Open

QLV2 #61

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
0385adf
ql docs: add QLV2 overview and protocol reference
nicoburniske Jun 4, 2026
71702fb
ql-wire: add wire-format definitions
nicoburniske Jun 4, 2026
1a09d0a
ql-fsm: add synchronous protocol state machine
nicoburniske Jun 4, 2026
ba41308
ql-rpc: add RPC modality layer
nicoburniske Jun 4, 2026
154a4d9
ql-runtime: add async runtime
nicoburniske Jun 4, 2026
6c0bb2f
workspace: remove legacy QLV1 crates
nicoburniske Jun 4, 2026
6f95798
ql: open stream service id
nicoburniske Jun 22, 2026
00972ae
fsm: store handshake id
nicoburniske Jun 22, 2026
4b20c72
wire: helper macros + cleanup
nicoburniske Jun 22, 2026
b4284cb
fsm: cleanup
nicoburniske Jun 23, 2026
c388687
wire: smaller ConnectionId
nicoburniske Jun 23, 2026
d3f3a2c
ql-rpc: scope routes by service id
nicoburniske Jun 23, 2026
2b06595
ql-common: extract shared primitives
nicoburniske Jun 23, 2026
b046cec
ql: stream close code origin
nicoburniske Jun 23, 2026
f599eae
ql: simpler error types
nicoburniske Jun 23, 2026
6ea9651
ql: better stream close codes
nicoburniske Jun 23, 2026
de6e69e
ql-rpc: drop wrappers
nicoburniske Jun 23, 2026
7e6bff7
ql: remove read max len
nicoburniske Jun 23, 2026
c35bafb
ql: StreamClose -> StreamReset
nicoburniske Jun 23, 2026
6820b91
ql-rpc: separate poll_finish from finish
nicoburniske Jun 23, 2026
80d834e
ql-rpc: stream handler context
nicoburniske Jun 24, 2026
d98a919
ql-runtime: better handle
nicoburniske Jun 24, 2026
c614335
ql-runtime: remove rpc wrappers
nicoburniske Jun 24, 2026
3ff9477
ql-fsm: simplify stream reset
nicoburniske Jun 24, 2026
e4e1580
ql-rpc: cleanup
nicoburniske Jun 24, 2026
f10e8f8
ql-rpc: separate streaminfo
nicoburniske Jun 25, 2026
6e742de
ql: refer to ql-common directly
nicoburniske Jun 25, 2026
032f92d
ql-rpc: more internal cleanup
nicoburniske Jun 25, 2026
a6f6c64
ql-fsm: remove unused error variant
nicoburniske Jun 25, 2026
73cc6ea
ql-wire: always have QID in the header
nicoburniske Jun 25, 2026
4e71eea
ql-wire: remove connectionid
nicoburniske Jun 25, 2026
0877b2b
ql-wire: LenBytes helper
nicoburniske Jun 29, 2026
985b0f4
ql-wire: remove name validation
nicoburniske Jun 29, 2026
8199cad
ql: arbitrary stream header
nicoburniske Jul 1, 2026
65ae649
ql-rpc: generic key
nicoburniske Jul 1, 2026
c6a7dad
ql-wire: bundle metadata bytes
nicoburniske Jul 6, 2026
59f66d7
ql-common: remove service-id
nicoburniske Jul 6, 2026
9bc0745
ql-common: varint conversions
nicoburniske Jul 6, 2026
20ff66a
ql-rpc: simplify
nicoburniske Jul 8, 2026
c3d79ba
ql-codec: impl
nicoburniske Jul 9, 2026
6505b91
ql-wire: port to codec
nicoburniske Jul 9, 2026
9ecb1f5
ql-fsm: update
nicoburniske Jul 9, 2026
d91e02a
ql-runtime: update
nicoburniske Jul 9, 2026
28e93ce
ql: merge ik and kk handshakes
nicoburniske Jul 10, 2026
4ee0787
ql: remove bundle metadata and name wrapper
nicoburniske Jul 10, 2026
e09d7e7
ql-wire: allow borrowed identity
nicoburniske Jul 21, 2026
d74b431
ql-wire: peer challenge
nicoburniske Jul 21, 2026
c35c115
fsm: retransmit time inference
nicoburniske Jul 21, 2026
14123dc
ql-runtime: separate crypto getter
nicoburniske Jul 21, 2026
a0a0408
ql-codec: make ByteSlice safe
badicsalex Jul 22, 2026
c4c01fa
ql: wrap varint fields in a Varint type
badicsalex Jul 22, 2026
7301b7e
ql: standardize codec impls
badicsalex Jul 22, 2026
27bdf54
ql: generate codec impls with macros
badicsalex Jul 22, 2026
edaf616
ql: consolidate the wire size constants
badicsalex Jul 22, 2026
0219878
ql: length-prefix byte fields as u32
badicsalex Jul 22, 2026
d3b35f4
ql-fsm: budget the stream header before polling
badicsalex Jul 22, 2026
c079ee8
ql-codec: single codec! macro
nicoburniske Jul 22, 2026
85853ea
ql-codec: useless tests
nicoburniske Jul 22, 2026
84c9a40
Merge pull request #74 from Foundation-Devices/ql-v2-alex-changes
nicoburniske Jul 22, 2026
89d09ee
ql-fsm: restore pairing invite codec
nicoburniske Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,206 changes: 606 additions & 1,600 deletions Cargo.lock

Large diffs are not rendered by default.

32 changes: 16 additions & 16 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
[workspace]
resolver = "2"
members = ["api", "backup-shard", "btp", "quantum-link-macros"]
members = [
"backup-shard",
"btp",
"ql-codec",
"ql-common",
"ql-fsm",
"ql-rpc",
"ql-runtime",
"ql-wire",
]

[workspace.package]
homepage = "https://github.com/Foundation-Devices/foundation-api"

[workspace.dependencies]
# blockchain commons
bc-components = { version = "0.28.0" }
bc-envelope = { version = "0.37.0" }
bc-xid = { version = "0.16.0" }
dcbor = { version = "0.23.3" }
gstp = { version = "0.11.0" }

chrono = "0.4"
bytes = "1"
getrandom = { version = "0.2" }
insta = { version = "1.43.2" }
thiserror = { version = "2" }
rkyv = { version = "0.8" }

# workspace crates
backup-shard = { path = "backup-shard" }
btp = { path = "btp" }
foundation-api = { path = "api" }
quantum-link-macros = { path = "quantum-link-macros" }

[patch.crates-io]
pqcrypto-traits = { git = "https://github.com/Foundation-Devices/pqcrypto", rev = "ebadf71214f67cb970242fa1053b4acb65767737" }
pqcrypto-mldsa = { git = "https://github.com/Foundation-Devices/pqcrypto", rev = "ebadf71214f67cb970242fa1053b4acb65767737" }
pqcrypto-mlkem = { git = "https://github.com/Foundation-Devices/pqcrypto", rev = "ebadf71214f67cb970242fa1053b4acb65767737" }
ql-codec = { path = "ql-codec" }
ql-common = { path = "ql-common" }
ql-fsm = { path = "ql-fsm" }
ql-rpc = { path = "ql-rpc" }
ql-wire = { path = "ql-wire" }
12 changes: 0 additions & 12 deletions Justfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
# Run clippy on all targets and features, treating warnings as errors
clippy:
cargo clippy --all-targets --all-features -- -D warnings

# Run golden/snapshot tests
golden:
cargo test -p foundation-api --test golden_tests

# Update golden/snapshot tests (accept all new snapshots)
golden-update:
INSTA_UPDATE=always cargo test -p foundation-api --test golden_tests

# Review pending golden/snapshot changes interactively (requires cargo-insta)
golden-review:
cargo insta review
67 changes: 67 additions & 0 deletions QLV2_overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# QuantumLink V2

QLV2 is designed around the shortcomings of QLV1.

QLV1 worked, but it treated each message too much like a standalone encrypted blob. That made routing hard, pairing clunky, repeated messages expensive, reliability awkward, and application encoding too baked into the protocol.

QLV2 moves QuantumLink toward authenticated encrypted sessions with reliable duplex byte streams.

## Problems And Solutions

### The protocol assumed specific product roles

**Problem:** QLV1 was effectively shaped around the Prime/Envoy relationship. It was not a generic protocol for arbitrary peers; the wire model and pairing flow assumed who the participants were and what direction the relationship moved in.

**Solution:** QLV2 models peers generically. A peer has a `PeerBundle` representing its public identity, including its QID, independent of whether it is Passport, Envoy, KeyOS, a mobile app, or something else.

### Messages were not routable

**Problem:** QLV1 messages were not cheaply introspectable. A peer or transport adapter could not look at a message and know where it was supposed to go.

**Solution:** QLV2 records have public, but verified, routing headers. Known-peer records expose sender and recipient QIDs, and session records expose enough metadata to route them to the right session.

This lets a single connection, like BLE, multiplex different senders and recipients on both ends. For example, iPhone/Android can have multiple apps using the same BLE connection, while KeyOS can still know which sender produced a record and which destination app should receive it.

This also opens the door to QL-level packet forwarding, because peers can cheaply inspect a record and forward it to the right destination without needing to understand or decrypt the payload.

### Encryption overhead was too high

**Problem:** The minimum payload size for a QLV1 message is about 6.6KB.

**Solution:** QLV2 amortizes the KEM and encryption setup cost into a session. Handshake messages are still large, but steady-state message overhead drops to roughly `35..42` bytes, depending on varint encoding size.

### Key compromise could expose old messages

**Problem:** QLV1 had no built-in key rotation model. If a long-term key was compromised, old messages were at risk.

**Solution:** QLV2 uses Noise-style session handshakes so every session gets unique encryption keys. Compromising one session does not automatically compromise future sessions.

### Reliability lived above the protocol

**Problem:** QLV1 was fundamentally unreliable, so reliability had to be rebuilt by each higher-level API. Any message flow that needed dependable delivery had to invent its own retry/reliability behavior in userspace.

**Solution:** QLV2 is built around reliable streams. One session can carry many duplex byte streams, and reliability is solved once at the QL layer instead of repeatedly in user/application space.

### Encoding was baked into the protocol

**Problem:** QLV1 was tied to a specific CBOR codec, even when the protocol did not really need that. This added serialization/deserialization cost and extra memory pressure.

**Solution:** QLV2 uses binary framing internally and exposes byte streams to user space. Since QL is fundamentally moving bytes, the implementation can use zero-copy byte views where possible instead of copying payloads through a serialization layer. Applications can still layer whatever encoding they want on top: JSON, CBOR, XML, or something else. That choice belongs above QL.

### RPC patterns had to be reinvented per API

**Problem:** QLV1 mixed protocol transport with application workflow shape. If a feature needed request/response behavior, progress updates, downloads, uploads, or subscriptions, that behavior had to be manually modeled in its message types. The protocol did not provide reusable workflow primitives, so every feature had to encode its own control flow.

**Solution:** QLV2 makes reliable byte streams the primitive. `ql-rpc` sits above QLV2 and gives those streams common RPC modalities: request/response, notification, upload, download, subscription, and duplex. QL stays focused on peer identity, session establishment, encryption, routing, and reliable byte transport.

### Pairing lived in userspace

**Problem:** QLV1 did not treat peer establishment as a protocol concern. Pairing had to be implemented as an application message flow, which made it directional, informal, and hard to generalize beyond the original Prime/Envoy convention.

**Solution:** QLV2 lifts peer establishment into the protocol. Pairing becomes one protocol-supported way to establish a session, not a one-off userspace convention.

QLV2 has different session establishment modalities for different starting states:

- `XX`: first-time pairing using a small out-of-band `PairingToken`
- `IK`: the initiator already knows the responder
- `KK`: both peers already know each other
Loading
Loading