aethermesh 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aethermesh/__init__.py +34 -0
- aethermesh/__version__.py +9 -0
- aethermesh/admission/__init__.py +13 -0
- aethermesh/admission/pipeline.py +282 -0
- aethermesh/bft/__init__.py +31 -0
- aethermesh/bft/invariants.py +93 -0
- aethermesh/bft/provenance.py +202 -0
- aethermesh/bft/semantic_matrix.py +154 -0
- aethermesh/cli.py +270 -0
- aethermesh/crdt/__init__.py +17 -0
- aethermesh/crdt/delta_state.py +195 -0
- aethermesh/crdt/semilattice.py +212 -0
- aethermesh/gossip/__init__.py +17 -0
- aethermesh/gossip/peer_sampling.py +178 -0
- aethermesh/gossip/protocol.py +289 -0
- aethermesh/models.py +395 -0
- aethermesh/py.typed +0 -0
- aethermesh/simulation/__init__.py +25 -0
- aethermesh/simulation/network.py +240 -0
- aethermesh/simulation/node.py +242 -0
- aethermesh/simulation/testbed.py +311 -0
- aethermesh-2.0.0.dist-info/METADATA +839 -0
- aethermesh-2.0.0.dist-info/RECORD +28 -0
- aethermesh-2.0.0.dist-info/WHEEL +5 -0
- aethermesh-2.0.0.dist-info/entry_points.txt +2 -0
- aethermesh-2.0.0.dist-info/licenses/AUTHORS.md +60 -0
- aethermesh-2.0.0.dist-info/licenses/LICENSE +26 -0
- aethermesh-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherMesh — Semantic compatibility matrix (Pillar 3, Section 3.3.3).
|
|
3
|
+
|
|
4
|
+
A compatibility matrix C assigns to each pair of operation kinds a
|
|
5
|
+
verdict on whether concurrent deltas of those kinds that touch
|
|
6
|
+
overlapping targets may coexist.
|
|
7
|
+
|
|
8
|
+
The matrix is evaluated locally and cheaply, which is what makes the
|
|
9
|
+
admission layer lightweight. It is also coarse: it classifies operation
|
|
10
|
+
kinds, not program behaviour, and it cannot detect semantic breakage
|
|
11
|
+
that depends on program meaning.
|
|
12
|
+
|
|
13
|
+
This module provides:
|
|
14
|
+
|
|
15
|
+
- SemanticVerdict: enum (ok, resolve, conflict)
|
|
16
|
+
- CompatibilityMatrix: the matrix itself
|
|
17
|
+
- DEFAULT_MATRIX: the illustrative matrix from Table 3.1
|
|
18
|
+
|
|
19
|
+
Reference:
|
|
20
|
+
Baladi, S. (2026). AetherMesh v1.0.0. Section 3.3.3 and Table 3.1.
|
|
21
|
+
Zenodo. https://doi.org/10.5281/zenodo.24038038
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Dict, FrozenSet, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
from aethermesh.models import DeltaKind
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# =============================================================================
|
|
34
|
+
# SemanticVerdict
|
|
35
|
+
# =============================================================================
|
|
36
|
+
|
|
37
|
+
class SemanticVerdict(str, Enum):
|
|
38
|
+
"""The three possible verdicts of the semantic compatibility matrix."""
|
|
39
|
+
OK = "ok"
|
|
40
|
+
RESOLVE = "resolve" # deterministic resolution required
|
|
41
|
+
CONFLICT = "conflict" # reject or quarantine
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# =============================================================================
|
|
45
|
+
# CompatibilityMatrix
|
|
46
|
+
# =============================================================================
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class CompatibilityMatrix:
|
|
50
|
+
"""
|
|
51
|
+
A compatibility matrix over operation kinds.
|
|
52
|
+
|
|
53
|
+
The matrix is symmetric: C[a][b] == C[b][a] for all a, b.
|
|
54
|
+
|
|
55
|
+
Reference: Table 3.1 of the paper.
|
|
56
|
+
"""
|
|
57
|
+
entries: Dict[Tuple[DeltaKind, DeltaKind], SemanticVerdict] = field(
|
|
58
|
+
default_factory=dict
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def set(
|
|
62
|
+
self,
|
|
63
|
+
a: DeltaKind,
|
|
64
|
+
b: DeltaKind,
|
|
65
|
+
verdict: SemanticVerdict,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Set a matrix entry (both directions)."""
|
|
68
|
+
self.entries[(a, b)] = verdict
|
|
69
|
+
self.entries[(b, a)] = verdict
|
|
70
|
+
|
|
71
|
+
def get(
|
|
72
|
+
self,
|
|
73
|
+
a: DeltaKind,
|
|
74
|
+
b: DeltaKind,
|
|
75
|
+
) -> SemanticVerdict:
|
|
76
|
+
"""
|
|
77
|
+
Get the verdict for a pair of kinds. Defaults to OK if unset.
|
|
78
|
+
"""
|
|
79
|
+
return self.entries.get((a, b), SemanticVerdict.OK)
|
|
80
|
+
|
|
81
|
+
def check(self, deltas: list) -> Tuple[SemanticVerdict, str]:
|
|
82
|
+
"""
|
|
83
|
+
Check a set of deltas for semantic compatibility.
|
|
84
|
+
|
|
85
|
+
Returns the worst verdict among all pairs:
|
|
86
|
+
- If any pair is CONFLICT, returns CONFLICT.
|
|
87
|
+
- Else if any pair is RESOLVE, returns RESOLVE.
|
|
88
|
+
- Else returns OK.
|
|
89
|
+
|
|
90
|
+
The reason string lists the conflicting pair (if any).
|
|
91
|
+
"""
|
|
92
|
+
worst = SemanticVerdict.OK
|
|
93
|
+
worst_pair: Optional[Tuple[str, str]] = None
|
|
94
|
+
|
|
95
|
+
for i, d_i in enumerate(deltas):
|
|
96
|
+
for d_j in deltas[i + 1:]:
|
|
97
|
+
v = self.get(d_i.kind, d_j.kind)
|
|
98
|
+
if v == SemanticVerdict.CONFLICT:
|
|
99
|
+
if worst != SemanticVerdict.CONFLICT:
|
|
100
|
+
worst = SemanticVerdict.CONFLICT
|
|
101
|
+
worst_pair = (d_i.kind.value, d_j.kind.value)
|
|
102
|
+
elif v == SemanticVerdict.RESOLVE:
|
|
103
|
+
if worst == SemanticVerdict.OK:
|
|
104
|
+
worst = SemanticVerdict.RESOLVE
|
|
105
|
+
worst_pair = (d_i.kind.value, d_j.kind.value)
|
|
106
|
+
|
|
107
|
+
if worst == SemanticVerdict.OK:
|
|
108
|
+
return worst, "ok"
|
|
109
|
+
if worst_pair:
|
|
110
|
+
return worst, f"{worst.value}: {worst_pair[0]} vs {worst_pair[1]}"
|
|
111
|
+
return worst, worst.value
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# =============================================================================
|
|
115
|
+
# Default matrix (Table 3.1 of the paper)
|
|
116
|
+
# =============================================================================
|
|
117
|
+
|
|
118
|
+
def _build_default_matrix() -> CompatibilityMatrix:
|
|
119
|
+
"""Build the illustrative matrix from Table 3.1 of the paper."""
|
|
120
|
+
m = CompatibilityMatrix()
|
|
121
|
+
OK = SemanticVerdict.OK
|
|
122
|
+
RESOLVE = SemanticVerdict.RESOLVE
|
|
123
|
+
CONFLICT = SemanticVerdict.CONFLICT
|
|
124
|
+
|
|
125
|
+
# Edit body
|
|
126
|
+
m.set(DeltaKind.EDIT_BODY, DeltaKind.EDIT_BODY, OK)
|
|
127
|
+
m.set(DeltaKind.EDIT_BODY, DeltaKind.RENAME_SYMBOL, OK)
|
|
128
|
+
m.set(DeltaKind.EDIT_BODY, DeltaKind.DELETE_SYMBOL, CONFLICT)
|
|
129
|
+
m.set(DeltaKind.EDIT_BODY, DeltaKind.CHANGE_SIGNATURE, OK)
|
|
130
|
+
m.set(DeltaKind.EDIT_BODY, DeltaKind.EDIT_METADATA, OK)
|
|
131
|
+
|
|
132
|
+
# Rename symbol
|
|
133
|
+
m.set(DeltaKind.RENAME_SYMBOL, DeltaKind.RENAME_SYMBOL, RESOLVE)
|
|
134
|
+
m.set(DeltaKind.RENAME_SYMBOL, DeltaKind.DELETE_SYMBOL, CONFLICT)
|
|
135
|
+
m.set(DeltaKind.RENAME_SYMBOL, DeltaKind.CHANGE_SIGNATURE, CONFLICT)
|
|
136
|
+
m.set(DeltaKind.RENAME_SYMBOL, DeltaKind.EDIT_METADATA, OK)
|
|
137
|
+
|
|
138
|
+
# Delete symbol
|
|
139
|
+
m.set(DeltaKind.DELETE_SYMBOL, DeltaKind.DELETE_SYMBOL, OK) # idempotent
|
|
140
|
+
m.set(DeltaKind.DELETE_SYMBOL, DeltaKind.CHANGE_SIGNATURE, CONFLICT)
|
|
141
|
+
m.set(DeltaKind.DELETE_SYMBOL, DeltaKind.EDIT_METADATA, OK)
|
|
142
|
+
|
|
143
|
+
# Change signature
|
|
144
|
+
m.set(DeltaKind.CHANGE_SIGNATURE, DeltaKind.CHANGE_SIGNATURE, RESOLVE)
|
|
145
|
+
m.set(DeltaKind.CHANGE_SIGNATURE, DeltaKind.EDIT_METADATA, OK)
|
|
146
|
+
|
|
147
|
+
# Edit metadata
|
|
148
|
+
m.set(DeltaKind.EDIT_METADATA, DeltaKind.EDIT_METADATA, RESOLVE) # LWW
|
|
149
|
+
|
|
150
|
+
return m
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
DEFAULT_MATRIX: CompatibilityMatrix = _build_default_matrix()
|
|
154
|
+
"""The illustrative compatibility matrix from Table 3.1 of the paper."""
|
aethermesh/cli.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherMesh — Command-line interface.
|
|
3
|
+
|
|
4
|
+
Provides three subcommands:
|
|
5
|
+
|
|
6
|
+
aethermesh simulate Run a simulated P2P testbed and measure the
|
|
7
|
+
three metrics from Section 5 of the paper.
|
|
8
|
+
|
|
9
|
+
aethermesh info Print version, DOI, and record information.
|
|
10
|
+
|
|
11
|
+
aethermesh verify Run a quick self-check (join laws, signature
|
|
12
|
+
verification, admission pipeline) without
|
|
13
|
+
a full simulation.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python -m aethermesh.cli simulate --nodes 50 --fanout 5
|
|
17
|
+
python -m aethermesh.cli info
|
|
18
|
+
python -m aethermesh.cli verify
|
|
19
|
+
|
|
20
|
+
Reference:
|
|
21
|
+
Baladi, S. (2026). AetherMesh v1.0.0. Section 5.
|
|
22
|
+
Zenodo. https://doi.org/10.5281/zenodo.24038038
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
from typing import Dict, List, Optional
|
|
31
|
+
|
|
32
|
+
from aethermesh.__version__ import (
|
|
33
|
+
__version__,
|
|
34
|
+
__author__,
|
|
35
|
+
__email__,
|
|
36
|
+
__doi__,
|
|
37
|
+
__license__,
|
|
38
|
+
__paper_version__,
|
|
39
|
+
__record_id__,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# =============================================================================
|
|
44
|
+
# Info
|
|
45
|
+
# =============================================================================
|
|
46
|
+
|
|
47
|
+
def cmd_info(args: argparse.Namespace) -> int:
|
|
48
|
+
"""Print version, DOI, and record information."""
|
|
49
|
+
print("=" * 60)
|
|
50
|
+
print(f"AetherMesh v{__version__}")
|
|
51
|
+
print("=" * 60)
|
|
52
|
+
print(f"Author: {__author__}")
|
|
53
|
+
print(f"Email: {__email__}")
|
|
54
|
+
print(f"License: {__license__}")
|
|
55
|
+
print(f"Paper version: {__paper_version__}")
|
|
56
|
+
print(f"Record ID: {__record_id__}")
|
|
57
|
+
print(f"DOI: {__doi__}")
|
|
58
|
+
print(f"DOI URL: https://doi.org/{__doi__}")
|
|
59
|
+
print()
|
|
60
|
+
print("AetherMesh is the decentralization layer of the Aether family:")
|
|
61
|
+
print(" AetherGit (storage) — 10.5281/zenodo.23966896")
|
|
62
|
+
print(" AetherFlow (integration) — 10.5281/zenodo.23979129")
|
|
63
|
+
print(" AetherProof (verification) — 10.5281/zenodo.24013124")
|
|
64
|
+
print(" AetherMesh (decentralization) — 10.5281/zenodo.24038038")
|
|
65
|
+
print()
|
|
66
|
+
print("Status: Reference implementation. Preliminary results.")
|
|
67
|
+
print(" Not a production system. No real-world deployment.")
|
|
68
|
+
print("=" * 60)
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# Simulate
|
|
74
|
+
# =============================================================================
|
|
75
|
+
|
|
76
|
+
def cmd_simulate(args: argparse.Namespace) -> int:
|
|
77
|
+
"""Run the full testbed and report all three metrics."""
|
|
78
|
+
from aethermesh.simulation.testbed import Testbed, TestbedConfig
|
|
79
|
+
|
|
80
|
+
print("=" * 60)
|
|
81
|
+
print(f"AetherMesh v{__version__} — simulated testbed")
|
|
82
|
+
print("=" * 60)
|
|
83
|
+
print(f"Nodes: {args.nodes}")
|
|
84
|
+
print(f"Fanout: {args.fanout}")
|
|
85
|
+
print(f"Rounds: {args.max_rounds}")
|
|
86
|
+
print(f"Seed: {args.seed}")
|
|
87
|
+
print()
|
|
88
|
+
|
|
89
|
+
config = TestbedConfig(
|
|
90
|
+
n_nodes=args.nodes,
|
|
91
|
+
fanout=args.fanout,
|
|
92
|
+
max_rounds=args.max_rounds,
|
|
93
|
+
seed=args.seed,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Metric 1: convergence
|
|
97
|
+
print("[1/3] Convergence latency")
|
|
98
|
+
tb1 = Testbed(config)
|
|
99
|
+
m1 = tb1.measure_convergence_latency(initial_delta_count=1)
|
|
100
|
+
print(f" Converged at time: {m1['converged_at_time']}")
|
|
101
|
+
print(f" Converged at round: {m1['converged_at_round']}")
|
|
102
|
+
print(f" Final fraction: {m1['final_fraction']:.2%}")
|
|
103
|
+
print()
|
|
104
|
+
|
|
105
|
+
# Metric 2: conflict overhead
|
|
106
|
+
print("[2/3] Conflict resolution overhead")
|
|
107
|
+
tb2 = Testbed(config)
|
|
108
|
+
m2 = tb2.measure_conflict_overhead(n_conflicting_deltas=10)
|
|
109
|
+
print(f" Conflicts flagged: {m2['conflicts_flagged']}")
|
|
110
|
+
print(f" Deadlocks: {m2['deadlocks']}")
|
|
111
|
+
print(f" Throughput overhead: {m2['throughput_overhead_pct']}%")
|
|
112
|
+
print()
|
|
113
|
+
|
|
114
|
+
# Metric 3: fault resilience
|
|
115
|
+
print("[3/3] Fault resilience")
|
|
116
|
+
tb3 = Testbed(config)
|
|
117
|
+
m3 = tb3.measure_fault_resilience(n_malformed=args.n_malformed)
|
|
118
|
+
print(f" Injected: {m3['injected']}")
|
|
119
|
+
print(f" Blocked: {m3['blocked']}")
|
|
120
|
+
print(f" Block rate: {m3['block_rate']:.2%}")
|
|
121
|
+
print()
|
|
122
|
+
|
|
123
|
+
# Summary
|
|
124
|
+
results = {
|
|
125
|
+
"version": __version__,
|
|
126
|
+
"config": {
|
|
127
|
+
"nodes": args.nodes,
|
|
128
|
+
"fanout": args.fanout,
|
|
129
|
+
"max_rounds": args.max_rounds,
|
|
130
|
+
"seed": args.seed,
|
|
131
|
+
},
|
|
132
|
+
"metric_1_convergence": m1,
|
|
133
|
+
"metric_2_conflict_overhead": m2,
|
|
134
|
+
"metric_3_fault_resilience": m3,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if args.output:
|
|
138
|
+
with open(args.output, "w") as f:
|
|
139
|
+
json.dump(results, f, indent=2)
|
|
140
|
+
print(f"Results written to {args.output}")
|
|
141
|
+
else:
|
|
142
|
+
print(json.dumps(results, indent=2))
|
|
143
|
+
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# =============================================================================
|
|
148
|
+
# Verify
|
|
149
|
+
# =============================================================================
|
|
150
|
+
|
|
151
|
+
def cmd_verify(args: argparse.Namespace) -> int:
|
|
152
|
+
"""Run a self-check without a full simulation."""
|
|
153
|
+
print("=" * 60)
|
|
154
|
+
print(f"AetherMesh v{__version__} — self-check")
|
|
155
|
+
print("=" * 60)
|
|
156
|
+
|
|
157
|
+
ok = True
|
|
158
|
+
|
|
159
|
+
# 1. Join laws on the Figure 4 example
|
|
160
|
+
print("[1/4] Join laws (Figure 4 example)")
|
|
161
|
+
from aethermesh.crdt.semilattice import build_figure4_example, verify_join_laws
|
|
162
|
+
lattice, elements = build_figure4_example()
|
|
163
|
+
laws_ok, violations = verify_join_laws(lattice, elements)
|
|
164
|
+
if laws_ok:
|
|
165
|
+
print(f" ✅ 7 elements, all 3 laws hold")
|
|
166
|
+
else:
|
|
167
|
+
print(f" ❌ {len(violations)} violations")
|
|
168
|
+
ok = False
|
|
169
|
+
|
|
170
|
+
# 2. Signature verification
|
|
171
|
+
print("[2/4] Ed25519 signature verification")
|
|
172
|
+
from aethermesh.bft import generate_keypair, sign_delta, verify_delta_signature
|
|
173
|
+
from aethermesh.models import Delta, DeltaKind
|
|
174
|
+
priv, pub = generate_keypair()
|
|
175
|
+
d = Delta.create(body={"op": "test"}, origin="test-agent", kind=DeltaKind.EDIT_BODY)
|
|
176
|
+
d_signed = sign_delta(d, priv, "key-1")
|
|
177
|
+
sig_ok = verify_delta_signature(d_signed, pub)
|
|
178
|
+
if sig_ok:
|
|
179
|
+
print(f" ✅ Valid signature verified")
|
|
180
|
+
else:
|
|
181
|
+
print(f" ❌ Signature verification failed")
|
|
182
|
+
ok = False
|
|
183
|
+
|
|
184
|
+
# 3. Admission pipeline
|
|
185
|
+
print("[3/4] Admission pipeline")
|
|
186
|
+
from aethermesh.bft import KeyRegistry
|
|
187
|
+
from aethermesh.admission.pipeline import AdmissionPipeline
|
|
188
|
+
from aethermesh.crdt.delta_state import DeltaStateCRDT
|
|
189
|
+
registry = KeyRegistry()
|
|
190
|
+
registry.register("test-agent", "key-1", pub)
|
|
191
|
+
pipeline = AdmissionPipeline(registry=registry)
|
|
192
|
+
crdt = DeltaStateCRDT(origin="test")
|
|
193
|
+
verdict = pipeline.admit(d_signed, crdt.replica)
|
|
194
|
+
if verdict.accepted:
|
|
195
|
+
print(f" ✅ Valid delta accepted")
|
|
196
|
+
else:
|
|
197
|
+
print(f" ❌ Valid delta rejected: {verdict.reason}")
|
|
198
|
+
ok = False
|
|
199
|
+
|
|
200
|
+
# 4. Reject unsigned
|
|
201
|
+
d_unsigned = Delta.create(body={"op": "test2"}, origin="test-agent", kind=DeltaKind.EDIT_BODY)
|
|
202
|
+
verdict2 = pipeline.admit(d_unsigned, crdt.replica)
|
|
203
|
+
if verdict2.verdict.value == "reject":
|
|
204
|
+
print(f" ✅ Unsigned delta rejected")
|
|
205
|
+
else:
|
|
206
|
+
print(f" ❌ Unsigned delta not rejected: {verdict2.verdict.value}")
|
|
207
|
+
ok = False
|
|
208
|
+
|
|
209
|
+
print("[4/4] Summary")
|
|
210
|
+
if ok:
|
|
211
|
+
print(" ✅ All checks passed")
|
|
212
|
+
print("=" * 60)
|
|
213
|
+
return 0
|
|
214
|
+
else:
|
|
215
|
+
print(" ❌ Some checks failed")
|
|
216
|
+
print("=" * 60)
|
|
217
|
+
return 1
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# =============================================================================
|
|
221
|
+
# Entry point
|
|
222
|
+
# =============================================================================
|
|
223
|
+
|
|
224
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
225
|
+
"""Build the argument parser."""
|
|
226
|
+
parser = argparse.ArgumentParser(
|
|
227
|
+
prog="aethermesh",
|
|
228
|
+
description=(
|
|
229
|
+
f"AetherMesh v{__version__} — decentralized P2P agent synchronization"
|
|
230
|
+
),
|
|
231
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"--version",
|
|
235
|
+
action="version",
|
|
236
|
+
version=f"aethermesh {__version__}",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
240
|
+
|
|
241
|
+
# info
|
|
242
|
+
p_info = sub.add_parser("info", help="Print version, DOI, and record information")
|
|
243
|
+
p_info.set_defaults(func=cmd_info)
|
|
244
|
+
|
|
245
|
+
# verify
|
|
246
|
+
p_verify = sub.add_parser("verify", help="Run a self-check (no simulation)")
|
|
247
|
+
p_verify.set_defaults(func=cmd_verify)
|
|
248
|
+
|
|
249
|
+
# simulate
|
|
250
|
+
p_sim = sub.add_parser("simulate", help="Run the full simulated testbed")
|
|
251
|
+
p_sim.add_argument("--nodes", type=int, default=50, help="Number of nodes (default: 50)")
|
|
252
|
+
p_sim.add_argument("--fanout", type=int, default=5, help="Gossip fanout (default: 5)")
|
|
253
|
+
p_sim.add_argument("--max-rounds", type=int, default=200, help="Max gossip rounds (default: 200)")
|
|
254
|
+
p_sim.add_argument("--seed", type=int, default=42, help="Random seed (default: 42)")
|
|
255
|
+
p_sim.add_argument("--n-malformed", type=int, default=100, help="Malformed deltas to inject (default: 100)")
|
|
256
|
+
p_sim.add_argument("--output", type=str, default=None, help="Write results JSON to this file")
|
|
257
|
+
p_sim.set_defaults(func=cmd_simulate)
|
|
258
|
+
|
|
259
|
+
return parser
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
263
|
+
"""Main entry point."""
|
|
264
|
+
parser = build_parser()
|
|
265
|
+
args = parser.parse_args(argv)
|
|
266
|
+
return args.func(args)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
sys.exit(main())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""AetherMesh — CRDT module (Pillar 1)."""
|
|
2
|
+
|
|
3
|
+
from aethermesh.crdt.semilattice import (
|
|
4
|
+
JoinSemilattice,
|
|
5
|
+
DeltaSetLattice,
|
|
6
|
+
JoinLawError,
|
|
7
|
+
verify_join_laws,
|
|
8
|
+
)
|
|
9
|
+
from aethermesh.crdt.delta_state import DeltaStateCRDT
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"JoinSemilattice",
|
|
13
|
+
"DeltaSetLattice",
|
|
14
|
+
"JoinLawError",
|
|
15
|
+
"verify_join_laws",
|
|
16
|
+
"DeltaStateCRDT",
|
|
17
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherMesh — Delta-state CRDT (Pillar 1).
|
|
3
|
+
|
|
4
|
+
Implements delta-state replication (Section 3.1.3 of the paper).
|
|
5
|
+
|
|
6
|
+
AetherMesh uses delta-state replication as its default: nodes ship
|
|
7
|
+
deltas that are themselves lattice elements, so duplication and
|
|
8
|
+
reordering remain harmless, and fall back to full-state exchange only
|
|
9
|
+
when a digest comparison indicates that incremental repair would cost
|
|
10
|
+
more than a state transfer.
|
|
11
|
+
|
|
12
|
+
This module provides:
|
|
13
|
+
|
|
14
|
+
- DeltaStateCRDT: the main CRDT class, operating on a Replica
|
|
15
|
+
|
|
16
|
+
Reference:
|
|
17
|
+
Baladi, S. (2026). AetherMesh v1.0.0. Sections 3.1.1 and 3.1.3.
|
|
18
|
+
Zenodo. https://doi.org/10.5281/zenodo.24038038
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Dict, FrozenSet, Iterable, List, Optional, Set, Tuple
|
|
24
|
+
|
|
25
|
+
from aethermesh.models import Delta, Replica
|
|
26
|
+
from aethermesh.crdt.semilattice import DeltaSetLattice
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DeltaStateCRDT:
|
|
30
|
+
"""
|
|
31
|
+
A delta-state CRDT over a `Replica`.
|
|
32
|
+
|
|
33
|
+
The CRDT wraps a `Replica` (which holds the admitted delta set)
|
|
34
|
+
and provides delta-based operations:
|
|
35
|
+
|
|
36
|
+
- apply(delta): join a single delta (idempotent)
|
|
37
|
+
- apply_many(deltas): join multiple deltas
|
|
38
|
+
- merge(other): join another CRDT's replica
|
|
39
|
+
- delta_since(digest): the deltas the peer is missing
|
|
40
|
+
- digest(): compact summary of local state
|
|
41
|
+
- full_state(): the entire admitted set (fallback)
|
|
42
|
+
|
|
43
|
+
The delta set lattice is the grow-only-set CRDT lifted to deltas
|
|
44
|
+
(Section 3.1.1), with set union as join.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, origin: str = "anonymous"):
|
|
48
|
+
self.replica = Replica(origin=origin)
|
|
49
|
+
self._lattice = DeltaSetLattice()
|
|
50
|
+
|
|
51
|
+
# -------------------------------------------------------------------------
|
|
52
|
+
# Core operations
|
|
53
|
+
# -------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def apply(self, delta: Delta) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
Join a single delta into the local replica.
|
|
58
|
+
|
|
59
|
+
Returns True if the delta was newly admitted, False if it was
|
|
60
|
+
already present (idempotence) or deferred (missing parents).
|
|
61
|
+
|
|
62
|
+
A delta with missing parents is deferred (Section 3.3.2) — it is
|
|
63
|
+
not rejected.
|
|
64
|
+
"""
|
|
65
|
+
if delta.id in self.replica:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
if not self.replica.has_parents(delta):
|
|
69
|
+
self.replica.deferred[delta.id] = delta
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
self.replica.join(delta)
|
|
73
|
+
self._try_drain_deferred()
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def apply_many(self, deltas: Iterable[Delta]) -> int:
|
|
77
|
+
"""Apply multiple deltas. Returns the count of newly admitted."""
|
|
78
|
+
count = 0
|
|
79
|
+
for d in deltas:
|
|
80
|
+
if self.apply(d):
|
|
81
|
+
count += 1
|
|
82
|
+
return count
|
|
83
|
+
|
|
84
|
+
def merge(self, other: "DeltaStateCRDT") -> int:
|
|
85
|
+
"""
|
|
86
|
+
Merge another CRDT's replica into this one (set union).
|
|
87
|
+
|
|
88
|
+
Returns the count of newly admitted deltas.
|
|
89
|
+
"""
|
|
90
|
+
count = 0
|
|
91
|
+
for did in other.replica.ids() - self.replica.ids():
|
|
92
|
+
delta = other.replica.admitted[did]
|
|
93
|
+
if self.apply(delta):
|
|
94
|
+
count += 1
|
|
95
|
+
return count
|
|
96
|
+
|
|
97
|
+
# -------------------------------------------------------------------------
|
|
98
|
+
# Delta exchange (Section 3.2.1)
|
|
99
|
+
# -------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def digest(self) -> FrozenSet[str]:
|
|
102
|
+
"""
|
|
103
|
+
A compact summary of local state: the set of admitted delta ids.
|
|
104
|
+
|
|
105
|
+
In production, this would be compressed into a version vector,
|
|
106
|
+
Merkle tree, or Bloom filter (Section 3.2.1). For the reference
|
|
107
|
+
implementation we use the raw id set, which is exact.
|
|
108
|
+
"""
|
|
109
|
+
return self.replica.ids()
|
|
110
|
+
|
|
111
|
+
def delta_since(self, peer_digest: FrozenSet[str]) -> List[Delta]:
|
|
112
|
+
"""
|
|
113
|
+
The deltas this replica has that the peer does not.
|
|
114
|
+
|
|
115
|
+
Used in step 2 of the anti-entropy exchange (Figure 5): the
|
|
116
|
+
responder returns "digest + list of deltas A appears to lack".
|
|
117
|
+
"""
|
|
118
|
+
missing = self.replica.ids() - peer_digest
|
|
119
|
+
return [self.replica.admitted[did] for did in missing]
|
|
120
|
+
|
|
121
|
+
def full_state(self) -> List[Delta]:
|
|
122
|
+
"""
|
|
123
|
+
The entire admitted set (state-based fallback).
|
|
124
|
+
|
|
125
|
+
Used when a digest comparison indicates that incremental repair
|
|
126
|
+
would cost more than a state transfer (Section 3.1.3).
|
|
127
|
+
"""
|
|
128
|
+
return list(self.replica.admitted.values())
|
|
129
|
+
|
|
130
|
+
# -------------------------------------------------------------------------
|
|
131
|
+
# Deferred deltas (Section 3.3.2)
|
|
132
|
+
# -------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def _try_drain_deferred(self) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Retry deferred deltas whose parents may now be present.
|
|
137
|
+
|
|
138
|
+
Called after each successful join.
|
|
139
|
+
"""
|
|
140
|
+
progress = True
|
|
141
|
+
while progress:
|
|
142
|
+
progress = False
|
|
143
|
+
ready = [
|
|
144
|
+
d for d in self.replica.deferred.values()
|
|
145
|
+
if self.replica.has_parents(d)
|
|
146
|
+
]
|
|
147
|
+
for d in ready:
|
|
148
|
+
del self.replica.deferred[d.id]
|
|
149
|
+
self.replica.join(d)
|
|
150
|
+
progress = True
|
|
151
|
+
|
|
152
|
+
def deferred_count(self) -> int:
|
|
153
|
+
"""Number of deltas currently deferred (missing parents)."""
|
|
154
|
+
return len(self.replica.deferred)
|
|
155
|
+
|
|
156
|
+
# -------------------------------------------------------------------------
|
|
157
|
+
# Materialization
|
|
158
|
+
# -------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
def materialize(self) -> List[Delta]:
|
|
161
|
+
"""Deterministic linearization of the admitted delta set."""
|
|
162
|
+
return self.replica.materialize()
|
|
163
|
+
|
|
164
|
+
# -------------------------------------------------------------------------
|
|
165
|
+
# Introspection
|
|
166
|
+
# -------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def __len__(self) -> int:
|
|
169
|
+
return len(self.replica)
|
|
170
|
+
|
|
171
|
+
def __contains__(self, delta_id: str) -> bool:
|
|
172
|
+
return delta_id in self.replica
|
|
173
|
+
|
|
174
|
+
def ids(self) -> FrozenSet[str]:
|
|
175
|
+
return self.replica.ids()
|
|
176
|
+
|
|
177
|
+
def state_size(self) -> int:
|
|
178
|
+
"""Number of admitted deltas."""
|
|
179
|
+
return len(self.replica)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# =============================================================================
|
|
183
|
+
# Sanity check on the join laws (Figure 4 example)
|
|
184
|
+
# =============================================================================
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
from aethermesh.crdt.semilattice import build_figure4_example, verify_join_laws
|
|
188
|
+
|
|
189
|
+
lattice, elements = build_figure4_example()
|
|
190
|
+
ok, violations = verify_join_laws(lattice, elements)
|
|
191
|
+
print(f"Lattice elements: {len(elements)}")
|
|
192
|
+
print(f"Join laws hold: {ok}")
|
|
193
|
+
if violations:
|
|
194
|
+
for v in violations[:5]:
|
|
195
|
+
print(f" - {v}")
|