nexaroa 0.0.111__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.
Files changed (78) hide show
  1. neuroshard/__init__.py +93 -0
  2. neuroshard/__main__.py +4 -0
  3. neuroshard/cli.py +466 -0
  4. neuroshard/core/__init__.py +92 -0
  5. neuroshard/core/consensus/verifier.py +252 -0
  6. neuroshard/core/crypto/__init__.py +20 -0
  7. neuroshard/core/crypto/ecdsa.py +392 -0
  8. neuroshard/core/economics/__init__.py +52 -0
  9. neuroshard/core/economics/constants.py +387 -0
  10. neuroshard/core/economics/ledger.py +2111 -0
  11. neuroshard/core/economics/market.py +975 -0
  12. neuroshard/core/economics/wallet.py +168 -0
  13. neuroshard/core/governance/__init__.py +74 -0
  14. neuroshard/core/governance/proposal.py +561 -0
  15. neuroshard/core/governance/registry.py +545 -0
  16. neuroshard/core/governance/versioning.py +332 -0
  17. neuroshard/core/governance/voting.py +453 -0
  18. neuroshard/core/model/__init__.py +30 -0
  19. neuroshard/core/model/dynamic.py +4186 -0
  20. neuroshard/core/model/llm.py +905 -0
  21. neuroshard/core/model/registry.py +164 -0
  22. neuroshard/core/model/scaler.py +387 -0
  23. neuroshard/core/model/tokenizer.py +568 -0
  24. neuroshard/core/network/__init__.py +56 -0
  25. neuroshard/core/network/connection_pool.py +72 -0
  26. neuroshard/core/network/dht.py +130 -0
  27. neuroshard/core/network/dht_plan.py +55 -0
  28. neuroshard/core/network/dht_proof_store.py +516 -0
  29. neuroshard/core/network/dht_protocol.py +261 -0
  30. neuroshard/core/network/dht_service.py +506 -0
  31. neuroshard/core/network/encrypted_channel.py +141 -0
  32. neuroshard/core/network/nat.py +201 -0
  33. neuroshard/core/network/nat_traversal.py +695 -0
  34. neuroshard/core/network/p2p.py +929 -0
  35. neuroshard/core/network/p2p_data.py +150 -0
  36. neuroshard/core/swarm/__init__.py +106 -0
  37. neuroshard/core/swarm/aggregation.py +729 -0
  38. neuroshard/core/swarm/buffers.py +643 -0
  39. neuroshard/core/swarm/checkpoint.py +709 -0
  40. neuroshard/core/swarm/compute.py +624 -0
  41. neuroshard/core/swarm/diloco.py +844 -0
  42. neuroshard/core/swarm/factory.py +1288 -0
  43. neuroshard/core/swarm/heartbeat.py +669 -0
  44. neuroshard/core/swarm/logger.py +487 -0
  45. neuroshard/core/swarm/router.py +658 -0
  46. neuroshard/core/swarm/service.py +640 -0
  47. neuroshard/core/training/__init__.py +29 -0
  48. neuroshard/core/training/checkpoint.py +600 -0
  49. neuroshard/core/training/distributed.py +1602 -0
  50. neuroshard/core/training/global_tracker.py +617 -0
  51. neuroshard/core/training/production.py +276 -0
  52. neuroshard/governance_cli.py +729 -0
  53. neuroshard/grpc_server.py +895 -0
  54. neuroshard/runner.py +3223 -0
  55. neuroshard/sdk/__init__.py +92 -0
  56. neuroshard/sdk/client.py +990 -0
  57. neuroshard/sdk/errors.py +101 -0
  58. neuroshard/sdk/types.py +282 -0
  59. neuroshard/tracker/__init__.py +0 -0
  60. neuroshard/tracker/server.py +864 -0
  61. neuroshard/ui/__init__.py +0 -0
  62. neuroshard/ui/app.py +102 -0
  63. neuroshard/ui/templates/index.html +1052 -0
  64. neuroshard/utils/__init__.py +0 -0
  65. neuroshard/utils/autostart.py +81 -0
  66. neuroshard/utils/hardware.py +121 -0
  67. neuroshard/utils/serialization.py +90 -0
  68. neuroshard/version.py +1 -0
  69. nexaroa-0.0.111.dist-info/METADATA +283 -0
  70. nexaroa-0.0.111.dist-info/RECORD +78 -0
  71. nexaroa-0.0.111.dist-info/WHEEL +5 -0
  72. nexaroa-0.0.111.dist-info/entry_points.txt +4 -0
  73. nexaroa-0.0.111.dist-info/licenses/LICENSE +190 -0
  74. nexaroa-0.0.111.dist-info/top_level.txt +2 -0
  75. protos/__init__.py +0 -0
  76. protos/neuroshard.proto +651 -0
  77. protos/neuroshard_pb2.py +160 -0
  78. protos/neuroshard_pb2_grpc.py +1298 -0
