summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/wire.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-03 15:20:40 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-03 15:20:40 +0200
commitc1be7571973c3d0b671ed4db2da41266ae3099d8 (patch)
treed2b98bf33727c5905669c9c3f40edba30db11ac3 /packages/meshbay-node/src/meshbay_node/transport/wire.py
parent691c6ba4ef51085c89aeddbcabd5c733861eb56b (diff)
downloadmeshbay-c1be7571973c3d0b671ed4db2da41266ae3099d8.tar.gz
refactor!: one file_chunk and index_sync encoder for every transport
`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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/wire.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/wire.py76
1 files changed, 76 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/wire.py b/packages/meshbay-node/src/meshbay_node/transport/wire.py
new file mode 100644
index 0000000..4e09167
--- /dev/null
+++ b/packages/meshbay-node/src/meshbay_node/transport/wire.py
@@ -0,0 +1,76 @@
+"""
+Wire shapes shared by every node transport.
+
+`file_chunk` lives in `meshbay_common.protocol` — it is pure crypto and shape, so a
+client can use the same encoder. This module is for the messages that also need the
+node's own view of its disk, which `meshbay-common` cannot see.
+
+Why it exists at all: `index_sync` was built twice, and the two copies did not agree.
+WebRTC sent `{group_id, version, entries, dirs, roots}` — the shape the shipping
+client reads — while QUIC sent `{index_b64}`, a signed, compressed, GEK-encrypted
+envelope produced by `GroupIndex.serialize()`. Same message type, two encodings, one
+consumer each and nothing asserting they matched. Same failure mode as the two
+`file_chunk` encoders, and the same fix: one builder, used by both.
+
+`GroupIndex.serialize()`/`deserialize()` are unchanged and still tested — they remain
+a correct signed index envelope — but they no longer describe any MNP message. Read
+them as an at-rest/interchange format, not as a wire contract.
+"""
+
+from __future__ import annotations
+
+from meshbay_common import MNP_VERSION
+from meshbay_common.protocol import MNP, index_entry_wire
+
+from meshbay_node.roots import RootSet
+
+# A group with a deep tree can hold more directories than anyone will navigate in one
+# sitting, and the whole list rides on one message.
+MAX_DIRS = 2000
+
+
+def list_dirs(roots: RootSet | None) -> list[str]:
+ """
+ Every directory in the group, as members address them, sorted.
+
+ Each root appears as a directory in its own right, so a root holding no files yet
+ is still somewhere a member can navigate to and upload into. An unavailable root
+ is listed too — its content is frozen, not gone, and hiding it would look exactly
+ like deletion.
+ """
+ if not roots:
+ return []
+ out: list[str] = []
+ for root in roots:
+ out.append(root.name)
+ if not root.available:
+ continue
+ try:
+ for path in sorted(root.path.rglob("*")):
+ if path.is_dir() and not path.name.startswith("."):
+ rel = path.relative_to(root.path)
+ if not any(part.startswith(".") for part in rel.parts):
+ out.append(f"{root.name}/{rel.as_posix()}")
+ except OSError:
+ continue
+ return sorted(out)[:MAX_DIRS]
+
+
+def index_sync_message(index, roots: RootSet | None) -> dict:
+ """
+ The full `index_sync` message for one group.
+
+ `dirs` and `roots` are here because directories are not index entries: without
+ them a folder someone just created, or one they emptied, does not exist as far as
+ a client is concerned, and a member cannot tell "the drive is unplugged" from "it
+ is all still there".
+ """
+ return {
+ "type": MNP.INDEX_SYNC,
+ "v": MNP_VERSION,
+ "group_id": index.group_id,
+ "version": index.version,
+ "entries": [index_entry_wire(e) for e in index.entries],
+ "dirs": list_dirs(roots),
+ "roots": roots.describe() if roots else [],
+ }