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
aethermesh/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherMesh v2.0.0 — Reference Implementation
|
|
3
|
+
|
|
4
|
+
Decentralized Peer-to-Peer Agent Synchronization and State CRDTs
|
|
5
|
+
for Autonomous Multi-Agent Systems.
|
|
6
|
+
|
|
7
|
+
This is the reference implementation of the AetherMesh architecture
|
|
8
|
+
described in the v1.0.0 research paper:
|
|
9
|
+
|
|
10
|
+
Baladi, S. (2026). AetherMesh: Decentralized Peer-to-Peer Agent
|
|
11
|
+
Synchronization and State CRDTs for Autonomous Multi-Agent Systems.
|
|
12
|
+
Zenodo. https://doi.org/10.5281/zenodo.24038038
|
|
13
|
+
|
|
14
|
+
The v1.0.0 paper specifies the architecture with proof sketches and
|
|
15
|
+
a simulated evaluation. This package (v2.0.0) provides a reference
|
|
16
|
+
implementation of the three pillars:
|
|
17
|
+
|
|
18
|
+
1. CRDTs for Code and Metadata DAGs
|
|
19
|
+
2. Epidemic Gossip Protocols
|
|
20
|
+
3. Lightweight Byzantine Fault Tolerance
|
|
21
|
+
|
|
22
|
+
Status: Reference implementation. Preliminary results. Not a
|
|
23
|
+
production system. Real-world deployment has not been evaluated.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from aethermesh.__version__ import __version__, __author__, __email__, __doi__, __license__
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"__author__",
|
|
31
|
+
"__email__",
|
|
32
|
+
"__doi__",
|
|
33
|
+
"__license__",
|
|
34
|
+
]
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherMesh — Admission pipeline (Pillar 3 integration).
|
|
3
|
+
|
|
4
|
+
Integrates the three BFT checks from Figure 7 of the paper into a
|
|
5
|
+
single admission pipeline:
|
|
6
|
+
|
|
7
|
+
Step 2: Signature check (Check 1: Signature)
|
|
8
|
+
Step 3: DAG integrity check (Check 2: DAG integrity)
|
|
9
|
+
Step 4: Schema check (Check 3: Schema)
|
|
10
|
+
Step 5: Semantics check (Check 4: Semantics)
|
|
11
|
+
Step 6: Join to semilattice (if all checks pass)
|
|
12
|
+
|
|
13
|
+
A delta that fails any check is rejected (or deferred, if its parents
|
|
14
|
+
are missing). A delta that passes all checks is admitted.
|
|
15
|
+
|
|
16
|
+
The admission determinism requirement (A6) is respected: every check
|
|
17
|
+
is a pure, deterministic function of the delta and its causal closure
|
|
18
|
+
(not of arrival order or local clock).
|
|
19
|
+
|
|
20
|
+
Reference:
|
|
21
|
+
Baladi, S. (2026). AetherMesh v1.0.0. Section 3.3.4 and Figure 7.
|
|
22
|
+
Zenodo. https://doi.org/10.5281/zenodo.24038038
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
30
|
+
|
|
31
|
+
from aethermesh.models import (
|
|
32
|
+
AdmissionStage,
|
|
33
|
+
AdmissionVerdict,
|
|
34
|
+
Delta,
|
|
35
|
+
Replica,
|
|
36
|
+
Verdict,
|
|
37
|
+
)
|
|
38
|
+
from aethermesh.bft.provenance import ProvenanceChecker, KeyRegistry
|
|
39
|
+
from aethermesh.bft.invariants import StructuralChecker
|
|
40
|
+
from aethermesh.bft.semantic_matrix import (
|
|
41
|
+
CompatibilityMatrix,
|
|
42
|
+
DEFAULT_MATRIX,
|
|
43
|
+
SemanticVerdict,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# =============================================================================
|
|
48
|
+
# StageResult
|
|
49
|
+
# =============================================================================
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class StageResult:
|
|
53
|
+
"""Result of one pipeline stage."""
|
|
54
|
+
stage: AdmissionStage
|
|
55
|
+
ok: bool
|
|
56
|
+
reason: str
|
|
57
|
+
verdict: Verdict # what the stage recommends (for defer handling)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# =============================================================================
|
|
61
|
+
# PipelineConfig
|
|
62
|
+
# =============================================================================
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class PipelineConfig:
|
|
66
|
+
"""
|
|
67
|
+
Configuration for the admission pipeline.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
max_parents: Max parents per delta (structural bound).
|
|
71
|
+
max_bytes: Max serialized delta size (structural bound).
|
|
72
|
+
reject_on_semantic_conflict: If True, semantic CONFLICT => reject.
|
|
73
|
+
If False, semantic CONFLICT => quarantine.
|
|
74
|
+
quarantine_on_semantic_resolve: If True, semantic RESOLVE => quarantine.
|
|
75
|
+
schema_validator: Optional custom schema validator (delta -> bool).
|
|
76
|
+
"""
|
|
77
|
+
max_parents: int = 64
|
|
78
|
+
max_bytes: int = 1_048_576
|
|
79
|
+
reject_on_semantic_conflict: bool = True
|
|
80
|
+
quarantine_on_semantic_resolve: bool = False
|
|
81
|
+
schema_validator: Optional[Callable[[Delta], Tuple[bool, str]]] = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# Default schema validator
|
|
86
|
+
# =============================================================================
|
|
87
|
+
|
|
88
|
+
def _default_schema_validator(delta: Delta) -> Tuple[bool, str]:
|
|
89
|
+
"""
|
|
90
|
+
Default schema check: the delta body must be a JSON-serializable
|
|
91
|
+
dict, and must contain an 'op' field (a string).
|
|
92
|
+
"""
|
|
93
|
+
if not isinstance(delta.body, dict):
|
|
94
|
+
return False, f"body must be a dict, got {type(delta.body).__name__}"
|
|
95
|
+
if "op" not in delta.body:
|
|
96
|
+
return False, "body missing 'op' field"
|
|
97
|
+
if not isinstance(delta.body["op"], str):
|
|
98
|
+
return False, f"'op' must be a string, got {type(delta.body['op']).__name__}"
|
|
99
|
+
try:
|
|
100
|
+
json.dumps(delta.body, sort_keys=True)
|
|
101
|
+
except (TypeError, ValueError) as e:
|
|
102
|
+
return False, f"body not JSON-serializable: {e}"
|
|
103
|
+
return True, "ok"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# =============================================================================
|
|
107
|
+
# AdmissionPipeline
|
|
108
|
+
# =============================================================================
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class AdmissionPipeline:
|
|
112
|
+
"""
|
|
113
|
+
The four-check admission pipeline (Figure 7 of the paper).
|
|
114
|
+
|
|
115
|
+
The pipeline is a pure function of (delta, replica_state):
|
|
116
|
+
given the same delta and the same causal closure, every honest
|
|
117
|
+
node reaches the same verdict (assumption A6).
|
|
118
|
+
|
|
119
|
+
Attributes:
|
|
120
|
+
registry: Key registry for provenance.
|
|
121
|
+
matrix: Semantic compatibility matrix.
|
|
122
|
+
config: Pipeline configuration.
|
|
123
|
+
"""
|
|
124
|
+
registry: KeyRegistry = field(default_factory=KeyRegistry)
|
|
125
|
+
matrix: CompatibilityMatrix = field(default_factory=lambda: DEFAULT_MATRIX)
|
|
126
|
+
config: PipelineConfig = field(default_factory=PipelineConfig)
|
|
127
|
+
|
|
128
|
+
def __post_init__(self):
|
|
129
|
+
self._provenance = ProvenanceChecker(registry=self.registry)
|
|
130
|
+
self._structural = StructuralChecker(
|
|
131
|
+
max_parents=self.config.max_parents,
|
|
132
|
+
max_bytes=self.config.max_bytes,
|
|
133
|
+
)
|
|
134
|
+
self._schema = self.config.schema_validator or _default_schema_validator
|
|
135
|
+
|
|
136
|
+
# -------------------------------------------------------------------------
|
|
137
|
+
# The four checks
|
|
138
|
+
# -------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def check_signature(self, delta: Delta) -> StageResult:
|
|
141
|
+
"""Check 1: Signature (Section 3.3.1)."""
|
|
142
|
+
ok, reason = self._provenance.check(delta)
|
|
143
|
+
verdict = Verdict.ACCEPT if ok else Verdict.REJECT
|
|
144
|
+
return StageResult(AdmissionStage.SIGNATURE, ok, reason, verdict)
|
|
145
|
+
|
|
146
|
+
def check_dag_integrity(
|
|
147
|
+
self,
|
|
148
|
+
delta: Delta,
|
|
149
|
+
replica: Replica,
|
|
150
|
+
) -> StageResult:
|
|
151
|
+
"""
|
|
152
|
+
Check 2: DAG integrity (Section 3.3.2).
|
|
153
|
+
|
|
154
|
+
A delta with missing parents is DEFERRED, not rejected.
|
|
155
|
+
"""
|
|
156
|
+
# Structural bounds first.
|
|
157
|
+
ok, reason = self._structural.check(delta)
|
|
158
|
+
if not ok:
|
|
159
|
+
return StageResult(AdmissionStage.DAG_INTEGRITY, False, reason, Verdict.REJECT)
|
|
160
|
+
|
|
161
|
+
# Parent presence: if any parent is missing, DEFER.
|
|
162
|
+
if not replica.has_parents(delta):
|
|
163
|
+
missing = [p for p in delta.parents if p not in replica.admitted]
|
|
164
|
+
return StageResult(
|
|
165
|
+
AdmissionStage.DAG_INTEGRITY,
|
|
166
|
+
False,
|
|
167
|
+
f"missing parents: {missing[:3]}",
|
|
168
|
+
Verdict.DEFER,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return StageResult(AdmissionStage.DAG_INTEGRITY, True, "ok", Verdict.ACCEPT)
|
|
172
|
+
|
|
173
|
+
def check_schema(self, delta: Delta) -> StageResult:
|
|
174
|
+
"""Check 3: Schema (Section 3.3.3)."""
|
|
175
|
+
ok, reason = self._schema(delta)
|
|
176
|
+
verdict = Verdict.ACCEPT if ok else Verdict.REJECT
|
|
177
|
+
return StageResult(AdmissionStage.SCHEMA, ok, reason, verdict)
|
|
178
|
+
|
|
179
|
+
def check_semantics(
|
|
180
|
+
self,
|
|
181
|
+
delta: Delta,
|
|
182
|
+
replica: Replica,
|
|
183
|
+
) -> StageResult:
|
|
184
|
+
"""
|
|
185
|
+
Check 4: Semantics (Section 3.3.3).
|
|
186
|
+
|
|
187
|
+
The semantic check is over the causal closure of the delta
|
|
188
|
+
(the delta plus its admitted ancestors).
|
|
189
|
+
"""
|
|
190
|
+
# Compute the causal closure (delta + admitted ancestors).
|
|
191
|
+
closure = [delta]
|
|
192
|
+
visited = set()
|
|
193
|
+
frontier = list(delta.parents)
|
|
194
|
+
while frontier:
|
|
195
|
+
did = frontier.pop()
|
|
196
|
+
if did in visited:
|
|
197
|
+
continue
|
|
198
|
+
visited.add(did)
|
|
199
|
+
ancestor = replica.admitted.get(did)
|
|
200
|
+
if ancestor is not None:
|
|
201
|
+
closure.append(ancestor)
|
|
202
|
+
frontier.extend(ancestor.parents)
|
|
203
|
+
|
|
204
|
+
verdict, reason = self.matrix.check(closure)
|
|
205
|
+
|
|
206
|
+
if verdict == SemanticVerdict.OK:
|
|
207
|
+
return StageResult(AdmissionStage.SEMANTICS, True, "ok", Verdict.ACCEPT)
|
|
208
|
+
if verdict == SemanticVerdict.RESOLVE:
|
|
209
|
+
if self.config.quarantine_on_semantic_resolve:
|
|
210
|
+
return StageResult(AdmissionStage.SEMANTICS, False, reason, Verdict.QUARANTINE)
|
|
211
|
+
return StageResult(AdmissionStage.SEMANTICS, False, reason, Verdict.REJECT)
|
|
212
|
+
# CONFLICT
|
|
213
|
+
if self.config.reject_on_semantic_conflict:
|
|
214
|
+
return StageResult(AdmissionStage.SEMANTICS, False, reason, Verdict.REJECT)
|
|
215
|
+
return StageResult(AdmissionStage.SEMANTICS, False, reason, Verdict.QUARANTINE)
|
|
216
|
+
|
|
217
|
+
# -------------------------------------------------------------------------
|
|
218
|
+
# Full pipeline
|
|
219
|
+
# -------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def admit(
|
|
222
|
+
self,
|
|
223
|
+
delta: Delta,
|
|
224
|
+
replica: Replica,
|
|
225
|
+
) -> AdmissionVerdict:
|
|
226
|
+
"""
|
|
227
|
+
Run the full pipeline on a delta against a local replica.
|
|
228
|
+
|
|
229
|
+
Returns an AdmissionVerdict with the final outcome.
|
|
230
|
+
|
|
231
|
+
The pipeline short-circuits on the first failing stage.
|
|
232
|
+
"""
|
|
233
|
+
# Idempotence: if already admitted, accept immediately.
|
|
234
|
+
if delta.id in replica:
|
|
235
|
+
return AdmissionVerdict(
|
|
236
|
+
delta_id=delta.id,
|
|
237
|
+
verdict=Verdict.ACCEPT,
|
|
238
|
+
stage=None,
|
|
239
|
+
reason="already admitted",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Check 1: Signature
|
|
243
|
+
r = self.check_signature(delta)
|
|
244
|
+
if not r.ok:
|
|
245
|
+
return AdmissionVerdict(delta.id, r.verdict, r.stage, r.reason)
|
|
246
|
+
|
|
247
|
+
# Check 2: DAG integrity (may defer)
|
|
248
|
+
r = self.check_dag_integrity(delta, replica)
|
|
249
|
+
if not r.ok:
|
|
250
|
+
return AdmissionVerdict(delta.id, r.verdict, r.stage, r.reason)
|
|
251
|
+
|
|
252
|
+
# Check 3: Schema
|
|
253
|
+
r = self.check_schema(delta)
|
|
254
|
+
if not r.ok:
|
|
255
|
+
return AdmissionVerdict(delta.id, r.verdict, r.stage, r.reason)
|
|
256
|
+
|
|
257
|
+
# Check 4: Semantics
|
|
258
|
+
r = self.check_semantics(delta, replica)
|
|
259
|
+
if not r.ok:
|
|
260
|
+
return AdmissionVerdict(delta.id, r.verdict, r.stage, r.reason)
|
|
261
|
+
|
|
262
|
+
return AdmissionVerdict(
|
|
263
|
+
delta_id=delta.id,
|
|
264
|
+
verdict=Verdict.ACCEPT,
|
|
265
|
+
stage=None,
|
|
266
|
+
reason="admitted",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# =============================================================================
|
|
271
|
+
# Convenience: build a pipeline with a fresh registry
|
|
272
|
+
# =============================================================================
|
|
273
|
+
|
|
274
|
+
def build_pipeline(
|
|
275
|
+
registry: Optional[KeyRegistry] = None,
|
|
276
|
+
config: Optional[PipelineConfig] = None,
|
|
277
|
+
) -> AdmissionPipeline:
|
|
278
|
+
"""Build an admission pipeline with sensible defaults."""
|
|
279
|
+
return AdmissionPipeline(
|
|
280
|
+
registry=registry or KeyRegistry(),
|
|
281
|
+
config=config or PipelineConfig(),
|
|
282
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""AetherMesh — Byzantine Fault Tolerance module (Pillar 3)."""
|
|
2
|
+
|
|
3
|
+
from aethermesh.bft.provenance import (
|
|
4
|
+
KeyRegistry,
|
|
5
|
+
ProvenanceChecker,
|
|
6
|
+
generate_keypair,
|
|
7
|
+
sign_delta,
|
|
8
|
+
verify_delta_signature,
|
|
9
|
+
)
|
|
10
|
+
from aethermesh.bft.invariants import (
|
|
11
|
+
StructuralChecker,
|
|
12
|
+
InvariantViolation,
|
|
13
|
+
)
|
|
14
|
+
from aethermesh.bft.semantic_matrix import (
|
|
15
|
+
CompatibilityMatrix,
|
|
16
|
+
DEFAULT_MATRIX,
|
|
17
|
+
SemanticVerdict,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"KeyRegistry",
|
|
22
|
+
"ProvenanceChecker",
|
|
23
|
+
"generate_keypair",
|
|
24
|
+
"sign_delta",
|
|
25
|
+
"verify_delta_signature",
|
|
26
|
+
"StructuralChecker",
|
|
27
|
+
"InvariantViolation",
|
|
28
|
+
"CompatibilityMatrix",
|
|
29
|
+
"DEFAULT_MATRIX",
|
|
30
|
+
"SemanticVerdict",
|
|
31
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherMesh — Structural invariants (Pillar 3, Section 3.3.2).
|
|
3
|
+
|
|
4
|
+
Structural checks verify that a delta is well-formed as a node of the
|
|
5
|
+
parent DAG:
|
|
6
|
+
|
|
7
|
+
- Id equals the hash of the content.
|
|
8
|
+
- Every parent pointer refers to a delta already admitted, or the
|
|
9
|
+
delta is held in a bounded deferral buffer.
|
|
10
|
+
- The pointer structure cannot form a cycle (guaranteed by content
|
|
11
|
+
addressing).
|
|
12
|
+
- Size and fan-in bounds hold.
|
|
13
|
+
|
|
14
|
+
A delta with missing parents is deferred, not rejected, because gossip
|
|
15
|
+
may deliver a child before its parent. Deferral is bounded in size and
|
|
16
|
+
time to prevent resource exhaustion (threat T9).
|
|
17
|
+
|
|
18
|
+
This module provides:
|
|
19
|
+
|
|
20
|
+
- InvariantViolation: exception for invariant failures
|
|
21
|
+
- StructuralChecker: the structural-check stage of admission
|
|
22
|
+
|
|
23
|
+
Reference:
|
|
24
|
+
Baladi, S. (2026). AetherMesh v1.0.0. Section 3.3.2.
|
|
25
|
+
Zenodo. https://doi.org/10.5281/zenodo.24038038
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
from aethermesh.models import Delta
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# InvariantViolation
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
class InvariantViolation(Exception):
|
|
42
|
+
"""Raised when a delta violates a structural invariant."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# StructuralChecker
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class StructuralChecker:
|
|
52
|
+
"""
|
|
53
|
+
The structural-check stage of the admission pipeline (Check 2).
|
|
54
|
+
|
|
55
|
+
Verifies:
|
|
56
|
+
- Content-addressed id matches the delta's actual content hash
|
|
57
|
+
(always true by construction in our implementation, but we
|
|
58
|
+
verify in case of manual tampering).
|
|
59
|
+
- Fan-in bound: number of parents does not exceed max_parents.
|
|
60
|
+
- Size bound: serialized delta size does not exceed max_bytes.
|
|
61
|
+
|
|
62
|
+
A delta with missing parents is NOT rejected by this checker; it is
|
|
63
|
+
deferred by the CRDT layer (Section 3.3.2).
|
|
64
|
+
|
|
65
|
+
Reference: Figure 7 (step 3) of the paper.
|
|
66
|
+
"""
|
|
67
|
+
max_parents: int = 64
|
|
68
|
+
max_bytes: int = 1_048_576 # 1 MiB
|
|
69
|
+
|
|
70
|
+
def check(self, delta: Delta) -> tuple[bool, str]:
|
|
71
|
+
"""
|
|
72
|
+
Returns (ok, reason). ok=True iff all structural invariants hold.
|
|
73
|
+
"""
|
|
74
|
+
# 1. Content-addressed id matches the delta's actual content.
|
|
75
|
+
# (The id is a property derived from the delta, so it always
|
|
76
|
+
# matches. But we recompute to ensure consistency.)
|
|
77
|
+
expected_id = delta.id
|
|
78
|
+
if len(expected_id) != 64: # SHA-256 hex length
|
|
79
|
+
return False, f"invalid content hash length: {len(expected_id)}"
|
|
80
|
+
|
|
81
|
+
# 2. Fan-in bound.
|
|
82
|
+
if len(delta.parents) > self.max_parents:
|
|
83
|
+
return False, f"fan-in exceeds limit: {len(delta.parents)} > {self.max_parents}"
|
|
84
|
+
|
|
85
|
+
# 3. Size bound.
|
|
86
|
+
try:
|
|
87
|
+
serialized = json.dumps(delta.to_dict(), sort_keys=True).encode("utf-8")
|
|
88
|
+
except (TypeError, ValueError) as e:
|
|
89
|
+
return False, f"delta not JSON-serializable: {e}"
|
|
90
|
+
if len(serialized) > self.max_bytes:
|
|
91
|
+
return False, f"delta exceeds size limit: {len(serialized)} > {self.max_bytes}"
|
|
92
|
+
|
|
93
|
+
return True, "ok"
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherMesh — Cryptographic provenance (Pillar 3, Section 3.3.1).
|
|
3
|
+
|
|
4
|
+
Every delta carries the identifier of its originating agent and a
|
|
5
|
+
digital signature over the delta's content hash, which in turn covers
|
|
6
|
+
the delta body and its parent identifiers.
|
|
7
|
+
|
|
8
|
+
We use Ed25519 (Bernstein et al., 2012). The architecture does not
|
|
9
|
+
depend on a particular scheme but does depend on its unforgeability
|
|
10
|
+
(assumption A5).
|
|
11
|
+
|
|
12
|
+
Each node maintains a local registry mapping agent identifiers to
|
|
13
|
+
public keys. How the registry is bootstrapped, extended, and revoked
|
|
14
|
+
is a trust-establishment question that the paper does not settle
|
|
15
|
+
(Section 7.4).
|
|
16
|
+
|
|
17
|
+
This module provides:
|
|
18
|
+
|
|
19
|
+
- KeyRegistry: local mapping from origin -> public key
|
|
20
|
+
- generate_keypair: Ed25519 keypair generation
|
|
21
|
+
- sign_delta: sign a delta with a private key
|
|
22
|
+
- verify_delta_signature: verify a delta's signature
|
|
23
|
+
- ProvenanceChecker: the admission-stage check
|
|
24
|
+
|
|
25
|
+
Reference:
|
|
26
|
+
Baladi, S. (2026). AetherMesh v1.0.0. Section 3.3.1.
|
|
27
|
+
Zenodo. https://doi.org/10.5281/zenodo.24038038
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from typing import Dict, Optional, Tuple
|
|
34
|
+
|
|
35
|
+
from cryptography.exceptions import InvalidSignature
|
|
36
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
37
|
+
Ed25519PrivateKey,
|
|
38
|
+
Ed25519PublicKey,
|
|
39
|
+
)
|
|
40
|
+
from cryptography.hazmat.primitives import serialization
|
|
41
|
+
|
|
42
|
+
from aethermesh.models import Delta, Signature
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# =============================================================================
|
|
46
|
+
# Key types
|
|
47
|
+
# =============================================================================
|
|
48
|
+
|
|
49
|
+
PrivateKey = Ed25519PrivateKey
|
|
50
|
+
PublicKey = Ed25519PublicKey
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# Keypair generation
|
|
55
|
+
# =============================================================================
|
|
56
|
+
|
|
57
|
+
def generate_keypair() -> Tuple[PrivateKey, PublicKey]:
|
|
58
|
+
"""
|
|
59
|
+
Generate a fresh Ed25519 keypair.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
(private_key, public_key)
|
|
63
|
+
"""
|
|
64
|
+
private = Ed25519PrivateKey.generate()
|
|
65
|
+
public = private.public_key()
|
|
66
|
+
return private, public
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# KeyRegistry
|
|
71
|
+
# =============================================================================
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class KeyRegistry:
|
|
75
|
+
"""
|
|
76
|
+
Local mapping from agent identifier to public key.
|
|
77
|
+
|
|
78
|
+
This is the trust anchor for provenance (assumption A5). How the
|
|
79
|
+
registry is populated, extended, and revoked is out of scope for
|
|
80
|
+
this module (Section 7.4 of the paper).
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
keys: Map from origin id -> {key_id -> public key}.
|
|
84
|
+
"""
|
|
85
|
+
keys: Dict[str, Dict[str, PublicKey]] = field(default_factory=dict)
|
|
86
|
+
|
|
87
|
+
def register(self, origin: str, key_id: str, public_key: PublicKey) -> None:
|
|
88
|
+
"""Register a public key for an origin."""
|
|
89
|
+
self.keys.setdefault(origin, {})[key_id] = public_key
|
|
90
|
+
|
|
91
|
+
def lookup(self, origin: str, key_id: str) -> Optional[PublicKey]:
|
|
92
|
+
"""Look up a public key for an origin. Returns None if unknown."""
|
|
93
|
+
return self.keys.get(origin, {}).get(key_id)
|
|
94
|
+
|
|
95
|
+
def has(self, origin: str, key_id: str) -> bool:
|
|
96
|
+
return self.lookup(origin, key_id) is not None
|
|
97
|
+
|
|
98
|
+
def __len__(self) -> int:
|
|
99
|
+
return sum(len(v) for v in self.keys.values())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# Signing
|
|
104
|
+
# =============================================================================
|
|
105
|
+
|
|
106
|
+
def sign_delta(
|
|
107
|
+
delta: Delta,
|
|
108
|
+
private_key: PrivateKey,
|
|
109
|
+
key_id: str,
|
|
110
|
+
) -> Delta:
|
|
111
|
+
"""
|
|
112
|
+
Sign a delta with an Ed25519 private key.
|
|
113
|
+
|
|
114
|
+
The signature covers the delta's content hash (its id), which
|
|
115
|
+
itself covers the body, parents, timestamp, origin, and kind.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
delta: The unsigned delta.
|
|
119
|
+
private_key: The origin's Ed25519 private key.
|
|
120
|
+
key_id: An identifier for the public key (for lookup).
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
A copy of the delta with a Signature attached.
|
|
124
|
+
"""
|
|
125
|
+
message = delta.content_hash
|
|
126
|
+
signature_bytes = private_key.sign(message)
|
|
127
|
+
return delta.with_signature(
|
|
128
|
+
Signature(
|
|
129
|
+
origin=delta.origin,
|
|
130
|
+
key_id=key_id,
|
|
131
|
+
value=signature_bytes,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def verify_delta_signature(
|
|
137
|
+
delta: Delta,
|
|
138
|
+
public_key: PublicKey,
|
|
139
|
+
) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Verify a delta's signature against a public key.
|
|
142
|
+
|
|
143
|
+
Returns True iff the signature is valid over the delta's content hash.
|
|
144
|
+
"""
|
|
145
|
+
if delta.signature is None:
|
|
146
|
+
return False
|
|
147
|
+
try:
|
|
148
|
+
public_key.verify(delta.signature.value, delta.content_hash)
|
|
149
|
+
return True
|
|
150
|
+
except InvalidSignature:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# =============================================================================
|
|
155
|
+
# ProvenanceChecker
|
|
156
|
+
# =============================================================================
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class ProvenanceChecker:
|
|
160
|
+
"""
|
|
161
|
+
The provenance-check stage of the admission pipeline (Check 1).
|
|
162
|
+
|
|
163
|
+
Verifies that a delta has:
|
|
164
|
+
- A signature attached.
|
|
165
|
+
- A registered key for its origin.
|
|
166
|
+
- A valid signature over the content hash.
|
|
167
|
+
|
|
168
|
+
Reference: Figure 7 (step 2) and Table (Section 3.3.4) of the paper.
|
|
169
|
+
"""
|
|
170
|
+
registry: KeyRegistry
|
|
171
|
+
|
|
172
|
+
def check(self, delta: Delta) -> Tuple[bool, str]:
|
|
173
|
+
"""
|
|
174
|
+
Returns (ok, reason). ok=True iff all provenance checks pass.
|
|
175
|
+
"""
|
|
176
|
+
if delta.signature is None:
|
|
177
|
+
return False, "no signature attached"
|
|
178
|
+
if delta.signature.origin != delta.origin:
|
|
179
|
+
return False, "signature origin does not match delta origin"
|
|
180
|
+
public_key = self.registry.lookup(delta.origin, delta.signature.key_id)
|
|
181
|
+
if public_key is None:
|
|
182
|
+
return False, f"unknown key: origin={delta.origin} key_id={delta.signature.key_id}"
|
|
183
|
+
if not verify_delta_signature(delta, public_key):
|
|
184
|
+
return False, "signature verification failed"
|
|
185
|
+
return True, "ok"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# =============================================================================
|
|
189
|
+
# Convenience: serialize/deserialize keys
|
|
190
|
+
# =============================================================================
|
|
191
|
+
|
|
192
|
+
def export_public_key(public_key: PublicKey) -> bytes:
|
|
193
|
+
"""Export a public key as raw bytes (32 bytes)."""
|
|
194
|
+
return public_key.public_bytes(
|
|
195
|
+
encoding=serialization.Encoding.Raw,
|
|
196
|
+
format=serialization.PublicFormat.Raw,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def import_public_key(raw: bytes) -> PublicKey:
|
|
201
|
+
"""Import a public key from raw bytes (32 bytes)."""
|
|
202
|
+
return Ed25519PublicKey.from_public_bytes(raw)
|