1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
"""
The message that says something changed has to be able to say what.
A group's directory table travelled on `index_sync` alone — a *full* index,
which the node only ever sends on request. Every ongoing change went out as an
`index_delta`, which carried files and nothing else. So a root added, removed,
ejected or plugged by the operator reached every other client's screen only
when somebody happened to reload the page.
It was hidden by the acks: `root_add_ack` and friends broadcast the new table
to whoever was connected, so the common cases looked fine. What that could not
cover is a client connecting mid-change, one whose ack was lost, or — the one
that surfaced it — the operator's own client, where the ack landed and was then
overwritten by an index fetched before the node had rebuilt anything.
Additive on the wire (MNP 1.1): a 1.0 client sees a field it does not read.
"""
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshbay_common.crypto import generate_gek
from meshbay_common.groupbox import PURPOSE_INDEX, unseal
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roots import RootSet
from meshbay_node.transport.wire import index_delta_message, index_sync_message
GROUP = "g" * 32
def _roots(tmp_path: Path) -> RootSet:
for name in ("Films", "Albums"):
(tmp_path / name).mkdir()
return RootSet.build([
{"path": str(tmp_path / "Films"), "writable": True},
{"path": str(tmp_path / "Albums"), "removable": True},
])
def _index() -> GroupIndex:
return GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate(),
gek=generate_gek())
def _payload(msg: dict, index: GroupIndex) -> dict:
"""What a member actually reads, through the seal rather than around it."""
return unseal(index.gek, PURPOSE_INDEX, msg["type"], GROUP, msg)
class _Delta:
base_version = 1
version = 2
additions: list = []
deletions: list = []
updates: list = []
def test_a_delta_carries_the_directory_table(tmp_path):
index = _index()
msg = index_delta_message(index, _Delta(), _roots(tmp_path))
payload = _payload(msg, index)
assert [r["name"] for r in payload["roots"]] == ["Films", "Albums"]
assert payload["roots"][0]["writable"] is True
assert payload["roots"][1]["removable"] is True
def test_the_table_is_sealed_with_the_rest(tmp_path):
"""
It is group content, not routing. Only `type`, `v` and `group_id` stay in
clear, because a receiver has to route and authenticate before it would
trust a decryption.
"""
index = _index()
msg = index_delta_message(index, _Delta(), _roots(tmp_path))
assert set(msg) - {"type", "v", "group_id"}, "nothing was sealed"
assert "roots" not in msg, "the directory table is outside the envelope"
def test_a_delta_still_works_without_a_table(tmp_path):
"""
The argument is optional, so an older caller — or a path that has no root
set to hand — produces a message a client reads exactly as before.
"""
index = _index()
payload = _payload(index_delta_message(index, _Delta()), index)
assert "roots" not in payload
assert payload["version"] == 2
def test_the_table_says_the_same_thing_on_both_messages(tmp_path):
"""
Two encodings of one idea is the drift `wire.py` exists to prevent — it
already happened twice, for `index_sync` and for `file_chunk`.
"""
index = _index()
roots = _roots(tmp_path)
delta = _payload(index_delta_message(index, _Delta(), roots), index)
sync = _payload(index_sync_message(index, roots), index)
assert delta["roots"] == sync["roots"]
def test_the_table_never_carries_a_path(tmp_path):
"""
This message goes to every member. Where a directory lives on the
operator's disk is theirs — see test_root_paths_are_operator_only.py.
"""
index = _index()
payload = _payload(index_delta_message(index, _Delta(), _roots(tmp_path)),
index)
assert not any("path" in r for r in payload["roots"])
def test_an_ejected_root_is_visible_in_the_delta(tmp_path):
"""
The case this was written for. An eject changes no file — the entries
freeze — so the delta it produces is empty of additions, deletions and
updates. Without the table it says literally nothing, which is how a
library disappearing from under the group's feet went unannounced.
"""
index = _index()
roots = _roots(tmp_path)
roots.roots[1].ejected = True
roots.roots[1].available = False
payload = _payload(index_delta_message(index, _Delta(), roots), index)
assert payload["additions"] == [] and payload["deletions"] == []
assert payload["roots"][1]["ejected"] is True
assert payload["roots"][1]["available"] is False
|