Remove Noise protocol references from wiki docs and tests

Delete 8 Noise-specific documentation pages (noise-xx.md,
transport-keys.md, adr-001/003/006, framing-codec.md) and update
~30 remaining wiki pages to reflect QUIC+TLS as the sole transport.
Remove obsolete Noise-based integration tests (auth_service.rs,
mls_group.rs). Code-side Noise removal was done in f334ed3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 08:25:23 +01:00
parent f334ed3d43
commit 9fdb37876a
36 changed files with 125 additions and 2201 deletions

View File

@@ -3,9 +3,9 @@
**Schema file:** `schemas/envelope.capnp`
**File ID:** `@0xe4a7f2c8b1d63509`
The Envelope is the legacy top-level wire message used in M1 for all quicnprotochat traffic over the Noise channel. Every frame exchanged between peers was serialised as an Envelope, with the Delivery Service routing by `(groupId, msgType)` without inspecting the payload.
The Envelope is the legacy top-level wire message used in M1 for all quicnprotochat traffic. Every frame exchanged between peers was serialised as an Envelope, with the Delivery Service routing by `(groupId, msgType)` without inspecting the payload.
> **Note:** The Envelope is the M1-era framing format. The current M3+ architecture uses Cap'n Proto RPC directly via the [NodeService](node-service-schema.md) interface. The Envelope schema remains in the codebase for backward compatibility and for use in integration tests that exercise the Noise transport path.
> **Note:** The Envelope is the M1-era framing format. The current M3+ architecture uses Cap'n Proto RPC directly via the [NodeService](node-service-schema.md) interface. The Envelope schema remains in the codebase for backward compatibility and for use in integration tests.
---
@@ -14,13 +14,12 @@ The Envelope is the legacy top-level wire message used in M1 for all quicnprotoc
```capnp
# envelope.capnp -- top-level wire message for all quicnprotochat traffic.
#
# Every frame exchanged over the Noise channel is serialised as an Envelope.
# Every frame is serialised as an Envelope.
# The Delivery Service routes by (groupId, msgType) without inspecting payload.
#
# Field sizing rationale:
# groupId / senderId : 32 bytes -- SHA-256 digest
# payload : opaque -- MLS blob or control data; size bounded by
# the Noise transport max message size (65535 B)
# payload : opaque -- MLS blob or control data
# timestampMs : UInt64 -- unix epoch milliseconds; sufficient until year 292M
#
# ID generated with: capnp id
@@ -82,7 +81,7 @@ A 32-byte `Data` field containing the SHA-256 digest of the sender's Ed25519 ide
### `payload @3 :Data`
An opaque byte string whose interpretation depends on `msgType`. The payload is bounded by the Noise transport maximum message size of 65,535 bytes (see [Framing Codec](framing-codec.md)).
An opaque byte string whose interpretation depends on `msgType`.
### `timestampMs @4 :UInt64`
@@ -110,7 +109,7 @@ The `MsgType` enum defines nine message types. Each variant determines how the `
### Control messages (0-1)
`ping` and `pong` are keepalive probes with empty payloads. They serve as health checks over long-lived Noise connections.
`ping` and `pong` are keepalive probes with empty payloads. They serve as health checks over long-lived connections.
### Authentication messages (2-4)
@@ -128,7 +127,7 @@ The `MsgType` enum defines nine message types. Each variant determines how the `
## Relationship to NodeService
The Envelope schema was the original M1 wire format, where all communication was multiplexed over a single Noise-encrypted TCP stream. With the transition to QUIC + TLS 1.3 and Cap'n Proto RPC in M3, the Envelope's role has been superseded by the [NodeService interface](node-service-schema.md), which provides typed RPC methods for each operation.
The Envelope schema was the original M1 wire format. With the transition to QUIC + TLS 1.3 and Cap'n Proto RPC in M3, the Envelope's role has been superseded by the [NodeService interface](node-service-schema.md), which provides typed RPC methods for each operation.
The key differences:
@@ -136,8 +135,8 @@ The key differences:
|---|---|---|
| Dispatch | Manual, based on `msgType` enum | Automatic, Cap'n Proto RPC method dispatch |
| Type safety | Payload is opaque `Data` | Each method has typed parameters and return values |
| Transport | Noise\_XX over TCP | QUIC + TLS 1.3 |
| Auth | Implicit (Noise handshake authenticates peers) | Explicit `Auth` struct per method call |
| Transport | QUIC + TLS 1.3 | QUIC + TLS 1.3 |
| Auth | None | Explicit `Auth` struct per method call |
---
@@ -147,5 +146,4 @@ The key differences:
- [NodeService Schema](node-service-schema.md) -- the current RPC interface that replaced Envelope-based dispatch
- [Auth Schema](auth-schema.md) -- standalone Authentication Service interface
- [Delivery Schema](delivery-schema.md) -- standalone Delivery Service interface
- [Framing Codec](framing-codec.md) -- length-prefixed framing that wraps serialised Envelopes
- [ADR-002: Cap'n Proto over MessagePack](../design-rationale/adr-002-capnproto.md) -- why Cap'n Proto was chosen for the wire format

View File

@@ -1,221 +0,0 @@
# Length-Prefixed Framing Codec
**Source file:** `crates/quicnprotochat-core/src/codec.rs`
The `LengthPrefixedCodec` is a stateless Tokio codec that frames byte payloads with a 4-byte little-endian length prefix. It is the bridge between Cap'n Proto serialisation (which produces a byte buffer of variable length) and the Noise transport (which needs discrete message boundaries over a TCP byte stream).
---
## Wire format
```text
+----------------------------+--------------------------------------+
| length (4 bytes, LE u32) | payload (length bytes) |
+----------------------------+--------------------------------------+
```
Each frame consists of:
1. A **4-byte length field** encoded as a little-endian unsigned 32-bit integer (`u32`). This gives a theoretical maximum payload size of 4,294,967,295 bytes, but the actual limit is much lower (see below).
2. A **payload** of exactly `length` bytes. The codec treats the payload as opaque -- it does not inspect or interpret the bytes.
### Byte order: little-endian
The length prefix uses **little-endian** byte order. This was a deliberate choice for consistency with Cap'n Proto's segment table encoding, which also uses little-endian 32-bit integers. Benefits of this choice:
- **No endianness confusion.** A developer inspecting a raw byte dump sees uniform little-endian encoding throughout the entire frame (length header + Cap'n Proto header + Cap'n Proto data).
- **Native performance on common architectures.** x86-64 and AArch64 (in its default little-endian mode) can read the length field without byte-swapping.
- **Alignment with Cap'n Proto conventions.** Cap'n Proto defines its canonical byte order as little-endian (segment count and segment sizes are LE u32).
### Example encoding
For the ASCII payload `"le-check"` (8 bytes), the encoded frame is:
```text
Offset Hex Meaning
------ ------------------ -------
0x00 08 00 00 00 Length = 8 (little-endian)
0x04 6C 65 2D 63 68 65 Payload: "le-che"
0x0A 63 6B Payload: "ck"
```
Total frame size: 4 (header) + 8 (payload) = 12 bytes.
---
## Frame size limit
```rust
/// Maximum Noise protocol message size in bytes (per RFC / Noise spec S3).
pub const NOISE_MAX_MSG: usize = 65_535;
```
The maximum payload size is **65,535 bytes** (64 KiB - 1), matching the Noise protocol specification's maximum message size. This constant is defined as `NOISE_MAX_MSG` in the codec module.
Any frame with a payload exceeding this limit is rejected as a protocol violation:
- **On encode:** `Encoder::encode()` returns `CodecError::FrameTooLarge` before writing any bytes to the buffer.
- **On decode:** `Decoder::decode()` returns `CodecError::FrameTooLarge` upon reading a length field that exceeds the limit, without attempting to read the payload bytes.
In both cases, the error is **unrecoverable**. The connection should be closed rather than retried, because an oversized frame indicates either a bug or a malicious peer.
### Relationship to Noise plaintext limit
The `NOISE_MAX_MSG` constant (65,535 bytes) represents the maximum Noise *message* size, which includes the Poly1305 authentication tag (16 bytes). The maximum *plaintext* per Noise transport frame is therefore:
```rust
/// Maximum plaintext bytes per Noise transport frame.
pub const MAX_PLAINTEXT_LEN: usize = 65_519; // 65,535 - 16
```
This constant is defined in `crates/quicnprotochat-core/src/error.rs`. The codec operates at the ciphertext level (framing Noise messages, not plaintext), so it uses `NOISE_MAX_MSG` as its limit.
---
## Implementation
The codec implements Tokio's `Encoder<Bytes>` and `Decoder` traits, making it compatible with `tokio_util::codec::Framed` for use with any `AsyncRead + AsyncWrite` stream.
### Struct
```rust
#[derive(Debug, Clone, Copy, Default)]
pub struct LengthPrefixedCodec;
```
The codec is **stateless** -- it holds no internal buffering state. This means it is `Clone`, `Copy`, and `Default`, and multiple codec instances are interchangeable.
### Encoder
```rust
impl Encoder<Bytes> for LengthPrefixedCodec {
type Error = CodecError;
fn encode(&mut self, item: Bytes, dst: &mut BytesMut) -> Result<(), Self::Error> {
let len = item.len();
if len > NOISE_MAX_MSG {
return Err(CodecError::FrameTooLarge {
len,
max: NOISE_MAX_MSG,
});
}
dst.reserve(4 + len);
dst.put_u32_le(len as u32);
dst.extend_from_slice(&item);
Ok(())
}
}
```
**Steps:**
1. Check payload size against `NOISE_MAX_MSG`. Reject if oversized.
2. Reserve exactly `4 + len` bytes in the output buffer to avoid reallocation.
3. Write the 4-byte little-endian length prefix.
4. Copy the payload bytes.
### Decoder
```rust
impl Decoder for LengthPrefixedCodec {
type Item = BytesMut;
type Error = CodecError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if src.len() < 4 {
src.reserve(4_usize.saturating_sub(src.len()));
return Ok(None);
}
let frame_len = u32::from_le_bytes([src[0], src[1], src[2], src[3]]) as usize;
if frame_len > NOISE_MAX_MSG {
return Err(CodecError::FrameTooLarge {
len: frame_len,
max: NOISE_MAX_MSG,
});
}
let total = 4 + frame_len;
if src.len() < total {
src.reserve(total - src.len());
return Ok(None);
}
src.advance(4);
Ok(Some(src.split_to(frame_len)))
}
}
```
**Steps:**
1. **Check for header completeness.** If fewer than 4 bytes are available, reserve the remaining bytes and return `Ok(None)` (the standard Tokio Decoder contract for "need more data").
2. **Peek at the length field** without advancing the cursor. This avoids mutating buffer state when the full frame is not yet available.
3. **Validate the length.** If it exceeds `NOISE_MAX_MSG`, return an error immediately.
4. **Check for payload completeness.** If fewer than `4 + frame_len` bytes are available, reserve the difference and return `Ok(None)`.
5. **Consume the frame.** Advance past the 4-byte header, then split the payload from the front of the buffer.
The `reserve()` calls in steps 1 and 4 are a performance optimization: they hint to Tokio how many additional bytes the decoder needs, avoiding O(n) polling behavior where the decoder is called once per incoming byte.
---
## Error handling
```rust
#[derive(Debug, Error)]
pub enum CodecError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("frame length {len} exceeds maximum {max} bytes")]
FrameTooLarge { len: usize, max: usize },
}
```
The codec produces two error variants:
| Variant | Cause | Recovery |
|---|---|---|
| `Io` | The underlying TCP stream returned an I/O error. Auto-converted from `std::io::Error` via the `From` impl required by `tokio-util`. | Depends on the I/O error. Typically the connection is broken and should be dropped. |
| `FrameTooLarge` | A frame's length field exceeds `NOISE_MAX_MSG` (65,535 bytes). | **Unrecoverable.** The connection should be closed. An oversized frame indicates a protocol violation -- either a bug or a malicious peer. |
---
## Transport context
The `LengthPrefixedCodec` is used in the **Noise transport path** (M1 stack), where Cap'n Proto messages and Noise handshake messages are sent over a raw TCP stream that has no built-in message boundaries.
In the **QUIC transport path** (M3+ stack), the codec is **not used**. QUIC provides native stream framing through its stream abstraction, and the `capnp-rpc` crate handles message delimitation internally. The QUIC path also does not need the 65,535-byte frame limit because QUIC flow control operates at a different level.
```text
Noise path: App -> Cap'n Proto -> LengthPrefixedCodec -> Noise encrypt -> TCP
QUIC path: App -> Cap'n Proto RPC -> capnp-rpc stream adapter -> QUIC stream -> UDP
```
---
## Test coverage
The codec module includes comprehensive tests that verify:
| Test | What it validates |
|---|---|
| `round_trip_empty_payload` | Empty payloads encode and decode correctly (0-length frame) |
| `round_trip_small_payload` | Small payloads survive a round trip without corruption |
| `round_trip_max_size_payload` | A payload of exactly `NOISE_MAX_MSG` bytes (the maximum) encodes and decodes correctly |
| `oversized_encode_returns_error` | Encoding a payload of `NOISE_MAX_MSG + 1` bytes returns `FrameTooLarge` |
| `oversized_length_field_decode_returns_error` | Decoding a frame with a length field exceeding `NOISE_MAX_MSG` returns `FrameTooLarge` |
| `partial_payload_returns_none` | A frame with a valid header but incomplete payload returns `None` (need more data) |
| `partial_header_returns_none` | A buffer with fewer than 4 bytes returns `None` (need more data) |
| `length_field_is_little_endian` | The encoded length of `"le-check"` (8 bytes) produces `[0x08, 0x00, 0x00, 0x00]` |
---
## Further reading
- [Wire Format Overview](overview.md) -- where the codec fits in the serialisation pipeline
- [Envelope Schema](envelope-schema.md) -- the Cap'n Proto messages that the codec frames (M1 path)
- [NodeService Schema](node-service-schema.md) -- the RPC messages carried over QUIC (M3+ path, does not use this codec)
- [ADR-003: RPC Inside the Noise Tunnel](../design-rationale/adr-003-rpc-inside-noise.md) -- why the codec sits between Cap'n Proto and Noise
- [Protocol Layers Overview](../protocol-layers/overview.md) -- how all the layers stack

View File

@@ -252,7 +252,6 @@ Cap'n Proto supports forward-compatible schema evolution through several mechani
- [Auth Schema](auth-schema.md) -- standalone Authentication Service interface (subset of NodeService)
- [Delivery Schema](delivery-schema.md) -- standalone Delivery Service interface (subset of NodeService)
- [Envelope Schema](envelope-schema.md) -- legacy M1 framing that NodeService replaced
- [Framing Codec](framing-codec.md) -- length-prefixed framing used in the Noise transport path
- [Architecture Overview](../architecture/overview.md) -- system-level view showing NodeService in context
- [ADR-005: Single-Use KeyPackages](../design-rationale/adr-005-single-use-keypackages.md) -- why fetchKeyPackage is destructive
- [ADR-004: MLS-Unaware DS](../design-rationale/adr-004-mls-unaware-ds.md) -- why payloads are opaque

View File

@@ -6,22 +6,22 @@ This section documents the serialisation pipeline that transforms application-le
## Serialisation pipeline
Data flows through four stages on the send path. The receive path reverses the order.
Data flows through three stages on the send path. The receive path reverses the order.
```text
Stage 1 Stage 2 Stage 3 Stage 4
-------- -------- -------- --------
Application Cap'n Proto Length-prefixed Transport
data serialisation framing encryption
Stage 1 Stage 2 Stage 3
-------- -------- --------
Application Cap'n Proto Transport
data serialisation encryption
ParsedEnvelope capnp::serialize [u32 LE len][payload] Noise ChaCha20-Poly1305
or RPC call (zero-copy bytes) or QUIC/TLS 1.3
RPC call capnp::serialize QUIC/TLS 1.3
(zero-copy bytes)
| | | |
v v v v
Rust structs Canonical byte Framed byte stream Encrypted
& method representation ready for transport ciphertext
invocations (no deserialization on the wire
| | |
v v v
Rust structs Canonical byte Encrypted
& method representation ciphertext
invocations (no deserialization on the wire
needed on receive)
```
@@ -45,42 +45,15 @@ The wire representation consists of:
Cap'n Proto's canonical form is deterministic for a given message, which makes it suitable for signing: two implementations that build the same logical message will produce identical bytes.
### Stage 3: Length-prefixed framing
### Stage 3: Transport encryption
Before the serialised bytes enter the transport, they are wrapped in a length-prefixed frame:
The serialised bytes are encrypted by the QUIC/TLS 1.3 transport layer. The QUIC transport uses native QUIC stream framing, which provides its own length delimitation. Cap'n Proto RPC over QUIC relies on the `capnp-rpc` crate's built-in stream adapter.
```text
+----------------------------+--------------------------------------+
| length (4 bytes, LE u32) | payload (length bytes) |
+----------------------------+--------------------------------------+
```
| Transport | Encryption | Authentication |
|---|---|---|
| **QUIC + TLS 1.3** | AES-128-GCM or ChaCha20-Poly1305 (negotiated by TLS) | Server cert (rustls/quinn) |
The length prefix is encoded as a **little-endian** 32-bit unsigned integer. Little-endian was chosen for consistency with Cap'n Proto's own segment table encoding, which also uses little-endian integers. This avoids byte-order confusion when the same buffer contains both framing headers and Cap'n Proto data.
The maximum payload size is **65,535 bytes**, matching the Noise protocol's maximum message size. Frames exceeding this limit are rejected as protocol violations. See [Framing Codec](framing-codec.md) for the full `LengthPrefixedCodec` implementation.
> **Note:** This framing stage applies only to the Noise transport path. The QUIC transport uses native QUIC stream framing, which provides its own length delimitation. Cap'n Proto RPC over QUIC relies on the `capnp-rpc` crate's built-in stream adapter rather than `LengthPrefixedCodec`.
### Stage 4: Transport encryption
The framed byte stream is encrypted by the transport layer:
| Transport | Encryption | Authentication | When Used |
|---|---|---|---|
| **Noise\_XX over TCP** | ChaCha20-Poly1305 (per-session key from XX handshake) | Mutual, via static X25519 keys | M1 stack, peer-to-peer, integration tests |
| **QUIC + TLS 1.3** | AES-128-GCM or ChaCha20-Poly1305 (negotiated by TLS) | Server cert (rustls/quinn) | M3+ primary transport |
In both cases, the transport layer treats the payload as opaque bytes. It does not inspect or interpret the Cap'n Proto content. This clean separation means the serialisation format can evolve independently of the transport.
---
## Little-endian framing rationale
Cap'n Proto uses little-endian encoding for its segment table (the header that precedes each serialised message). The `LengthPrefixedCodec` uses the same byte order for its 4-byte length field. This consistency means:
1. A developer inspecting a raw byte dump sees uniform endianness throughout.
2. On little-endian architectures (x86-64, AArch64 in LE mode), both the framing header and the Cap'n Proto header can be read without byte-swapping.
3. There is no risk of accidentally mixing big-endian and little-endian headers in the same stream.
The transport layer treats the payload as opaque bytes. It does not inspect or interpret the Cap'n Proto content. This clean separation means the serialisation format can evolve independently of the transport.
---
@@ -95,8 +68,6 @@ The Cap'n Proto schemas that define the wire-level messages are documented on de
| `schemas/delivery.capnp` | [Delivery Schema](delivery-schema.md) | Delivery Service RPC interface |
| `schemas/node.capnp` | [NodeService Schema](node-service-schema.md) | Unified node RPC (current) |
The length-prefixed framing codec that wraps serialised messages is documented at [Framing Codec](framing-codec.md).
---
## Further reading
@@ -104,4 +75,3 @@ The length-prefixed framing codec that wraps serialised messages is documented a
- [Architecture Overview](../architecture/overview.md) -- system-level view of how services compose
- [Protocol Layers Overview](../protocol-layers/overview.md) -- how transport, framing, and E2E encryption stack
- [ADR-002: Cap'n Proto over MessagePack](../design-rationale/adr-002-capnproto.md) -- why Cap'n Proto was chosen
- [ADR-003: RPC Inside the Noise Tunnel](../design-rationale/adr-003-rpc-inside-noise.md) -- why RPC runs inside the encrypted channel