@@ -0,0 +1,252 @@
1
+ """
2
+ Proof of Neural Work - Semantic Verifier
3
+
4
+ This module enforces UNIVERSAL verification rules - the "laws of physics" that apply
5
+ to ALL nodes regardless of their internal state. These are constraints that no
6
+ legitimate node can violate.
7
+
8
+ Architecture:
9
+ ============
10
+ The verification system has two layers:
11
+
12
+ 1. ProofVerifier (this file) - Universal Constraints
13
+ - Physical rate limits (no GPU can exceed certain throughput)
14
+ - Required fields validation
15
+ - Format and sanity checks
16
+ - Delegates to model_interface for node-specific checks
17
+
18
+ 2. ModelInterface (e.g., SwarmEnabledDynamicNode) - Node-Specific State
19
+ - Internal counter verification (only the node knows its true work count)
20
+ - Model hash matching against local architecture
21
+ - Training enabled status
22
+
23
+ The Verifier enforces *laws of physics*.
24
+ The Node enforces *personal integrity*.
25
+ """
26
+
27
+ import logging
28
+ from typing import Tuple, Optional, Any
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # =============================================================================
34
+ # PHYSICAL CONSTANTS - Hardware Limits
35
+ # =============================================================================
36
+ # These are generous upper bounds based on current GPU capabilities.
37
+ # Even an H100 cluster cannot exceed these sustained rates.
38
+
39
+ # Maximum training batches per second (sustained)
40
+ # Rationale: Even H100 with small batch takes ~50ms per step minimum
41
+ # 2.0 batches/sec = 500ms/batch, very generous for any real training
42
+ MAX_TRAINING_RATE_PER_SEC = 2.0
43
+
44
+ # Maximum inference tokens per second per GPU
45
+ # Rationale: H100 can do ~3000 tokens/sec for small models, ~500 for large
46
+ # 5000 is generous upper bound for any realistic deployment
47
+ MAX_INFERENCE_TOKENS_PER_SEC = 5000.0
48
+
49
+ # Minimum uptime to claim meaningful work (prevents micro-farming)
50
+ MIN_UPTIME_FOR_TRAINING_REWARD = 10.0 # 10 seconds
51
+
52
+
53
+ class ProofVerifier:
54
+ """
55
+ Verifies the semantic validity of Proof of Neural Work.
56
+
57
+ Enforces universal constraints that apply to ALL nodes:
58
+ - Physical hardware limits (rate caps)
59
+ - Required field validation
60
+ - Proof format sanity
61
+
62
+ Delegates node-specific verification to the model_interface:
63
+ - Internal work counter validation
64
+ - Local model hash matching
65
+ - Training state verification
66
+
67
+ Usage:
68
+ verifier = ProofVerifier(model_interface=swarm_node)
69
+ is_valid, reason = verifier.verify_work_content(proof)
70
+ """
71
+
72
+ def __init__(self, model_interface: Optional[Any] = None):
73
+ """
74
+ Initialize the verifier.
75
+
76
+ Args:
77
+ model_interface: Object implementing verify_training_work(proof).
78
+ Typically a SwarmEnabledDynamicNode.
79
+ Required for training proof verification.
80
+ """
81
+ self.model_interface = model_interface
82
+
83
+ def verify_work_content(self, proof: Any) -> Tuple[bool, str]:
84
+ """
85
+ Verify the content of a PoNW proof.
86
+
87
+ Applies universal constraints first, then delegates to
88
+ model_interface for node-specific validation.
89
+
90
+ Args:
91
+ proof: The PoNWProof object to verify
92
+
93
+ Returns:
94
+ (is_valid, reason) tuple
95
+ """
96
+ proof_type = getattr(proof, 'proof_type', None)
97
+
98
+ if proof_type == "training":
99
+ return self._verify_training_work(proof)
100
+ elif proof_type == "inference":
101
+ return self._verify_inference_work(proof)
102
+ elif proof_type == "uptime":
103
+ return self._verify_uptime_work(proof)
104
+ elif proof_type == "data":
105
+ return self._verify_data_work(proof)
106
+ else:
107
+ return False, f"Unknown proof type: {proof_type}"
108
+
109
+ def _verify_training_work(self, proof: Any) -> Tuple[bool, str]:
110
+ """
111
+ Verify training proof with both universal and node-specific checks.
112
+
113
+ Universal Checks (this method):
114
+ 1. Required fields present (model_hash)
115
+ 2. Physical rate limit (can't exceed hardware capabilities)
116
+ 3. Minimum uptime threshold
117
+
118
+ Node-Specific Checks (delegated to model_interface):
119
+ 4. Model hash matches local architecture
120
+ 5. Claimed batches <= internal counter
121
+ 6. Training is actually enabled
122
+
123
+ The "Golden Rule" of PoNW: L(w - lr * g) < L(w)
124
+ We verify this indirectly through counter checks.
125
+ """
126
+ # =====================================================================
127
+ # UNIVERSAL CHECK 1: Required Fields
128
+ # =====================================================================
129
+ if not getattr(proof, 'model_hash', None):
130
+ return False, "Missing model hash for training proof"
131
+
132
+ # =====================================================================
133
+ # UNIVERSAL CHECK 2: Physical Rate Limit
134
+ # =====================================================================
135
+ # No GPU can sustain more than MAX_TRAINING_RATE_PER_SEC batches/second.
136
+ # This is a hard physical constraint.
137
+ uptime = getattr(proof, 'uptime_seconds', 0)
138
+ batches = getattr(proof, 'training_batches', 0)
139
+
140
+ if uptime > 0 and batches > 0:
141
+ rate = batches / uptime
142
+ if rate > MAX_TRAINING_RATE_PER_SEC:
143
+ return False, (
144
+ f"Impossible training rate: {rate:.2f} batches/sec "
145
+ f"(max: {MAX_TRAINING_RATE_PER_SEC})"
146
+ )
147
+
148
+ # =====================================================================
149
+ # UNIVERSAL CHECK 3: Minimum Uptime
150
+ # =====================================================================
151
+ # Prevent micro-farming attacks with tiny uptimes
152
+ if batches > 0 and uptime < MIN_UPTIME_FOR_TRAINING_REWARD:
153
+ return False, (
154
+ f"Uptime too short for training reward: {uptime:.1f}s "
155
+ f"(min: {MIN_UPTIME_FOR_TRAINING_REWARD}s)"
156
+ )
157
+
158
+ # =====================================================================
159
+ # NODE-SPECIFIC CHECKS: Delegate to ModelInterface
160
+ # =====================================================================
161
+ # These checks require knowledge of the node's internal state.
162
+ if self.model_interface is None:
163
+ return False, "Cannot verify training work: no model interface available"
164
+
165
+ if not hasattr(self.model_interface, 'verify_training_work'):
166
+ return False, "Model interface missing verify_training_work method"
167
+
168
+ return self.model_interface.verify_training_work(proof)
169
+
170
+ def _verify_inference_work(self, proof: Any) -> Tuple[bool, str]:
171
+ """
172
+ Verify inference proof via economic receipts.
173
+
174
+ Inference rewards flow from User -> Node.
175
+ We verify that the User authorized this work.
176
+
177
+ Universal Checks:
178
+ 1. Request ID present (links to payment)
179
+ 2. Token rate within physical limits
180
+ """
181
+ # =====================================================================
182
+ # UNIVERSAL CHECK 1: Request ID Required
183
+ # =====================================================================
184
+ if not getattr(proof, 'request_id', None):
185
+ return False, "Missing request_id: anonymous inference not verifiable"
186
+
187
+ # =====================================================================
188
+ # UNIVERSAL CHECK 2: Token Rate Limit
189
+ # =====================================================================
190
+ uptime = getattr(proof, 'uptime_seconds', 0)
191
+ tokens = getattr(proof, 'tokens_processed', 0)
192
+
193
+ if uptime > 0 and tokens > 0:
194
+ rate = tokens / uptime
195
+ if rate > MAX_INFERENCE_TOKENS_PER_SEC:
196
+ return False, (
197
+ f"Impossible inference rate: {rate:.0f} tokens/sec "
198
+ f"(max: {MAX_INFERENCE_TOKENS_PER_SEC})"
199
+ )
200
+
201
+ # Inference receipts are further validated by the Market/Ledger
202
+ # which checks that the user actually paid for this request
203
+ return True, "Inference work linked to valid request"
204
+
205
+ def _verify_uptime_work(self, proof: Any) -> Tuple[bool, str]:
206
+ """
207
+ Verify uptime proof.
208
+
209
+ Uptime is verified through:
210
+ 1. Gossip protocol (peers attest to availability)
211
+ 2. Rate limits in the Ledger (can't claim faster than real time)
212
+
213
+ The Ledger handles most uptime validation through its
214
+ rate limiting and gossip-based peer attestation.
215
+ """
216
+ uptime = getattr(proof, 'uptime_seconds', 0)
217
+
218
+ # Sanity check: uptime can't be negative
219
+ if uptime < 0:
220
+ return False, "Negative uptime is invalid"
221
+
222
+ # Sanity check: uptime can't exceed proof window
223
+ # (The Ledger enforces this more precisely with timestamps)
224
+ max_reasonable_uptime = 3600 * 24 # 24 hours max per proof
225
+ if uptime > max_reasonable_uptime:
226
+ return False, f"Uptime {uptime}s exceeds maximum proof window"
227
+
228
+ return True, "Uptime valid"
229
+
230
+ def _verify_data_work(self, proof: Any) -> Tuple[bool, str]:
231
+ """
232
+ Verify data serving proof.
233
+
234
+ Data serving is verified optimistically:
235
+ - Nodes claim they served data shards
236
+ - The tracker/DHT can spot-check availability
237
+ - Economic incentives discourage false claims
238
+
239
+ Future: Could add Merkle proofs for data possession.
240
+ """
241
+ layers_held = getattr(proof, 'layers_held', 0)
242
+
243
+ # Sanity check: can't hold negative layers
244
+ if layers_held < 0:
245
+ return False, "Negative layers_held is invalid"
246
+
247
+ # Sanity check: can't hold more layers than exist
248
+ max_layers = 128 # Generous upper bound
249
+ if layers_held > max_layers:
250
+ return False, f"layers_held {layers_held} exceeds maximum {max_layers}"
251
+
252
+ return True, "Data serving valid (optimistic verification)"
@@ -0,0 +1,20 @@
1
+ # neuroshard/core/crypto/__init__.py
2
+ """
3
+ Cryptography components for NeuroShard.
4
+
5
+ - ecdsa: ECDSA signing and verification
6
+ """
7
+
8
+ __all__ = [
9
+ 'sign_message',
10
+ 'verify_signature',
11
+ 'generate_keypair',
12
+ 'is_valid_node_id_format',
13
+ ]
14
+
15
+ def __getattr__(name):
16
+ """Lazy loading of submodules."""
17
+ if name in ('sign_message', 'verify_signature', 'generate_keypair', 'is_valid_node_id_format'):
18
+ from neuroshard.core.crypto import ecdsa
19
+ return getattr(ecdsa, name)
20
+ raise AttributeError(f"module 'neuroshard.core.crypto' has no attribute '{name}'")
@@ -0,0 +1,392 @@
1
+ """
2
+ NEURO Cryptographic Module - ECDSA Only
3
+
4
+ This module provides cryptographic primitives for the NEURO token system
5
+ using ECDSA (Elliptic Curve Digital Signature Algorithm) with secp256k1.
6
+
7
+ Security Model:
8
+ ===============
9
+ ECDSA allows ANYONE to verify a signature without knowing the private key.
10
+ This enables TRUE TRUSTLESS verification in the P2P network.
11
+
12
+ Key Components:
13
+ 1. Private Key: 32-byte secret derived from node_token
14
+ 2. Public Key: 33-byte compressed point on secp256k1 curve
15
+ 3. Node ID: First 32 hex chars of SHA256(public_key)
16
+ 4. Signature: DER-encoded ECDSA signature
17
+
18
+ Why ECDSA over HMAC:
19
+ - HMAC requires shared secret - only the signer can verify
20
+ - ECDSA uses public/private key pair - ANYONE can verify with public key
21
+ - This enables trustless P2P verification without central authority
22
+ """
23
+
24
+ import hashlib
25
+ import os
26
+ import logging
27
+ from typing import Tuple, Optional
28
+ from dataclasses import dataclass
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Import cryptography library for ECDSA - REQUIRED
33
+ from cryptography.hazmat.primitives import hashes, serialization
34
+ from cryptography.hazmat.primitives.asymmetric import ec
35
+ from cryptography.hazmat.backends import default_backend
36
+ from cryptography.exceptions import InvalidSignature
37
+
38
+ ECDSA_AVAILABLE = True
39
+
40
+
41
+ @dataclass
42
+ class KeyPair:
43
+ """ECDSA key pair for signing and verification."""
44
+ private_key_bytes: bytes # 32 bytes
45
+ public_key_bytes: bytes # 33 bytes (compressed)
46
+ node_id: str # 32 hex chars (from public key hash)
47
+
48
+
49
+ def derive_keypair_from_token(node_token: str) -> KeyPair:
50
+ """
51
+ Derive an ECDSA key pair from a node token.
52
+
53
+ The private key is derived deterministically from the token,
54
+ ensuring the same token always produces the same key pair.
55
+
56
+ Args:
57
+ node_token: The node's secret token (from registration)
58
+
59
+ Returns:
60
+ KeyPair with private key, public key, and derived node_id
61
+
62
+ Raises:
63
+ ValueError: If key derivation fails
64
+ """
65
+ # Derive 32-byte private key from token
66
+ private_key_bytes = hashlib.sha256(node_token.encode()).digest()
67
+
68
+ # Create ECDSA private key from bytes
69
+ private_key = ec.derive_private_key(
70
+ int.from_bytes(private_key_bytes, 'big'),
71
+ ec.SECP256K1(),
72
+ default_backend()
73
+ )
74
+
75
+ # Get public key in compressed format
76
+ public_key = private_key.public_key()
77
+ public_key_bytes = public_key.public_bytes(
78
+ encoding=serialization.Encoding.X962,
79
+ format=serialization.PublicFormat.CompressedPoint
80
+ )
81
+
82
+ # Derive node_id from public key (first 32 hex chars of SHA256)
83
+ node_id = hashlib.sha256(public_key_bytes).hexdigest()[:32]
84
+
85
+ return KeyPair(
86
+ private_key_bytes=private_key_bytes,
87
+ public_key_bytes=public_key_bytes,
88
+ node_id=node_id
89
+ )
90
+
91
+
92
+ def ecdsa_sign(payload: str, private_key_bytes: bytes) -> str:
93
+ """
94
+ Sign a payload using ECDSA with secp256k1 curve.
95
+
96
+ Args:
97
+ payload: The message to sign
98
+ private_key_bytes: 32-byte private key
99
+
100
+ Returns:
101
+ Hex-encoded DER signature
102
+ """
103
+ # Recreate private key from bytes
104
+ private_key = ec.derive_private_key(
105
+ int.from_bytes(private_key_bytes, 'big'),
106
+ ec.SECP256K1(),
107
+ default_backend()
108
+ )
109
+
110
+ # Sign the payload
111
+ signature = private_key.sign(
112
+ payload.encode(),
113
+ ec.ECDSA(hashes.SHA256())
114
+ )
115
+
116
+ # Return hex-encoded signature
117
+ return signature.hex()
118
+
119
+
120
+ def ecdsa_verify(payload: str, signature_hex: str, public_key_bytes: bytes) -> bool:
121
+ """
122
+ Verify an ECDSA signature.
123
+
124
+ This is the key function that enables TRUSTLESS verification.
125
+ Anyone can verify a signature using only the public key.
126
+
127
+ Args:
128
+ payload: The original message
129
+ signature_hex: Hex-encoded DER signature
130
+ public_key_bytes: 33-byte compressed public key
131
+
132
+ Returns:
133
+ True if signature is valid
134
+ """
135
+ try:
136
+ # Decode signature
137
+ signature = bytes.fromhex(signature_hex)
138
+
139
+ # Recreate public key from bytes
140
+ public_key = ec.EllipticCurvePublicKey.from_encoded_point(
141
+ ec.SECP256K1(),
142
+ public_key_bytes
143
+ )
144
+
145
+ # Verify signature
146
+ public_key.verify(
147
+ signature,
148
+ payload.encode(),
149
+ ec.ECDSA(hashes.SHA256())
150
+ )
151
+
152
+ return True
153
+ except InvalidSignature:
154
+ logger.debug(f"ECDSA signature verification failed")
155
+ return False
156
+ except Exception as e:
157
+ logger.warning(f"ECDSA verification error: {e}")
158
+ return False
159
+
160
+
161
+ def derive_node_id_from_token(token: str) -> str:
162
+ """
163
+ Derive a deterministic node ID from a token.
164
+
165
+ node_id = SHA256(public_key)[:32]
166
+
167
+ This ensures the same token always produces the same node_id.
168
+ """
169
+ keypair = derive_keypair_from_token(token)
170
+ return keypair.node_id
171
+
172
+
173
+ def sign_message(payload: str, node_token: str) -> str:
174
+ """
175
+ Sign a message using the node's ECDSA private key.
176
+
177
+ Convenience wrapper around ecdsa_sign that derives the key from token.
178
+
179
+ Args:
180
+ payload: The message to sign
181
+ node_token: The node's secret token
182
+
183
+ Returns:
184
+ Hex-encoded ECDSA signature
185
+ """
186
+ keypair = derive_keypair_from_token(node_token)
187
+ return ecdsa_sign(payload, keypair.private_key_bytes)
188
+
189
+
190
+ class NodeCrypto:
191
+ """
192
+ Cryptographic operations for a node using ECDSA.
193
+
194
+ Provides signing and verification using secp256k1 curve.
195
+ """
196
+
197
+ def __init__(self, node_token: str):
198
+ """
199
+ Initialize crypto with node token.
200
+
201
+ Args:
202
+ node_token: The node's secret token
203
+
204
+ Raises:
205
+ ValueError: If key derivation fails
206
+ """
207
+ self.node_token = node_token
208
+ self.keypair = derive_keypair_from_token(node_token)
209
+ self.node_id = self.keypair.node_id
210
+
211
+ logger.info(f"ECDSA initialized for node {self.node_id[:16]}...")
212
+
213
+ def sign(self, payload: str) -> str:
214
+ """
215
+ Sign a payload using ECDSA.
216
+
217
+ Args:
218
+ payload: The message to sign
219
+
220
+ Returns:
221
+ Hex-encoded ECDSA signature
222
+ """
223
+ return ecdsa_sign(payload, self.keypair.private_key_bytes)
224
+
225
+ def verify(self, payload: str, signature: str, public_key_bytes: Optional[bytes] = None) -> bool:
226
+ """
227
+ Verify a signature.
228
+
229
+ Args:
230
+ payload: The original message
231
+ signature: Hex-encoded ECDSA signature
232
+ public_key_bytes: Public key to verify against (defaults to our own)
233
+
234
+ Returns:
235
+ True if signature is valid
236
+ """
237
+ pk = public_key_bytes or self.keypair.public_key_bytes
238
+ return ecdsa_verify(payload, signature, pk)
239
+
240
+ def get_public_key_hex(self) -> str:
241
+ """Get the public key in hex format for sharing."""
242
+ return self.keypair.public_key_bytes.hex()
243
+
244
+ def get_public_key_bytes(self) -> bytes:
245
+ """Get the raw public key bytes."""
246
+ return self.keypair.public_key_bytes
247
+
248
+ def store_public_key(self, node_id: str, public_key_hex: str) -> bool:
249
+ """
250
+ Store a public key for another node (for verification).
251
+
252
+ This is called when receiving proofs/stakes from other nodes
253
+ that include their public key.
254
+
255
+ Args:
256
+ node_id: The node's ID
257
+ public_key_hex: Hex-encoded public key
258
+
259
+ Returns:
260
+ True if stored successfully
261
+ """
262
+ return register_public_key(node_id, public_key_hex)
263
+
264
+
265
+ # Registry of known public keys (node_id -> public_key_bytes)
266
+ # This is populated via DHT gossip
267
+ _public_key_registry: dict = {}
268
+
269
+
270
+ def register_public_key(node_id: str, public_key_hex: str) -> bool:
271
+ """
272
+ Register a node's public key for future verification.
273
+
274
+ Called when receiving public key announcements via gossip.
275
+
276
+ Args:
277
+ node_id: The node's ID (should match SHA256(public_key)[:32])
278
+ public_key_hex: Hex-encoded compressed public key
279
+
280
+ Returns:
281
+ True if registration successful
282
+ """
283
+ try:
284
+ public_key_bytes = bytes.fromhex(public_key_hex)
285
+
286
+ # Verify the public key matches the claimed node_id
287
+ expected_node_id = hashlib.sha256(public_key_bytes).hexdigest()[:32]
288
+ if expected_node_id != node_id:
289
+ logger.warning(f"Public key mismatch for {node_id[:16]}... "
290
+ f"(expected {expected_node_id[:16]}...)")
291
+ return False
292
+
293
+ _public_key_registry[node_id] = public_key_bytes
294
+ logger.debug(f"Registered public key for {node_id[:16]}...")
295
+ return True
296
+ except Exception as e:
297
+ logger.error(f"Failed to register public key: {e}")
298
+ return False
299
+
300
+
301
+ def get_public_key(node_id: str) -> Optional[bytes]:
302
+ """Get a registered public key for a node."""
303
+ return _public_key_registry.get(node_id)
304
+
305
+
306
+ def verify_signature(node_id: str, payload: str, signature: str, public_key_hex: str = None) -> bool:
307
+ """
308
+ Verify a signature from any node.
309
+
310
+ This is the key function for trustless verification in the P2P network.
311
+
312
+ SECURITY: We NEVER accept unverified signatures. If we don't have the
313
+ public key, the signature is REJECTED. The sender must include their
314
+ public key in the message for verification.
315
+
316
+ Args:
317
+ node_id: The claimed signer's node ID
318
+ payload: The original message
319
+ signature: Hex-encoded ECDSA signature
320
+ public_key_hex: Optional public key (if not in registry)
321
+
322
+ Returns:
323
+ True if signature is valid, False otherwise
324
+ """
325
+ # Try to get public key from registry first
326
+ public_key = get_public_key(node_id)
327
+
328
+ # If not in registry, try the provided public key
329
+ if not public_key and public_key_hex:
330
+ try:
331
+ public_key = bytes.fromhex(public_key_hex)
332
+
333
+ # CRITICAL: Verify node_id matches the public key!
334
+ # node_id should be SHA256(public_key)[:32]
335
+ expected_node_id = hashlib.sha256(public_key).hexdigest()[:32]
336
+ if expected_node_id != node_id:
337
+ logger.warning(f"Public key mismatch! Claimed {node_id[:16]}... but key gives {expected_node_id[:16]}...")
338
+ return False
339
+
340
+ # Register for future use
341
+ register_public_key(node_id, public_key_hex)
342
+ except Exception as e:
343
+ logger.debug(f"Failed to parse provided public key: {e}")
344
+
345
+ if not public_key:
346
+ # SECURITY: No public key = REJECT
347
+ # We cannot verify without the public key
348
+ logger.warning(f"Signature rejected: No public key for {node_id[:16]}...")
349
+ return False
350
+
351
+ return ecdsa_verify(payload, signature, public_key)
352
+
353
+
354
+ def is_valid_signature_format(signature: str) -> bool:
355
+ """
356
+ Check if a signature has valid ECDSA format.
357
+
358
+ ECDSA DER signatures are typically 70-72 bytes (140-144 hex chars).
359
+ """
360
+ if not signature:
361
+ return False
362
+
363
+ # Must be hex
364
+ if not all(c in '0123456789abcdef' for c in signature.lower()):
365
+ return False
366
+
367
+ # ECDSA DER signatures are typically 70-72 bytes
368
+ # In hex that's 140-144 characters
369
+ sig_len = len(signature)
370
+ if sig_len < 128 or sig_len > 150:
371
+ logger.debug(f"Unusual signature length: {sig_len}")
372
+ # Still allow - some valid signatures can vary
373
+
374
+ return True
375
+
376
+
377
+ def is_valid_node_id_format(node_id: str) -> bool:
378
+ """
379
+ Check if a node ID has valid format.
380
+
381
+ Node IDs are 32 hex characters (128 bits).
382
+ """
383
+ if not node_id:
384
+ return False
385
+
386
+ if len(node_id) != 32:
387
+ return False
388
+
389
+ if not all(c in '0123456789abcdef' for c in node_id.lower()):
390
+ return False
391
+
392
+ return True