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.
- neuroshard/__init__.py +93 -0
- neuroshard/__main__.py +4 -0
- neuroshard/cli.py +466 -0
- neuroshard/core/__init__.py +92 -0
- neuroshard/core/consensus/verifier.py +252 -0
- neuroshard/core/crypto/__init__.py +20 -0
- neuroshard/core/crypto/ecdsa.py +392 -0
- neuroshard/core/economics/__init__.py +52 -0
- neuroshard/core/economics/constants.py +387 -0
- neuroshard/core/economics/ledger.py +2111 -0
- neuroshard/core/economics/market.py +975 -0
- neuroshard/core/economics/wallet.py +168 -0
- neuroshard/core/governance/__init__.py +74 -0
- neuroshard/core/governance/proposal.py +561 -0
- neuroshard/core/governance/registry.py +545 -0
- neuroshard/core/governance/versioning.py +332 -0
- neuroshard/core/governance/voting.py +453 -0
- neuroshard/core/model/__init__.py +30 -0
- neuroshard/core/model/dynamic.py +4186 -0
- neuroshard/core/model/llm.py +905 -0
- neuroshard/core/model/registry.py +164 -0
- neuroshard/core/model/scaler.py +387 -0
- neuroshard/core/model/tokenizer.py +568 -0
- neuroshard/core/network/__init__.py +56 -0
- neuroshard/core/network/connection_pool.py +72 -0
- neuroshard/core/network/dht.py +130 -0
- neuroshard/core/network/dht_plan.py +55 -0
- neuroshard/core/network/dht_proof_store.py +516 -0
- neuroshard/core/network/dht_protocol.py +261 -0
- neuroshard/core/network/dht_service.py +506 -0
- neuroshard/core/network/encrypted_channel.py +141 -0
- neuroshard/core/network/nat.py +201 -0
- neuroshard/core/network/nat_traversal.py +695 -0
- neuroshard/core/network/p2p.py +929 -0
- neuroshard/core/network/p2p_data.py +150 -0
- neuroshard/core/swarm/__init__.py +106 -0
- neuroshard/core/swarm/aggregation.py +729 -0
- neuroshard/core/swarm/buffers.py +643 -0
- neuroshard/core/swarm/checkpoint.py +709 -0
- neuroshard/core/swarm/compute.py +624 -0
- neuroshard/core/swarm/diloco.py +844 -0
- neuroshard/core/swarm/factory.py +1288 -0
- neuroshard/core/swarm/heartbeat.py +669 -0
- neuroshard/core/swarm/logger.py +487 -0
- neuroshard/core/swarm/router.py +658 -0
- neuroshard/core/swarm/service.py +640 -0
- neuroshard/core/training/__init__.py +29 -0
- neuroshard/core/training/checkpoint.py +600 -0
- neuroshard/core/training/distributed.py +1602 -0
- neuroshard/core/training/global_tracker.py +617 -0
- neuroshard/core/training/production.py +276 -0
- neuroshard/governance_cli.py +729 -0
- neuroshard/grpc_server.py +895 -0
- neuroshard/runner.py +3223 -0
- neuroshard/sdk/__init__.py +92 -0
- neuroshard/sdk/client.py +990 -0
- neuroshard/sdk/errors.py +101 -0
- neuroshard/sdk/types.py +282 -0
- neuroshard/tracker/__init__.py +0 -0
- neuroshard/tracker/server.py +864 -0
- neuroshard/ui/__init__.py +0 -0
- neuroshard/ui/app.py +102 -0
- neuroshard/ui/templates/index.html +1052 -0
- neuroshard/utils/__init__.py +0 -0
- neuroshard/utils/autostart.py +81 -0
- neuroshard/utils/hardware.py +121 -0
- neuroshard/utils/serialization.py +90 -0
- neuroshard/version.py +1 -0
- nexaroa-0.0.111.dist-info/METADATA +283 -0
- nexaroa-0.0.111.dist-info/RECORD +78 -0
- nexaroa-0.0.111.dist-info/WHEEL +5 -0
- nexaroa-0.0.111.dist-info/entry_points.txt +4 -0
- nexaroa-0.0.111.dist-info/licenses/LICENSE +190 -0
- nexaroa-0.0.111.dist-info/top_level.txt +2 -0
- protos/__init__.py +0 -0
- protos/neuroshard.proto +651 -0
- protos/neuroshard_pb2.py +160 -0
- 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
|