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 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,9 @@
1
+ """AetherMesh version information."""
2
+
3
+ __version__ = "2.0.0"
4
+ __author__ = "Samir Baladi"
5
+ __email__ = "gitdeeper@gmail.com"
6
+ __doi__ = "10.5281/zenodo.24038038"
7
+ __license__ = "MIT"
8
+ __paper_version__ = "1.0.0"
9
+ __record_id__ = "AETHERMESH-02"
@@ -0,0 +1,13 @@
1
+ """AetherMesh — Admission pipeline (Pillar 3 integration)."""
2
+
3
+ from aethermesh.admission.pipeline import (
4
+ AdmissionPipeline,
5
+ PipelineConfig,
6
+ StageResult,
7
+ )
8
+
9
+ __all__ = [
10
+ "AdmissionPipeline",
11
+ "PipelineConfig",
12
+ "StageResult",
13
+ ]
@@ -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)