zexus 1.6.8 → 1.7.2

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.
Files changed (177) hide show
  1. package/README.md +12 -5
  2. package/package.json +1 -1
  3. package/src/__init__.py +7 -0
  4. package/src/zexus/__init__.py +1 -1
  5. package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
  7. package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
  8. package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
  9. package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
  10. package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
  11. package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
  12. package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
  13. package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
  14. package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
  15. package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
  16. package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
  17. package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
  18. package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  19. package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
  20. package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
  22. package/src/zexus/advanced_types.py +17 -2
  23. package/src/zexus/blockchain/__init__.py +411 -0
  24. package/src/zexus/blockchain/accelerator.py +1160 -0
  25. package/src/zexus/blockchain/chain.py +660 -0
  26. package/src/zexus/blockchain/consensus.py +821 -0
  27. package/src/zexus/blockchain/contract_vm.py +1019 -0
  28. package/src/zexus/blockchain/crypto.py +79 -14
  29. package/src/zexus/blockchain/events.py +526 -0
  30. package/src/zexus/blockchain/loadtest.py +721 -0
  31. package/src/zexus/blockchain/monitoring.py +350 -0
  32. package/src/zexus/blockchain/mpt.py +716 -0
  33. package/src/zexus/blockchain/multichain.py +951 -0
  34. package/src/zexus/blockchain/multiprocess_executor.py +338 -0
  35. package/src/zexus/blockchain/network.py +886 -0
  36. package/src/zexus/blockchain/node.py +666 -0
  37. package/src/zexus/blockchain/rpc.py +1203 -0
  38. package/src/zexus/blockchain/rust_bridge.py +421 -0
  39. package/src/zexus/blockchain/storage.py +423 -0
  40. package/src/zexus/blockchain/tokens.py +750 -0
  41. package/src/zexus/blockchain/upgradeable.py +1004 -0
  42. package/src/zexus/blockchain/verification.py +1602 -0
  43. package/src/zexus/blockchain/wallet.py +621 -0
  44. package/src/zexus/capability_system.py +184 -9
  45. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  46. package/src/zexus/cli/main.py +383 -34
  47. package/src/zexus/cli/zpm.py +1 -1
  48. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  53. package/src/zexus/compiler/bytecode.py +124 -7
  54. package/src/zexus/compiler/compat_runtime.py +6 -2
  55. package/src/zexus/compiler/lexer.py +16 -5
  56. package/src/zexus/compiler/parser.py +108 -7
  57. package/src/zexus/compiler/semantic.py +18 -19
  58. package/src/zexus/compiler/zexus_ast.py +26 -1
  59. package/src/zexus/concurrency_system.py +79 -0
  60. package/src/zexus/config.py +54 -0
  61. package/src/zexus/crypto_bridge.py +244 -8
  62. package/src/zexus/dap/__init__.py +10 -0
  63. package/src/zexus/dap/__main__.py +4 -0
  64. package/src/zexus/dap/dap_server.py +391 -0
  65. package/src/zexus/dap/debug_engine.py +298 -0
  66. package/src/zexus/environment.py +112 -9
  67. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  70. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  71. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  72. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  73. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  74. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  75. package/src/zexus/evaluator/bytecode_compiler.py +457 -37
  76. package/src/zexus/evaluator/core.py +644 -50
  77. package/src/zexus/evaluator/expressions.py +358 -62
  78. package/src/zexus/evaluator/functions.py +458 -20
  79. package/src/zexus/evaluator/resource_limiter.py +4 -4
  80. package/src/zexus/evaluator/statements.py +774 -122
  81. package/src/zexus/evaluator/unified_execution.py +573 -72
  82. package/src/zexus/evaluator/utils.py +14 -2
  83. package/src/zexus/evaluator_original.py +1 -1
  84. package/src/zexus/event_loop.py +186 -0
  85. package/src/zexus/lexer.py +742 -458
  86. package/src/zexus/lsp/__init__.py +1 -1
  87. package/src/zexus/lsp/definition_provider.py +163 -9
  88. package/src/zexus/lsp/server.py +22 -8
  89. package/src/zexus/lsp/symbol_provider.py +182 -9
  90. package/src/zexus/module_cache.py +239 -9
  91. package/src/zexus/module_manager.py +129 -1
  92. package/src/zexus/object.py +76 -6
  93. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  94. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  95. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  96. package/src/zexus/parser/parser.py +1349 -408
  97. package/src/zexus/parser/strategy_context.py +755 -58
  98. package/src/zexus/parser/strategy_structural.py +121 -21
  99. package/src/zexus/persistence.py +15 -1
  100. package/src/zexus/renderer/__init__.py +61 -0
  101. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  103. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  104. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  105. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  106. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  107. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  108. package/src/zexus/renderer/backend.py +261 -0
  109. package/src/zexus/renderer/canvas.py +78 -0
  110. package/src/zexus/renderer/color_system.py +201 -0
  111. package/src/zexus/renderer/graphics.py +31 -0
  112. package/src/zexus/renderer/layout.py +222 -0
  113. package/src/zexus/renderer/main_renderer.py +66 -0
  114. package/src/zexus/renderer/painter.py +30 -0
  115. package/src/zexus/renderer/tk_backend.py +208 -0
  116. package/src/zexus/renderer/web_backend.py +260 -0
  117. package/src/zexus/runtime/__init__.py +10 -2
  118. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  119. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  120. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  121. package/src/zexus/runtime/file_flags.py +137 -0
  122. package/src/zexus/runtime/load_manager.py +368 -0
  123. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  124. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  125. package/src/zexus/security.py +424 -34
  126. package/src/zexus/stdlib/fs.py +23 -18
  127. package/src/zexus/stdlib/http.py +289 -186
  128. package/src/zexus/stdlib/sockets.py +207 -163
  129. package/src/zexus/stdlib/websockets.py +282 -0
  130. package/src/zexus/stdlib_integration.py +369 -2
  131. package/src/zexus/strategy_recovery.py +6 -3
  132. package/src/zexus/type_checker.py +423 -0
  133. package/src/zexus/virtual_filesystem.py +189 -2
  134. package/src/zexus/vm/__init__.py +113 -3
  135. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  136. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  137. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  138. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  139. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  140. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  141. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  142. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  143. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  144. package/src/zexus/vm/async_optimizer.py +80 -6
  145. package/src/zexus/vm/binary_bytecode.py +659 -0
  146. package/src/zexus/vm/bytecode.py +59 -11
  147. package/src/zexus/vm/bytecode_converter.py +26 -12
  148. package/src/zexus/vm/cabi.c +1985 -0
  149. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  150. package/src/zexus/vm/cabi.h +127 -0
  151. package/src/zexus/vm/cache.py +561 -17
  152. package/src/zexus/vm/compiler.py +818 -51
  153. package/src/zexus/vm/fastops.c +15743 -0
  154. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  155. package/src/zexus/vm/fastops.pyx +288 -0
  156. package/src/zexus/vm/gas_metering.py +50 -9
  157. package/src/zexus/vm/jit.py +364 -20
  158. package/src/zexus/vm/native_jit_backend.py +1816 -0
  159. package/src/zexus/vm/native_runtime.cpp +1388 -0
  160. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  161. package/src/zexus/vm/optimizer.py +161 -11
  162. package/src/zexus/vm/parallel_vm.py +140 -45
  163. package/src/zexus/vm/peephole_optimizer.py +82 -4
  164. package/src/zexus/vm/profiler.py +38 -18
  165. package/src/zexus/vm/register_allocator.py +16 -5
  166. package/src/zexus/vm/register_vm.py +8 -5
  167. package/src/zexus/vm/vm.py +3581 -531
  168. package/src/zexus/vm/wasm_compiler.py +658 -0
  169. package/src/zexus/zexus_ast.py +137 -11
  170. package/src/zexus/zexus_token.py +16 -5
  171. package/src/zexus/zpm/installer.py +55 -15
  172. package/src/zexus/zpm/package_manager.py +1 -1
  173. package/src/zexus/zpm/registry.py +257 -28
  174. package/src/zexus.egg-info/PKG-INFO +16 -6
  175. package/src/zexus.egg-info/SOURCES.txt +129 -17
  176. package/src/zexus.egg-info/entry_points.txt +1 -0
  177. package/src/zexus.egg-info/requires.txt +4 -0
