From c1be7571973c3d0b671ed4db2da41266ae3099d8 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 3 Sep 2026 15:20:40 +0200 Subject: refactor!: one file_chunk and index_sync encoder for every transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `file_chunk` and `index_sync` were each built twice, once per transport, and the two copies did not agree. WebRTC sent binary, unsigned chunks carrying a `file_id`; QUIC sent base64 fields, two BLAKE3 hashes, a per-chunk Ed25519 signature and no `file_id`. `index_sync` was plain entries on one transport and a `GroupIndex.serialize()` envelope on the other. One message type, two shapes, one consumer each, and nothing that failed when they drifted — finding C6 one size down, in the two places the handshake unification did not reach. Phase 9.15 moved WebRTC to the binary format and dropped the per-chunk signature; the QUIC encoder was never brought along. It is dropped here rather than reintroduced: the AES-GCM tag authenticates the ciphertext under a GEK-derived key, and since C3 the node authenticates itself once in the handshake instead of once per megabyte. `meshbay_common.protocol` now owns the chunk codec (`chunk_ciphertext`, `file_chunk_wire`, `file_chunk_plaintext`) and `meshbay_node/transport/wire.py` the index builder, which also absorbs the delta the daemon used to hand-build. `test_transport_wire_parity.py` fails if either server grows its own copy back. `ChunkRequest`/`ChunkResponse` are deleted. `ChunkResponse` described the QUIC half while reading like the contract for both, which is what made the fork hard to see at all. BREAKING CHANGE: MNP 0.15 changes the encoding of `file_chunk` and `index_sync` on the QUIC transport. The WebRTC shapes are byte for byte unchanged and no QUIC client ships, which is why this is a MINOR bump; a deployed QUIC peer would have made it MAJOR. Also fixes a test fixture that put a `Path` where the daemon puts a `RootSet`. Nothing caught it: the old QUIC index handler never touched `roots`, and `entry_abs_path` fell through `Path.resolve(strict=...)`, reading the virtual path as a truthy flag and returning the right file by accident. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3 --- .../src/meshbay_node/transport/quic_server.py | 49 ++++------------------ 1 file changed, 8 insertions(+), 41 deletions(-) (limited to 'packages/meshbay-node/src/meshbay_node/transport/quic_server.py') diff --git a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py index 30c7daf..8a32538 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/quic_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/quic_server.py @@ -26,7 +26,6 @@ import subprocess from pathlib import Path from typing import Any, Callable -import blake3 import jwt import msgpack from aioquic.asyncio import QuicConnectionProtocol, serve @@ -47,13 +46,10 @@ from meshbay_common.handshake import ( quic_binding, verify_proof, ) -from meshbay_common.crypto import ( - sign_chunk, - pk_to_b64, -) -from meshbay_common.webcrypto import chunk_key_aes as derive_chunk_key, encrypt_chunk_aes as encrypt_chunk -from meshbay_common.protocol import MNP +from meshbay_common.crypto import pk_to_b64 +from meshbay_common.protocol import MNP, file_chunk_wire from meshbay_node.indexer import GroupIndex +from meshbay_node.transport.wire import index_sync_message log = logging.getLogger(__name__) @@ -390,12 +386,7 @@ class _MNPServerProtocol(QuicConnectionProtocol): def _do_index_sync_sync(self, stream_id: int) -> None: ctx = self._group_ctx() - wire = ctx["index"].serialize() - self._send(stream_id, { - "type": MNP.INDEX_SYNC, - "v": MNP_VERSION, - "index_b64": base64.b64encode(wire).decode(), - }) + self._send(stream_id, index_sync_message(ctx["index"], ctx.get("roots"))) def _do_file_request_sync(self, stream_id: int, msg: dict) -> None: """Serve file chunk synchronously (blocking I/O — acceptable for test sizes).""" @@ -414,12 +405,7 @@ class _MNPServerProtocol(QuicConnectionProtocol): file_hash = bytes.fromhex(entry.id) chunk_data = _read_and_encrypt( - self._ctx["sk_node"], - ctx["gek"], - file_path, - chunk_index, - file_hash, - ) + ctx["gek"], file_path, chunk_index, file_hash, entry.id) self._send(stream_id, chunk_data) async def _do_stream_segment(self, stream_id: int, msg: dict) -> None: @@ -520,36 +506,17 @@ class _MNPServerProtocol(QuicConnectionProtocol): def _read_and_encrypt( - sk_node: Ed25519PrivateKey, gek: bytes, file_path: Path, chunk_index: int, file_hash: bytes, + file_id: str = "", ) -> dict: - """Read and encrypt one chunk (blocking — runs in executor).""" + """Read one chunk off disk and encrypt it, in the one shape every transport uses.""" with open(file_path, "rb") as f: f.seek(chunk_index * CHUNK_SIZE) plaintext = f.read(CHUNK_SIZE) - - pt_hash = blake3.blake3(plaintext).digest() - ckey = derive_chunk_key(gek, file_hash, chunk_index) - nonce, ct = encrypt_chunk(ckey, plaintext) - ct_hash = blake3.blake3(ct).digest() - sig = sign_chunk(sk_node, chunk_index, nonce, ct_hash) - - return { - "type": MNP.FILE_CHUNK, - "v": MNP_VERSION, - "chunk_index": chunk_index, - "plaintext_size": len(plaintext), - "nonce_b64": base64.b64encode(nonce).decode(), - "ct_b64": base64.b64encode(ct).decode(), - "ct_hash_b64": base64.b64encode(ct_hash).decode(), - "pt_hash_b64": base64.b64encode(pt_hash).decode(), - "sig_b64": base64.b64encode(sig).decode(), - "pk_node_b64": pk_to_b64(sk_node.public_key()), - "file_hash_b64": base64.b64encode(file_hash).decode(), - } + return file_chunk_wire(gek, plaintext, chunk_index, file_hash, file_id) def _extract_segment(file_path: Path, start_time: float, duration: float) -> bytes | None: -- cgit v1.2.3