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,545 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NEP Registry - Decentralized Proposal Storage
|
|
3
|
+
|
|
4
|
+
The registry stores all NEPs and their status. It's replicated across
|
|
5
|
+
nodes via the gossip protocol, similar to how proofs and stakes are shared.
|
|
6
|
+
|
|
7
|
+
Storage:
|
|
8
|
+
- SQLite locally (fast queries)
|
|
9
|
+
- Gossip sync (network consistency)
|
|
10
|
+
- DHT backup (persistence)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sqlite3
|
|
14
|
+
import time
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import threading
|
|
18
|
+
from typing import Dict, List, Optional, Tuple
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from .proposal import NEP, NEPType, NEPStatus, EconomicImpact, UpgradePath
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NEPRegistry:
|
|
27
|
+
"""
|
|
28
|
+
Registry of all NeuroShard Enhancement Proposals.
|
|
29
|
+
|
|
30
|
+
Responsibilities:
|
|
31
|
+
- Store and retrieve NEPs
|
|
32
|
+
- Track proposal lifecycle
|
|
33
|
+
- Enforce submission rules (e.g., stake requirement to propose)
|
|
34
|
+
- Sync with network via gossip
|
|
35
|
+
|
|
36
|
+
Economic Incentives (Stake-Proportional):
|
|
37
|
+
- Proposers pay PROPOSAL_FEE upfront (burned if spam/no-quorum)
|
|
38
|
+
- Rewards scale with total stake that participated in voting
|
|
39
|
+
- This aligns incentives: more impactful proposals = more votes = more reward
|
|
40
|
+
|
|
41
|
+
Reward Formula:
|
|
42
|
+
proposer_reward = PROPOSER_REWARD_RATE * total_stake_voted
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
- 1000 NEURO stake voted → 1000 * 0.001 = 1 NEURO reward
|
|
46
|
+
- 100,000 NEURO stake voted → 100,000 * 0.001 = 100 NEURO reward
|
|
47
|
+
- 1M NEURO stake voted → 1,000,000 * 0.001 = 1000 NEURO reward
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Minimum stake to create a proposal (prevents spam)
|
|
51
|
+
MIN_STAKE_TO_PROPOSE = 100.0 # 100 NEURO
|
|
52
|
+
|
|
53
|
+
# Proposal submission fee (held in escrow, burned if spam/no-quorum)
|
|
54
|
+
PROPOSAL_FEE = 10.0 # 10 NEURO
|
|
55
|
+
|
|
56
|
+
# Stake-proportional rewards (% of total stake that voted)
|
|
57
|
+
PROPOSER_REWARD_RATE_APPROVED = 0.001 # 0.1% of total stake voted (if approved)
|
|
58
|
+
PROPOSER_REWARD_RATE_QUORUM = 0.0001 # 0.01% of total stake voted (if rejected but quorum)
|
|
59
|
+
|
|
60
|
+
# Minimum/maximum caps to prevent extremes
|
|
61
|
+
PROPOSER_REWARD_MIN = 1.0 # At least 1 NEURO (if approved)
|
|
62
|
+
PROPOSER_REWARD_MAX = 1000.0 # At most 1000 NEURO (prevents gaming)
|
|
63
|
+
|
|
64
|
+
# Voter rewards (incentivizes participation)
|
|
65
|
+
# Reward = VOTER_REWARD_RATE * voter's_stake
|
|
66
|
+
VOTER_REWARD_RATE = 0.0001 # 0.01% of voter's stake per vote
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
db_path: str = "nep_registry.db",
|
|
71
|
+
ledger=None, # NEUROLedger for stake checks
|
|
72
|
+
):
|
|
73
|
+
self.db_path = db_path
|
|
74
|
+
self.ledger = ledger
|
|
75
|
+
self.lock = threading.Lock()
|
|
76
|
+
self._next_nep_number = 1
|
|
77
|
+
|
|
78
|
+
self._init_db()
|
|
79
|
+
self._load_next_number()
|
|
80
|
+
|
|
81
|
+
def _init_db(self):
|
|
82
|
+
"""Initialize SQLite database."""
|
|
83
|
+
with self.lock:
|
|
84
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
85
|
+
conn.execute("""
|
|
86
|
+
CREATE TABLE IF NOT EXISTS neps (
|
|
87
|
+
nep_id TEXT PRIMARY KEY,
|
|
88
|
+
nep_type TEXT NOT NULL,
|
|
89
|
+
status TEXT NOT NULL,
|
|
90
|
+
title TEXT NOT NULL,
|
|
91
|
+
author_node_id TEXT NOT NULL,
|
|
92
|
+
content_hash TEXT NOT NULL,
|
|
93
|
+
created_at REAL NOT NULL,
|
|
94
|
+
updated_at REAL NOT NULL,
|
|
95
|
+
voting_start REAL,
|
|
96
|
+
voting_end REAL,
|
|
97
|
+
activation_block INTEGER,
|
|
98
|
+
full_data TEXT NOT NULL
|
|
99
|
+
)
|
|
100
|
+
""")
|
|
101
|
+
|
|
102
|
+
conn.execute("""
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_nep_status
|
|
104
|
+
ON neps(status)
|
|
105
|
+
""")
|
|
106
|
+
|
|
107
|
+
conn.execute("""
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_nep_type
|
|
109
|
+
ON neps(nep_type)
|
|
110
|
+
""")
|
|
111
|
+
|
|
112
|
+
# Track highest NEP number
|
|
113
|
+
conn.execute("""
|
|
114
|
+
CREATE TABLE IF NOT EXISTS registry_meta (
|
|
115
|
+
key TEXT PRIMARY KEY,
|
|
116
|
+
value TEXT
|
|
117
|
+
)
|
|
118
|
+
""")
|
|
119
|
+
|
|
120
|
+
def _load_next_number(self):
|
|
121
|
+
"""Load the next NEP number from database."""
|
|
122
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
123
|
+
row = conn.execute(
|
|
124
|
+
"SELECT value FROM registry_meta WHERE key = 'next_nep_number'"
|
|
125
|
+
).fetchone()
|
|
126
|
+
|
|
127
|
+
if row:
|
|
128
|
+
self._next_nep_number = int(row[0])
|
|
129
|
+
else:
|
|
130
|
+
# Count existing NEPs and set next
|
|
131
|
+
count = conn.execute("SELECT COUNT(*) FROM neps").fetchone()[0]
|
|
132
|
+
self._next_nep_number = count + 1
|
|
133
|
+
|
|
134
|
+
def _allocate_nep_id(self) -> str:
|
|
135
|
+
"""Allocate the next NEP ID."""
|
|
136
|
+
nep_id = f"NEP-{self._next_nep_number:03d}"
|
|
137
|
+
self._next_nep_number += 1
|
|
138
|
+
|
|
139
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
140
|
+
conn.execute(
|
|
141
|
+
"INSERT OR REPLACE INTO registry_meta (key, value) VALUES (?, ?)",
|
|
142
|
+
("next_nep_number", str(self._next_nep_number))
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return nep_id
|
|
146
|
+
|
|
147
|
+
def submit_proposal(
|
|
148
|
+
self,
|
|
149
|
+
nep: NEP,
|
|
150
|
+
node_id: str,
|
|
151
|
+
signature: str,
|
|
152
|
+
) -> Tuple[bool, str, Optional[str]]:
|
|
153
|
+
"""
|
|
154
|
+
Submit a new proposal to the registry.
|
|
155
|
+
|
|
156
|
+
Requirements:
|
|
157
|
+
1. Author must have MIN_STAKE_TO_PROPOSE staked
|
|
158
|
+
2. Proposal must be properly signed
|
|
159
|
+
3. Proposal fee is burned
|
|
160
|
+
|
|
161
|
+
Returns: (success, message, nep_id)
|
|
162
|
+
"""
|
|
163
|
+
# Check stake requirement
|
|
164
|
+
if self.ledger:
|
|
165
|
+
stake = self.ledger.get_local_stake(node_id)
|
|
166
|
+
if stake < self.MIN_STAKE_TO_PROPOSE:
|
|
167
|
+
return False, f"Insufficient stake to propose: {stake:.2f} < {self.MIN_STAKE_TO_PROPOSE}", None
|
|
168
|
+
|
|
169
|
+
# Verify signature
|
|
170
|
+
# (In production, verify with ECDSA)
|
|
171
|
+
if not signature:
|
|
172
|
+
return False, "Proposal must be signed", None
|
|
173
|
+
|
|
174
|
+
# Allocate official NEP ID
|
|
175
|
+
nep_id = self._allocate_nep_id()
|
|
176
|
+
nep.nep_id = nep_id
|
|
177
|
+
nep.status = NEPStatus.DRAFT
|
|
178
|
+
nep.signature = signature
|
|
179
|
+
nep.updated_at = time.time()
|
|
180
|
+
|
|
181
|
+
# Store in database
|
|
182
|
+
with self.lock:
|
|
183
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
184
|
+
conn.execute("""
|
|
185
|
+
INSERT INTO neps
|
|
186
|
+
(nep_id, nep_type, status, title, author_node_id,
|
|
187
|
+
content_hash, created_at, updated_at, full_data)
|
|
188
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
189
|
+
""", (
|
|
190
|
+
nep.nep_id,
|
|
191
|
+
nep.nep_type.value,
|
|
192
|
+
nep.status.value,
|
|
193
|
+
nep.title,
|
|
194
|
+
nep.author_node_id,
|
|
195
|
+
nep.content_hash,
|
|
196
|
+
nep.created_at,
|
|
197
|
+
nep.updated_at,
|
|
198
|
+
json.dumps(nep.to_dict()),
|
|
199
|
+
))
|
|
200
|
+
|
|
201
|
+
logger.info(f"Submitted proposal: {nep.summary()}")
|
|
202
|
+
|
|
203
|
+
# Burn proposal fee
|
|
204
|
+
if self.ledger:
|
|
205
|
+
# TODO: Implement fee burn
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
return True, f"Proposal submitted as {nep_id}", nep_id
|
|
209
|
+
|
|
210
|
+
def get_proposal(self, nep_id: str) -> Optional[NEP]:
|
|
211
|
+
"""Retrieve a proposal by ID."""
|
|
212
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
213
|
+
row = conn.execute(
|
|
214
|
+
"SELECT full_data FROM neps WHERE nep_id = ?",
|
|
215
|
+
(nep_id,)
|
|
216
|
+
).fetchone()
|
|
217
|
+
|
|
218
|
+
if not row:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
return NEP.from_dict(json.loads(row[0]))
|
|
222
|
+
|
|
223
|
+
def update_status(
|
|
224
|
+
self,
|
|
225
|
+
nep_id: str,
|
|
226
|
+
new_status: NEPStatus,
|
|
227
|
+
voting_start: float = None,
|
|
228
|
+
voting_end: float = None,
|
|
229
|
+
activation_block: int = None,
|
|
230
|
+
) -> bool:
|
|
231
|
+
"""Update a proposal's status."""
|
|
232
|
+
with self.lock:
|
|
233
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
234
|
+
# Get current NEP
|
|
235
|
+
row = conn.execute(
|
|
236
|
+
"SELECT full_data FROM neps WHERE nep_id = ?",
|
|
237
|
+
(nep_id,)
|
|
238
|
+
).fetchone()
|
|
239
|
+
|
|
240
|
+
if not row:
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
nep = NEP.from_dict(json.loads(row[0]))
|
|
244
|
+
nep.status = new_status
|
|
245
|
+
nep.updated_at = time.time()
|
|
246
|
+
|
|
247
|
+
if voting_start:
|
|
248
|
+
nep.voting_start = voting_start
|
|
249
|
+
if voting_end:
|
|
250
|
+
nep.voting_end = voting_end
|
|
251
|
+
if activation_block:
|
|
252
|
+
nep.activation_block = activation_block
|
|
253
|
+
|
|
254
|
+
conn.execute("""
|
|
255
|
+
UPDATE neps SET
|
|
256
|
+
status = ?,
|
|
257
|
+
voting_start = ?,
|
|
258
|
+
voting_end = ?,
|
|
259
|
+
activation_block = ?,
|
|
260
|
+
updated_at = ?,
|
|
261
|
+
full_data = ?
|
|
262
|
+
WHERE nep_id = ?
|
|
263
|
+
""", (
|
|
264
|
+
new_status.value,
|
|
265
|
+
nep.voting_start,
|
|
266
|
+
nep.voting_end,
|
|
267
|
+
nep.activation_block,
|
|
268
|
+
nep.updated_at,
|
|
269
|
+
json.dumps(nep.to_dict()),
|
|
270
|
+
nep_id,
|
|
271
|
+
))
|
|
272
|
+
|
|
273
|
+
logger.info(f"Updated {nep_id} status to {new_status.value}")
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
def start_voting(
|
|
277
|
+
self,
|
|
278
|
+
nep_id: str,
|
|
279
|
+
voting_duration_days: int = 7,
|
|
280
|
+
) -> Tuple[bool, str]:
|
|
281
|
+
"""Start the voting period for a proposal."""
|
|
282
|
+
nep = self.get_proposal(nep_id)
|
|
283
|
+
if not nep:
|
|
284
|
+
return False, "Proposal not found"
|
|
285
|
+
|
|
286
|
+
if nep.status not in [NEPStatus.DRAFT, NEPStatus.REVIEW]:
|
|
287
|
+
return False, f"Cannot start voting from status: {nep.status.value}"
|
|
288
|
+
|
|
289
|
+
voting_start = time.time()
|
|
290
|
+
voting_end = voting_start + (voting_duration_days * 24 * 3600)
|
|
291
|
+
|
|
292
|
+
self.update_status(
|
|
293
|
+
nep_id,
|
|
294
|
+
NEPStatus.VOTING,
|
|
295
|
+
voting_start=voting_start,
|
|
296
|
+
voting_end=voting_end,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return True, f"Voting started, ends in {voting_duration_days} days"
|
|
300
|
+
|
|
301
|
+
def list_proposals(
|
|
302
|
+
self,
|
|
303
|
+
status: NEPStatus = None,
|
|
304
|
+
nep_type: NEPType = None,
|
|
305
|
+
limit: int = 100,
|
|
306
|
+
) -> List[NEP]:
|
|
307
|
+
"""List proposals with optional filtering."""
|
|
308
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
309
|
+
query = "SELECT full_data FROM neps WHERE 1=1"
|
|
310
|
+
params = []
|
|
311
|
+
|
|
312
|
+
if status:
|
|
313
|
+
query += " AND status = ?"
|
|
314
|
+
params.append(status.value)
|
|
315
|
+
|
|
316
|
+
if nep_type:
|
|
317
|
+
query += " AND nep_type = ?"
|
|
318
|
+
params.append(nep_type.value)
|
|
319
|
+
|
|
320
|
+
query += " ORDER BY created_at DESC LIMIT ?"
|
|
321
|
+
params.append(limit)
|
|
322
|
+
|
|
323
|
+
rows = conn.execute(query, params).fetchall()
|
|
324
|
+
|
|
325
|
+
return [NEP.from_dict(json.loads(row[0])) for row in rows]
|
|
326
|
+
|
|
327
|
+
def get_active_config(self) -> Dict[str, any]:
|
|
328
|
+
"""
|
|
329
|
+
Get the current active configuration based on all ACTIVE NEPs.
|
|
330
|
+
|
|
331
|
+
This is what nodes use to determine current protocol parameters.
|
|
332
|
+
"""
|
|
333
|
+
config = {}
|
|
334
|
+
|
|
335
|
+
active_neps = self.list_proposals(status=NEPStatus.ACTIVE)
|
|
336
|
+
|
|
337
|
+
# Sort by activation order (older first)
|
|
338
|
+
active_neps.sort(key=lambda n: n.activation_block or 0)
|
|
339
|
+
|
|
340
|
+
# Apply parameter changes in order
|
|
341
|
+
for nep in active_neps:
|
|
342
|
+
for change in nep.parameter_changes:
|
|
343
|
+
key = f"{change.module}.{change.parameter}"
|
|
344
|
+
config[key] = change.new_value
|
|
345
|
+
|
|
346
|
+
return config
|
|
347
|
+
|
|
348
|
+
def to_gossip_dict(self, nep_id: str) -> Optional[Dict]:
|
|
349
|
+
"""Serialize a NEP for gossip protocol."""
|
|
350
|
+
nep = self.get_proposal(nep_id)
|
|
351
|
+
if not nep:
|
|
352
|
+
return None
|
|
353
|
+
return nep.to_dict()
|
|
354
|
+
|
|
355
|
+
def from_gossip_dict(self, data: Dict) -> bool:
|
|
356
|
+
"""
|
|
357
|
+
Receive a NEP from gossip and store it.
|
|
358
|
+
|
|
359
|
+
Returns True if the NEP was new/updated.
|
|
360
|
+
"""
|
|
361
|
+
try:
|
|
362
|
+
incoming_nep = NEP.from_dict(data)
|
|
363
|
+
|
|
364
|
+
existing = self.get_proposal(incoming_nep.nep_id)
|
|
365
|
+
|
|
366
|
+
if existing:
|
|
367
|
+
# Only update if incoming is newer
|
|
368
|
+
if incoming_nep.updated_at > existing.updated_at:
|
|
369
|
+
with self.lock:
|
|
370
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
371
|
+
conn.execute("""
|
|
372
|
+
UPDATE neps SET
|
|
373
|
+
status = ?,
|
|
374
|
+
voting_start = ?,
|
|
375
|
+
voting_end = ?,
|
|
376
|
+
activation_block = ?,
|
|
377
|
+
updated_at = ?,
|
|
378
|
+
full_data = ?
|
|
379
|
+
WHERE nep_id = ?
|
|
380
|
+
""", (
|
|
381
|
+
incoming_nep.status.value,
|
|
382
|
+
incoming_nep.voting_start,
|
|
383
|
+
incoming_nep.voting_end,
|
|
384
|
+
incoming_nep.activation_block,
|
|
385
|
+
incoming_nep.updated_at,
|
|
386
|
+
json.dumps(incoming_nep.to_dict()),
|
|
387
|
+
incoming_nep.nep_id,
|
|
388
|
+
))
|
|
389
|
+
return True
|
|
390
|
+
return False
|
|
391
|
+
else:
|
|
392
|
+
# New NEP
|
|
393
|
+
with self.lock:
|
|
394
|
+
with sqlite3.connect(self.db_path, timeout=60.0) as conn:
|
|
395
|
+
conn.execute("""
|
|
396
|
+
INSERT INTO neps
|
|
397
|
+
(nep_id, nep_type, status, title, author_node_id,
|
|
398
|
+
content_hash, created_at, updated_at,
|
|
399
|
+
voting_start, voting_end, activation_block, full_data)
|
|
400
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
401
|
+
""", (
|
|
402
|
+
incoming_nep.nep_id,
|
|
403
|
+
incoming_nep.nep_type.value,
|
|
404
|
+
incoming_nep.status.value,
|
|
405
|
+
incoming_nep.title,
|
|
406
|
+
incoming_nep.author_node_id,
|
|
407
|
+
incoming_nep.content_hash,
|
|
408
|
+
incoming_nep.created_at,
|
|
409
|
+
incoming_nep.updated_at,
|
|
410
|
+
incoming_nep.voting_start,
|
|
411
|
+
incoming_nep.voting_end,
|
|
412
|
+
incoming_nep.activation_block,
|
|
413
|
+
json.dumps(incoming_nep.to_dict()),
|
|
414
|
+
))
|
|
415
|
+
|
|
416
|
+
logger.info(f"Received new NEP via gossip: {incoming_nep.summary()}")
|
|
417
|
+
return True
|
|
418
|
+
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.error(f"Failed to process gossip NEP: {e}")
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def finalize_proposal(
|
|
425
|
+
self,
|
|
426
|
+
nep_id: str,
|
|
427
|
+
approved: bool,
|
|
428
|
+
quorum_reached: bool,
|
|
429
|
+
total_stake_voted: float = 0.0,
|
|
430
|
+
) -> Tuple[bool, str, float]:
|
|
431
|
+
"""
|
|
432
|
+
Finalize a proposal after voting ends.
|
|
433
|
+
|
|
434
|
+
Rewards are STAKE-PROPORTIONAL:
|
|
435
|
+
- Approved: reward = 0.1% of total_stake_voted + fee refund
|
|
436
|
+
- Rejected w/ quorum: reward = 0.01% of total_stake_voted (community engaged)
|
|
437
|
+
- Rejected w/o quorum: Fee burned (nobody cared = spam)
|
|
438
|
+
|
|
439
|
+
This means:
|
|
440
|
+
- A proposal that engaged 100K NEURO of stake → ~100 NEURO reward
|
|
441
|
+
- A proposal that engaged 1M NEURO of stake → ~1000 NEURO reward (capped)
|
|
442
|
+
- More engagement = more reward = incentive for quality proposals
|
|
443
|
+
|
|
444
|
+
Returns: (success, message, reward_amount)
|
|
445
|
+
"""
|
|
446
|
+
nep = self.get_proposal(nep_id)
|
|
447
|
+
if not nep:
|
|
448
|
+
return False, "Proposal not found", 0.0
|
|
449
|
+
|
|
450
|
+
if nep.status != NEPStatus.VOTING:
|
|
451
|
+
return False, f"Cannot finalize from status: {nep.status.value}", 0.0
|
|
452
|
+
|
|
453
|
+
reward = 0.0
|
|
454
|
+
|
|
455
|
+
if approved:
|
|
456
|
+
# Proposal passed! Reward proportional to engagement
|
|
457
|
+
new_status = NEPStatus.APPROVED
|
|
458
|
+
|
|
459
|
+
# Calculate stake-proportional reward
|
|
460
|
+
base_reward = total_stake_voted * self.PROPOSER_REWARD_RATE_APPROVED
|
|
461
|
+
base_reward = max(base_reward, self.PROPOSER_REWARD_MIN) # At least minimum
|
|
462
|
+
base_reward = min(base_reward, self.PROPOSER_REWARD_MAX) # Cap at maximum
|
|
463
|
+
|
|
464
|
+
reward = base_reward + self.PROPOSAL_FEE # Plus fee refund
|
|
465
|
+
|
|
466
|
+
message = (
|
|
467
|
+
f"Approved! Proposer earns {reward:.2f} NEURO "
|
|
468
|
+
f"({base_reward:.2f} reward + {self.PROPOSAL_FEE} fee refund, "
|
|
469
|
+
f"based on {total_stake_voted:.0f} NEURO stake participation)"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# Credit reward to author
|
|
473
|
+
if self.ledger:
|
|
474
|
+
try:
|
|
475
|
+
self.ledger._credit_local_balance(
|
|
476
|
+
nep.author_node_id,
|
|
477
|
+
reward,
|
|
478
|
+
f"NEP approved: {nep_id} (stake participation: {total_stake_voted:.0f})"
|
|
479
|
+
)
|
|
480
|
+
except Exception as e:
|
|
481
|
+
logger.error(f"Failed to credit proposer reward: {e}")
|
|
482
|
+
|
|
483
|
+
elif quorum_reached:
|
|
484
|
+
# Rejected but community engaged - smaller proportional reward
|
|
485
|
+
new_status = NEPStatus.REJECTED
|
|
486
|
+
|
|
487
|
+
# Smaller rate for rejected proposals, but still rewards engagement
|
|
488
|
+
reward = total_stake_voted * self.PROPOSER_REWARD_RATE_QUORUM
|
|
489
|
+
reward = min(reward, self.PROPOSER_REWARD_MAX * 0.1) # Cap at 10% of max
|
|
490
|
+
|
|
491
|
+
message = (
|
|
492
|
+
f"Rejected (quorum reached). Proposer receives {reward:.2f} NEURO "
|
|
493
|
+
f"(based on {total_stake_voted:.0f} NEURO stake participation)"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
if self.ledger and reward > 0:
|
|
497
|
+
try:
|
|
498
|
+
self.ledger._credit_local_balance(
|
|
499
|
+
nep.author_node_id,
|
|
500
|
+
reward,
|
|
501
|
+
f"NEP rejected (quorum): {nep_id}"
|
|
502
|
+
)
|
|
503
|
+
except Exception as e:
|
|
504
|
+
logger.error(f"Failed to credit partial refund: {e}")
|
|
505
|
+
else:
|
|
506
|
+
# No quorum - fee burned (anti-spam)
|
|
507
|
+
new_status = NEPStatus.REJECTED
|
|
508
|
+
reward = 0.0
|
|
509
|
+
message = f"Rejected (no quorum). Fee of {self.PROPOSAL_FEE} NEURO burned."
|
|
510
|
+
logger.info(f"Burned {self.PROPOSAL_FEE} NEURO for rejected proposal {nep_id}")
|
|
511
|
+
|
|
512
|
+
# Update status
|
|
513
|
+
self.update_status(nep_id, new_status)
|
|
514
|
+
|
|
515
|
+
logger.info(f"Finalized {nep_id}: {message}")
|
|
516
|
+
return True, message, reward
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
def calculate_expected_reward(total_stake_voted: float, approved: bool = True) -> float:
|
|
520
|
+
"""
|
|
521
|
+
Calculate expected reward for a proposal based on stake participation.
|
|
522
|
+
|
|
523
|
+
Useful for UI to show "potential reward" before voting ends.
|
|
524
|
+
"""
|
|
525
|
+
if approved:
|
|
526
|
+
reward = total_stake_voted * NEPRegistry.PROPOSER_REWARD_RATE_APPROVED
|
|
527
|
+
reward = max(reward, NEPRegistry.PROPOSER_REWARD_MIN)
|
|
528
|
+
reward = min(reward, NEPRegistry.PROPOSER_REWARD_MAX)
|
|
529
|
+
return reward + NEPRegistry.PROPOSAL_FEE
|
|
530
|
+
else:
|
|
531
|
+
reward = total_stake_voted * NEPRegistry.PROPOSER_REWARD_RATE_QUORUM
|
|
532
|
+
return min(reward, NEPRegistry.PROPOSER_REWARD_MAX * 0.1)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# Convenience functions
|
|
536
|
+
def get_active_neps(registry: NEPRegistry) -> List[NEP]:
|
|
537
|
+
"""Get all currently active NEPs."""
|
|
538
|
+
return registry.list_proposals(status=NEPStatus.ACTIVE)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def get_pending_neps(registry: NEPRegistry) -> List[NEP]:
|
|
542
|
+
"""Get all NEPs pending vote or activation."""
|
|
543
|
+
voting = registry.list_proposals(status=NEPStatus.VOTING)
|
|
544
|
+
scheduled = registry.list_proposals(status=NEPStatus.SCHEDULED)
|
|
545
|
+
return voting + scheduled
|