@@ -0,0 +1,886 @@
1
+ """
2
+ Zexus Blockchain — Peer-to-Peer Networking Layer
3
+
4
+ Provides peer discovery, connection management, and message propagation
5
+ for the Zexus blockchain network. Built on top of the existing asyncio
6
+ TCP/WebSocket infrastructure in ``stdlib.sockets``.
7
+
8
+ Protocol messages are JSON-encoded with a type field and optional payload.
9
+ """
10
+
11
+ import asyncio
12
+ import hashlib
13
+ import json
14
+ import random
15
+ import ssl
16
+ import threading
17
+ import time
18
+ import logging
19
+ import os
20
+ from dataclasses import dataclass, field, asdict
21
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple
22
+
23
+ logger = logging.getLogger("zexus.blockchain.network")
24
+
25
+
26
+ # ── TLS helpers ────────────────────────────────────────────────────────
27
+
28
+ def _generate_self_signed_cert(cert_path: str, key_path: str):
29
+ """Generate a self-signed TLS certificate for node identity.
30
+
31
+ Uses the ``cryptography`` library (already a dependency) to produce
32
+ an ECDSA P-256 keypair and an X.509 certificate valid for 10 years.
33
+ The certificate's Subject is set to the key's SHA-256 fingerprint so
34
+ it doubles as a verifiable node identity.
35
+ """
36
+ from cryptography import x509
37
+ from cryptography.x509.oid import NameOID
38
+ from cryptography.hazmat.primitives import hashes, serialization
39
+ from cryptography.hazmat.primitives.asymmetric import ec
40
+ from cryptography.hazmat.backends import default_backend
41
+ import datetime
42
+
43
+ key = ec.generate_private_key(ec.SECP256R1(), default_backend())
44
+
45
+ # Derive a human-readable CN from the public key fingerprint
46
+ pub_bytes = key.public_key().public_bytes(
47
+ serialization.Encoding.DER,
48
+ serialization.PublicFormat.SubjectPublicKeyInfo,
49
+ )
50
+ fingerprint = hashlib.sha256(pub_bytes).hexdigest()[:16]
51
+ subject = issuer = x509.Name([
52
+ x509.NameAttribute(NameOID.COMMON_NAME, f"zexus-node-{fingerprint}"),
53
+ ])
54
+
55
+ cert = (
56
+ x509.CertificateBuilder()
57
+ .subject_name(subject)
58
+ .issuer_name(issuer)
59
+ .public_key(key.public_key())
60
+ .serial_number(x509.random_serial_number())
61
+ .not_valid_before(datetime.datetime.utcnow())
62
+ .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=3650))
63
+ .sign(key, hashes.SHA256(), default_backend())
64
+ )
65
+
66
+ os.makedirs(os.path.dirname(cert_path) or ".", exist_ok=True)
67
+
68
+ with open(key_path, "wb") as f:
69
+ f.write(key.private_bytes(
70
+ serialization.Encoding.PEM,
71
+ serialization.PrivateFormat.TraditionalOpenSSL,
72
+ serialization.NoEncryption(),
73
+ ))
74
+
75
+ with open(cert_path, "wb") as f:
76
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
77
+
78
+ return cert_path, key_path
79
+
80
+
81
+ def _make_server_ssl_context(
82
+ cert_path: str,
83
+ key_path: str,
84
+ ca_cert_path: Optional[str] = None,
85
+ require_client_cert: bool = False,
86
+ ) -> ssl.SSLContext:
87
+ """Create a TLS server context with optional mutual-TLS (mTLS).
88
+
89
+ Parameters
90
+ ----------
91
+ cert_path : str
92
+ Path to the server certificate (PEM).
93
+ key_path : str
94
+ Path to the server private key (PEM).
95
+ ca_cert_path : str, optional
96
+ Path to a CA bundle. When provided, the server verifies peer
97
+ certificates against this CA — enabling proper CA-signed
98
+ certificate chains instead of self-signed only.
99
+ require_client_cert : bool
100
+ When *True*, the server demands a valid client certificate
101
+ (mutual TLS). Only effective when ``ca_cert_path`` is set.
102
+ """
103
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
104
+ ctx.minimum_version = ssl.TLSVersion.TLSv1_2
105
+ ctx.load_cert_chain(cert_path, key_path)
106
+
107
+ if ca_cert_path and os.path.isfile(ca_cert_path):
108
+ ctx.load_verify_locations(ca_cert_path)
109
+ if require_client_cert:
110
+ ctx.verify_mode = ssl.CERT_REQUIRED
111
+ else:
112
+ ctx.verify_mode = ssl.CERT_OPTIONAL
113
+ logger.info("Server TLS: CA-signed verification enabled (mTLS=%s)", require_client_cert)
114
+ else:
115
+ ctx.verify_mode = ssl.CERT_NONE
116
+
117
+ ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:!aNULL:!MD5:!DSS")
118
+ return ctx
119
+
120
+
121
+ def _make_client_ssl_context(
122
+ cert_path: Optional[str] = None,
123
+ key_path: Optional[str] = None,
124
+ ca_cert_path: Optional[str] = None,
125
+ ) -> ssl.SSLContext:
126
+ """Create a TLS client context with optional CA verification.
127
+
128
+ Parameters
129
+ ----------
130
+ cert_path : str, optional
131
+ Client certificate for mutual TLS.
132
+ key_path : str, optional
133
+ Client private key for mutual TLS.
134
+ ca_cert_path : str, optional
135
+ CA bundle. When provided the client verifies the server's
136
+ certificate against known CA roots — enabling production-grade
137
+ certificate validation instead of trusting all self-signed certs.
138
+ """
139
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
140
+ ctx.minimum_version = ssl.TLSVersion.TLSv1_2
141
+
142
+ if ca_cert_path and os.path.isfile(ca_cert_path):
143
+ ctx.load_verify_locations(ca_cert_path)
144
+ ctx.check_hostname = False # P2P nodes don't use DNS hostnames
145
+ ctx.verify_mode = ssl.CERT_REQUIRED
146
+ logger.info("Client TLS: CA-signed server verification enabled")
147
+ else:
148
+ ctx.check_hostname = False
149
+ ctx.verify_mode = ssl.CERT_NONE # fallback: accept self-signed
150
+
151
+ if cert_path and key_path:
152
+ ctx.load_cert_chain(cert_path, key_path)
153
+
154
+ ctx.set_ciphers("ECDHE+AESGCM:ECDHE+CHACHA20:!aNULL:!MD5:!DSS")
155
+ return ctx
156
+
157
+
158
+ # ── Message types ──────────────────────────────────────────────────────────
159
+
160
+ class MessageType:
161
+ """Protocol message types."""
162
+ # Discovery
163
+ PING = "ping"
164
+ PONG = "pong"
165
+ FIND_PEERS = "find_peers"
166
+ PEERS = "peers"
167
+ # Chain sync
168
+ GET_BLOCKS = "get_blocks"
169
+ BLOCKS = "blocks"
170
+ GET_HEADERS = "get_headers"
171
+ HEADERS = "headers"
172
+ NEW_BLOCK = "new_block"
173
+ # Transactions
174
+ NEW_TX = "new_tx"
175
+ GET_TX = "get_tx"
176
+ TX = "tx"
177
+ # Consensus
178
+ VOTE = "vote"
179
+ PROPOSE = "propose"
180
+ # General
181
+ HANDSHAKE = "handshake"
182
+ DISCONNECT = "disconnect"
183
+ ERROR = "error"
184
+
185
+
186
+ @dataclass
187
+ class Message:
188
+ """Network protocol message."""
189
+ type: str
190
+ payload: Dict[str, Any] = field(default_factory=dict)
191
+ sender: str = ""
192
+ nonce: str = ""
193
+ timestamp: float = 0.0
194
+
195
+ def __post_init__(self):
196
+ self.timestamp = self.timestamp or time.time()
197
+ self.nonce = self.nonce or hashlib.sha256(
198
+ f"{self.type}{self.timestamp}{random.random()}".encode()
199
+ ).hexdigest()[:16]
200
+
201
+ def encode(self) -> bytes:
202
+ """Serialize to JSON bytes."""
203
+ return json.dumps(asdict(self)).encode("utf-8")
204
+
205
+ @staticmethod
206
+ def decode(data: bytes) -> 'Message':
207
+ """Deserialize from JSON bytes."""
208
+ d = json.loads(data.decode("utf-8"))
209
+ return Message(**d)
210
+
211
+ def to_dict(self) -> Dict[str, Any]:
212
+ return asdict(self)
213
+
214
+
215
+ @dataclass
216
+ class PeerInfo:
217
+ """Information about a network peer."""
218
+ peer_id: str = ""
219
+ host: str = ""
220
+ port: int = 0
221
+ chain_id: str = ""
222
+ height: int = 0
223
+ version: str = "1.0.0"
224
+ last_seen: float = 0.0
225
+ latency_ms: float = 0.0
226
+ reputation: int = 100 # 0-100 score
227
+ connected: bool = False
228
+
229
+ @property
230
+ def address(self) -> str:
231
+ return f"{self.host}:{self.port}"
232
+
233
+
234
+ class PeerReputationManager:
235
+ """Track and enforce peer reputation to resist Sybil and DoS attacks.
236
+
237
+ Every peer starts at score 100 (maximum). Good behaviour (valid blocks,
238
+ valid transactions) earns points; bad behaviour (invalid messages, spam,
239
+ protocol violations) costs points. Peers whose score drops to 0 are
240
+ banned for ``ban_duration`` seconds.
241
+
242
+ The manager also enforces per-peer message rate limiting.
243
+ """
244
+
245
+ # ── Reputation deltas ──────────────────────────────────────────
246
+ VALID_BLOCK = 5
247
+ VALID_TX = 1
248
+ INVALID_BLOCK = -20
249
+ INVALID_TX = -10
250
+ PROTOCOL_VIOLATION = -25
251
+ TIMEOUT = -5
252
+ SPAM = -15
253
+ SUCCESSFUL_SYNC = 10
254
+
255
+ def __init__(self, ban_duration: float = 3600.0,
256
+ rate_limit: int = 100,
257
+ rate_window: float = 60.0):
258
+ self.ban_duration = ban_duration # seconds
259
+ self.rate_limit = rate_limit # msgs per window
260
+ self.rate_window = rate_window # seconds
261
+ self._bans: Dict[str, float] = {} # peer_id -> unban timestamp
262
+ self._msg_counts: Dict[str, List[float]] = {} # peer_id -> [timestamps]
263
+
264
+ def update(self, peer: PeerInfo, delta: int, reason: str = "") -> int:
265
+ """Adjust a peer's reputation score.
266
+
267
+ Returns the new score. If it drops to 0, the peer is banned.
268
+ """
269
+ old = peer.reputation
270
+ peer.reputation = max(0, min(100, peer.reputation + delta))
271
+ if reason:
272
+ logger.debug("Reputation %s: %d -> %d (%s)", peer.peer_id[:8], old, peer.reputation, reason)
273
+ if peer.reputation == 0:
274
+ self.ban(peer.peer_id)
275
+ return peer.reputation
276
+
277
+ def ban(self, peer_id: str):
278
+ """Ban a peer for ``ban_duration`` seconds."""
279
+ self._bans[peer_id] = time.time() + self.ban_duration
280
+ logger.warning("Peer %s BANNED for %ds", peer_id[:8], int(self.ban_duration))
281
+
282
+ def unban(self, peer_id: str):
283
+ """Manually unban a peer."""
284
+ self._bans.pop(peer_id, None)
285
+
286
+ def is_banned(self, peer_id: str) -> bool:
287
+ """Check if a peer is currently banned."""
288
+ if peer_id not in self._bans:
289
+ return False
290
+ if time.time() >= self._bans[peer_id]:
291
+ del self._bans[peer_id]
292
+ return False
293
+ return True
294
+
295
+ def check_rate_limit(self, peer_id: str) -> bool:
296
+ """Check if a peer has exceeded the message rate limit.
297
+
298
+ Returns True if the message should be allowed, False if rate-limited.
299
+ """
300
+ now = time.time()
301
+ timestamps = self._msg_counts.setdefault(peer_id, [])
302
+ # Prune old entries
303
+ cutoff = now - self.rate_window
304
+ self._msg_counts[peer_id] = [t for t in timestamps if t > cutoff]
305
+ timestamps = self._msg_counts[peer_id]
306
+
307
+ if len(timestamps) >= self.rate_limit:
308
+ return False # Rate limited
309
+ timestamps.append(now)
310
+ return True
311
+
312
+ def get_banned_peers(self) -> List[str]:
313
+ """Return list of currently banned peer IDs."""
314
+ now = time.time()
315
+ # Prune expired bans
316
+ self._bans = {pid: ts for pid, ts in self._bans.items() if ts > now}
317
+ return list(self._bans.keys())
318
+
319
+
320
+ class PeerConnection:
321
+ """Manages a single peer connection with send/receive capabilities."""
322
+
323
+ def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter,
324
+ peer_info: PeerInfo, is_inbound: bool = False):
325
+ self.reader = reader
326
+ self.writer = writer
327
+ self.peer_info = peer_info
328
+ self.is_inbound = is_inbound
329
+ self._closed = False
330
+ self._recv_task: Optional[asyncio.Task] = None
331
+
332
+ @property
333
+ def is_connected(self) -> bool:
334
+ return not self._closed and self.writer is not None
335
+
336
+ async def send(self, msg: Message) -> bool:
337
+ """Send a message to this peer."""
338
+ if self._closed:
339
+ return False
340
+ try:
341
+ data = msg.encode()
342
+ length = len(data)
343
+ self.writer.write(length.to_bytes(4, "big") + data)
344
+ await self.writer.drain()
345
+ return True
346
+ except (ConnectionError, OSError, asyncio.CancelledError):
347
+ self._closed = True
348
+ return False
349
+
350
+ async def receive(self) -> Optional[Message]:
351
+ """Receive a single message from this peer."""
352
+ if self._closed:
353
+ return None
354
+ try:
355
+ length_bytes = await self.reader.readexactly(4)
356
+ length = int.from_bytes(length_bytes, "big")
357
+ if length > 10 * 1024 * 1024: # 10 MB max message size
358
+ logger.warning("Message too large from %s: %d bytes", self.peer_info.address, length)
359
+ self._closed = True
360
+ return None
361
+ data = await self.reader.readexactly(length)
362
+ return Message.decode(data)
363
+ except (ConnectionError, asyncio.IncompleteReadError, asyncio.CancelledError):
364
+ self._closed = True
365
+ return None
366
+
367
+ async def close(self):
368
+ """Close the connection."""
369
+ self._closed = True
370
+ if self.writer:
371
+ try:
372
+ self.writer.close()
373
+ await self.writer.wait_closed()
374
+ except Exception:
375
+ pass
376
+
377
+
378
+ class P2PNetwork:
379
+ """Peer-to-peer network manager for the Zexus blockchain.
380
+
381
+ Handles:
382
+ - Listening for inbound connections
383
+ - Connecting to peers
384
+ - Peer discovery and management
385
+ - Message routing and broadcasting
386
+ - Seen-message deduplication
387
+ """
388
+
389
+ def __init__(self, host: str = "0.0.0.0", port: int = 30303,
390
+ chain_id: str = "zexus-mainnet", node_id: str = "",
391
+ max_peers: int = 25, min_peers: int = 3,
392
+ tls_cert: Optional[str] = None, tls_key: Optional[str] = None,
393
+ tls_ca: Optional[str] = None,
394
+ tls_enabled: bool = True,
395
+ tls_mutual: bool = False,
396
+ tls_pinned_certs: Optional[List[str]] = None,
397
+ data_dir: Optional[str] = None):
398
+ self.host = host
399
+ self.port = port
400
+ self.chain_id = chain_id
401
+ self.node_id = node_id or hashlib.sha256(
402
+ f"{host}:{port}:{time.time()}:{random.random()}".encode()
403
+ ).hexdigest()[:40]
404
+ self.max_peers = max_peers
405
+ self.min_peers = min_peers
406
+
407
+ # ── TLS configuration ────────────────────────────────────────
408
+ self.tls_enabled = tls_enabled
409
+ self.tls_mutual = tls_mutual
410
+ self._server_ssl: Optional[ssl.SSLContext] = None
411
+ self._client_ssl: Optional[ssl.SSLContext] = None
412
+
413
+ # Certificate pinning: set of SHA-256 fingerprints of trusted
414
+ # peer certificates. If non-empty, only peers whose TLS cert
415
+ # fingerprint is in this set are accepted.
416
+ self._pinned_certs: Set[str] = set(tls_pinned_certs or [])
417
+
418
+ if tls_enabled:
419
+ # Auto-generate certs if none provided
420
+ _data = data_dir or os.path.join(os.path.expanduser("~"), ".zexus", "tls")
421
+ self._cert_path = tls_cert or os.path.join(_data, "node.crt")
422
+ self._key_path = tls_key or os.path.join(_data, "node.key")
423
+ self._ca_path = tls_ca # None = self-signed mode
424
+
425
+ if not (os.path.exists(self._cert_path) and os.path.exists(self._key_path)):
426
+ logger.info("Generating TLS certificate for node identity...")
427
+ _generate_self_signed_cert(self._cert_path, self._key_path)
428
+
429
+ self._server_ssl = _make_server_ssl_context(
430
+ self._cert_path, self._key_path,
431
+ ca_cert_path=self._ca_path,
432
+ require_client_cert=self.tls_mutual,
433
+ )
434
+ self._client_ssl = _make_client_ssl_context(
435
+ self._cert_path, self._key_path,
436
+ ca_cert_path=self._ca_path,
437
+ )
438
+ mode = "mTLS" if tls_mutual else ("CA-verified" if tls_ca else "self-signed")
439
+ logger.info("TLS enabled (%s) — all P2P traffic is encrypted", mode)
440
+
441
+ # Connection state
442
+ self.peers: Dict[str, PeerConnection] = {} # peer_id -> connection
443
+ self.known_peers: Dict[str, PeerInfo] = {} # peer_id -> info (includes disconnected)
444
+ self.bootstrap_nodes: List[Tuple[str, int]] = []
445
+
446
+ # Message handling
447
+ self._handlers: Dict[str, List[Callable]] = {}
448
+ self._seen_messages: Set[str] = set()
449
+ self._seen_max = 10_000
450
+
451
+ # Sybil / DoS resistance
452
+ self.reputation = PeerReputationManager()
453
+
454
+ # Server state
455
+ self._server: Optional[asyncio.AbstractServer] = None
456
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
457
+ self._running = False
458
+ self._recv_tasks: Dict[str, asyncio.Task] = {}
459
+
460
+ # ── Event handler registration ─────────────────────────────────────
461
+
462
+ def on(self, msg_type: str, handler: Callable):
463
+ """Register a handler for a message type."""
464
+ self._handlers.setdefault(msg_type, []).append(handler)
465
+
466
+ def off(self, msg_type: str, handler: Optional[Callable] = None):
467
+ """Remove handler(s) for a message type."""
468
+ if handler is None:
469
+ self._handlers.pop(msg_type, None)
470
+ elif msg_type in self._handlers:
471
+ self._handlers[msg_type] = [h for h in self._handlers[msg_type] if h != handler]
472
+
473
+ async def _dispatch(self, msg: Message, conn: PeerConnection):
474
+ """Dispatch a received message to registered handlers.
475
+
476
+ Enforces rate-limiting and reputation checks before dispatch.
477
+ """
478
+ peer_id = conn.peer_info.peer_id
479
+
480
+ # Block banned peers
481
+ if self.reputation.is_banned(peer_id):
482
+ logger.debug("Dropping message from banned peer %s", peer_id[:8])
483
+ return
484
+
485
+ # Rate-limit check
486
+ if not self.reputation.check_rate_limit(peer_id):
487
+ self.reputation.update(conn.peer_info, PeerReputationManager.SPAM,
488
+ reason="rate limit exceeded")
489
+ logger.warning("Rate-limited peer %s", peer_id[:8])
490
+ if self.reputation.is_banned(peer_id):
491
+ await self.disconnect_peer(peer_id)
492
+ return
493
+
494
+ handlers = self._handlers.get(msg.type, [])
495
+ for handler in handlers:
496
+ try:
497
+ result = handler(msg, conn)
498
+ if asyncio.iscoroutine(result):
499
+ await result
500
+ except Exception as e:
501
+ logger.error("Handler error for %s: %s", msg.type, e)
502
+
503
+ # ── Server lifecycle ───────────────────────────────────────────────
504
+
505
+ async def start(self):
506
+ """Start listening for inbound connections."""
507
+ if self._running:
508
+ return
509
+ self._loop = asyncio.get_event_loop()
510
+ self._server = await asyncio.start_server(
511
+ self._handle_inbound, self.host, self.port,
512
+ ssl=self._server_ssl, # None when TLS disabled → plain TCP
513
+ )
514
+ self._running = True
515
+ tls_status = "TLS" if self.tls_enabled else "plaintext"
516
+ logger.info("P2P listening on %s:%d (%s, node_id=%s)",
517
+ self.host, self.port, tls_status, self.node_id[:8])
518
+
519
+ # Register built-in handlers
520
+ self.on(MessageType.PING, self._handle_ping)
521
+ self.on(MessageType.FIND_PEERS, self._handle_find_peers)
522
+ self.on(MessageType.HANDSHAKE, self._handle_handshake_msg)
523
+
524
+ # Bootstrap connections
525
+ asyncio.ensure_future(self._bootstrap())
526
+
527
+ async def stop(self):
528
+ """Stop the P2P network."""
529
+ self._running = False
530
+ # Close all peer connections
531
+ for peer_id, conn in list(self.peers.items()):
532
+ await conn.send(Message(type=MessageType.DISCONNECT, sender=self.node_id))
533
+ await conn.close()
534
+ self.peers.clear()
535
+
536
+ # Cancel receive tasks
537
+ for task in self._recv_tasks.values():
538
+ task.cancel()
539
+ self._recv_tasks.clear()
540
+
541
+ # Stop server
542
+ if self._server:
543
+ self._server.close()
544
+ await self._server.wait_closed()
545
+ self._server = None
546
+ logger.info("P2P network stopped")
547
+
548
+ @property
549
+ def peer_count(self) -> int:
550
+ return len(self.peers)
551
+
552
+ @property
553
+ def is_running(self) -> bool:
554
+ return self._running
555
+
556
+ # ── Certificate pinning ──────────────────────────────────────────
557
+
558
+ def _check_cert_pin(self, writer: asyncio.StreamWriter) -> bool:
559
+ """Verify the peer's TLS certificate fingerprint against the
560
+ pinned set. Returns *True* if pinning is disabled or if the
561
+ cert is in the pinned set; *False* to reject the connection."""
562
+ if not self._pinned_certs:
563
+ return True # pinning not configured — accept all
564
+ ssl_obj = writer.get_extra_info("ssl_object")
565
+ if ssl_obj is None:
566
+ return True # non-TLS — nothing to pin
567
+ try:
568
+ der = ssl_obj.getpeercert(binary_form=True)
569
+ if der is None:
570
+ return False # no cert presented
571
+ fp = hashlib.sha256(der).hexdigest()
572
+ if fp in self._pinned_certs:
573
+ return True
574
+ logger.warning("Certificate pin mismatch: %s", fp)
575
+ return False
576
+ except Exception:
577
+ return False
578
+
579
+ # ── Connections ────────────────────────────────────────────────────
580
+
581
+ async def _handle_inbound(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
582
+ """Handle a new inbound TCP connection."""
583
+ addr = writer.get_extra_info("peername")
584
+ logger.debug("Inbound connection from %s", addr)
585
+
586
+ if len(self.peers) >= self.max_peers:
587
+ writer.close()
588
+ return
589
+
590
+ # Certificate pinning check
591
+ if not self._check_cert_pin(writer):
592
+ logger.warning("Rejected inbound connection (cert pin mismatch) from %s", addr)
593
+ writer.close()
594
+ return
595
+
596
+ # Create temporary peer info, will be updated on handshake
597
+ temp_info = PeerInfo(host=addr[0] if addr else "unknown", port=addr[1] if addr else 0)
598
+ conn = PeerConnection(reader, writer, temp_info, is_inbound=True)
599
+
600
+ # Send our handshake
601
+ await conn.send(Message(
602
+ type=MessageType.HANDSHAKE,
603
+ sender=self.node_id,
604
+ payload={
605
+ "chain_id": self.chain_id,
606
+ "port": self.port,
607
+ "version": "1.0.0",
608
+ }
609
+ ))
610
+
611
+ # Wait for handshake response with timeout
612
+ try:
613
+ msg = await asyncio.wait_for(conn.receive(), timeout=10.0)
614
+ except asyncio.TimeoutError:
615
+ await conn.close()
616
+ return
617
+
618
+ if not msg or msg.type != MessageType.HANDSHAKE:
619
+ await conn.close()
620
+ return
621
+
622
+ peer_id = msg.sender
623
+ if peer_id == self.node_id or peer_id in self.peers:
624
+ await conn.close()
625
+ return
626
+
627
+ # Reject banned peers
628
+ if self.reputation.is_banned(peer_id):
629
+ logger.info("Rejected banned peer %s", peer_id[:8])
630
+ await conn.close()
631
+ return
632
+ await conn.close()
633
+ return
634
+
635
+ if not msg or msg.type != MessageType.HANDSHAKE:
636
+ await conn.close()
637
+ return
638
+
639
+ peer_id = msg.sender
640
+ if peer_id == self.node_id or peer_id in self.peers:
641
+ await conn.close()
642
+ return
643
+
644
+ # Accept the peer
645
+ conn.peer_info.peer_id = peer_id
646
+ conn.peer_info.chain_id = msg.payload.get("chain_id", "")
647
+ conn.peer_info.connected = True
648
+ conn.peer_info.last_seen = time.time()
649
+ self.peers[peer_id] = conn
650
+ self.known_peers[peer_id] = conn.peer_info
651
+ logger.info("Peer connected (inbound): %s", peer_id[:8])
652
+
653
+ # Start receive loop
654
+ self._recv_tasks[peer_id] = asyncio.ensure_future(self._peer_recv_loop(peer_id))
655
+
656
+ async def connect_to(self, host: str, port: int) -> Optional[PeerConnection]:
657
+ """Connect to a remote peer."""
658
+ if len(self.peers) >= self.max_peers:
659
+ return None
660
+
661
+ try:
662
+ reader, writer = await asyncio.wait_for(
663
+ asyncio.open_connection(host, port, ssl=self._client_ssl),
664
+ timeout=10.0,
665
+ )
666
+ except (ConnectionError, asyncio.TimeoutError, OSError) as e:
667
+ logger.debug("Failed to connect to %s:%d: %s", host, port, e)
668
+ return None
669
+
670
+ # Certificate pinning check
671
+ if not self._check_cert_pin(writer):
672
+ logger.warning("Rejected outbound connection (cert pin mismatch) to %s:%d", host, port)
673
+ writer.close()
674
+ return None
675
+
676
+ temp_info = PeerInfo(host=host, port=port)
677
+ conn = PeerConnection(reader, writer, temp_info, is_inbound=False)
678
+
679
+ # Send handshake
680
+ await conn.send(Message(
681
+ type=MessageType.HANDSHAKE,
682
+ sender=self.node_id,
683
+ payload={
684
+ "chain_id": self.chain_id,
685
+ "port": self.port,
686
+ "version": "1.0.0",
687
+ }
688
+ ))
689
+
690
+ # Wait for remote handshake
691
+ try:
692
+ msg = await asyncio.wait_for(conn.receive(), timeout=10.0)
693
+ except asyncio.TimeoutError:
694
+ await conn.close()
695
+ return None
696
+
697
+ if not msg or msg.type != MessageType.HANDSHAKE:
698
+ await conn.close()
699
+ return None
700
+
701
+ peer_id = msg.sender
702
+ if peer_id == self.node_id or peer_id in self.peers:
703
+ await conn.close()
704
+ return None
705
+
706
+ # Reject banned peers
707
+ if self.reputation.is_banned(peer_id):
708
+ logger.info("Refused connection to banned peer %s", peer_id[:8])
709
+ await conn.close()
710
+ return None
711
+
712
+ conn.peer_info.peer_id = peer_id
713
+ conn.peer_info.chain_id = msg.payload.get("chain_id", "")
714
+ conn.peer_info.connected = True
715
+ conn.peer_info.last_seen = time.time()
716
+ self.peers[peer_id] = conn
717
+ self.known_peers[peer_id] = conn.peer_info
718
+ logger.info("Peer connected (outbound): %s @ %s:%d", peer_id[:8], host, port)
719
+
720
+ self._recv_tasks[peer_id] = asyncio.ensure_future(self._peer_recv_loop(peer_id))
721
+ return conn
722
+
723
+ async def disconnect_peer(self, peer_id: str):
724
+ """Disconnect a specific peer."""
725
+ conn = self.peers.pop(peer_id, None)
726
+ if conn:
727
+ await conn.send(Message(type=MessageType.DISCONNECT, sender=self.node_id))
728
+ await conn.close()
729
+ if peer_id in self.known_peers:
730
+ self.known_peers[peer_id].connected = False
731
+ task = self._recv_tasks.pop(peer_id, None)
732
+ if task:
733
+ task.cancel()
734
+
735
+ # ── Message sending ────────────────────────────────────────────────
736
+
737
+ async def send(self, peer_id: str, msg: Message) -> bool:
738
+ """Send a message to a specific peer."""
739
+ conn = self.peers.get(peer_id)
740
+ if not conn:
741
+ return False
742
+ msg.sender = self.node_id
743
+ return await conn.send(msg)
744
+
745
+ async def broadcast(self, msg: Message, exclude: Optional[Set[str]] = None):
746
+ """Broadcast a message to all connected peers."""
747
+ msg.sender = self.node_id
748
+ exclude = exclude or set()
749
+ for peer_id, conn in list(self.peers.items()):
750
+ if peer_id not in exclude:
751
+ success = await conn.send(msg)
752
+ if not success:
753
+ await self.disconnect_peer(peer_id)
754
+
755
+ async def gossip(self, msg: Message, fanout: int = 0, exclude: Optional[Set[str]] = None):
756
+ """Gossip a message to a random subset of peers.
757
+
758
+ If fanout is 0, broadcasts to all peers. Otherwise, selects
759
+ ``fanout`` random peers from the connected set.
760
+ """
761
+ # Deduplication
762
+ if msg.nonce in self._seen_messages:
763
+ return
764
+ self._seen_messages.add(msg.nonce)
765
+ if len(self._seen_messages) > self._seen_max:
766
+ # Trim oldest (convert to list, drop first half)
767
+ self._seen_messages = set(list(self._seen_messages)[self._seen_max // 2:])
768
+
769
+ msg.sender = self.node_id
770
+ exclude = exclude or set()
771
+ candidates = [pid for pid in self.peers if pid not in exclude]
772
+
773
+ if fanout > 0 and len(candidates) > fanout:
774
+ candidates = random.sample(candidates, fanout)
775
+
776
+ for peer_id in candidates:
777
+ conn = self.peers.get(peer_id)
778
+ if conn:
779
+ await conn.send(msg)
780
+
781
+ # ── Receive loop ───────────────────────────────────────────────────
782
+
783
+ async def _peer_recv_loop(self, peer_id: str):
784
+ """Continuous receive loop for a peer."""
785
+ conn = self.peers.get(peer_id)
786
+ if not conn:
787
+ return
788
+ while self._running and conn.is_connected:
789
+ msg = await conn.receive()
790
+ if msg is None:
791
+ break
792
+ conn.peer_info.last_seen = time.time()
793
+
794
+ if msg.type == MessageType.DISCONNECT:
795
+ break
796
+
797
+ await self._dispatch(msg, conn)
798
+
799
+ # Cleanup
800
+ self.peers.pop(peer_id, None)
801
+ if peer_id in self.known_peers:
802
+ self.known_peers[peer_id].connected = False
803
+ logger.debug("Peer disconnected: %s", peer_id[:8])
804
+
805
+ # ── Built-in handlers ──────────────────────────────────────────────
806
+
807
+ async def _handle_ping(self, msg: Message, conn: PeerConnection):
808
+ await conn.send(Message(
809
+ type=MessageType.PONG,
810
+ sender=self.node_id,
811
+ payload={"echo": msg.nonce}
812
+ ))
813
+
814
+ async def _handle_find_peers(self, msg: Message, conn: PeerConnection):
815
+ """Return known peers to the requester."""
816
+ peers_list = []
817
+ for pid, info in self.known_peers.items():
818
+ if pid != msg.sender and info.connected:
819
+ peers_list.append({
820
+ "peer_id": info.peer_id,
821
+ "host": info.host,
822
+ "port": info.port,
823
+ })
824
+ await conn.send(Message(
825
+ type=MessageType.PEERS,
826
+ sender=self.node_id,
827
+ payload={"peers": peers_list[:20]}
828
+ ))
829
+
830
+ async def _handle_handshake_msg(self, msg: Message, conn: PeerConnection):
831
+ """Handle late handshake messages (re-announcements)."""
832
+ pass # Already handled during connection setup
833
+
834
+ # ── Peer discovery ─────────────────────────────────────────────────
835
+
836
+ def add_bootstrap_node(self, host: str, port: int):
837
+ """Add a bootstrap node for initial peer discovery."""
838
+ self.bootstrap_nodes.append((host, port))
839
+
840
+ async def _bootstrap(self):
841
+ """Connect to bootstrap nodes and discover more peers."""
842
+ for host, port in self.bootstrap_nodes:
843
+ if len(self.peers) >= self.max_peers:
844
+ break
845
+ await self.connect_to(host, port)
846
+
847
+ # Discover more peers from connected ones
848
+ if self.peers:
849
+ await self.broadcast(Message(
850
+ type=MessageType.FIND_PEERS,
851
+ sender=self.node_id,
852
+ ))
853
+
854
+ async def discover_peers(self):
855
+ """Actively discover new peers by querying existing connections."""
856
+ if not self.peers:
857
+ await self._bootstrap()
858
+ return
859
+ await self.broadcast(Message(
860
+ type=MessageType.FIND_PEERS,
861
+ sender=self.node_id,
862
+ ))
863
+
864
+ # ── Utility ────────────────────────────────────────────────────────
865
+
866
+ def get_network_info(self) -> Dict[str, Any]:
867
+ """Get network status information."""
868
+ return {
869
+ "node_id": self.node_id,
870
+ "host": self.host,
871
+ "port": self.port,
872
+ "chain_id": self.chain_id,
873
+ "tls_enabled": self.tls_enabled,
874
+ "connected_peers": len(self.peers),
875
+ "known_peers": len(self.known_peers),
876
+ "running": self._running,
877
+ "peers": [
878
+ {
879
+ "peer_id": pid[:8],
880
+ "address": conn.peer_info.address,
881
+ "inbound": conn.is_inbound,
882
+ "last_seen": conn.peer_info.last_seen,
883
+ }
884
+ for pid, conn in self.peers.items()
885
+ ]
886
+ }