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,1004 @@
1
+ """
2
+ Zexus Blockchain — Upgradeable Contracts & Chains
3
+ ==================================================
4
+
5
+ Implements the **Transparent Proxy** and **UUPS (Universal Upgradeable
6
+ Proxy Standard)** patterns adapted for the Zexus runtime, plus a
7
+ **chain governance upgrade** mechanism that allows live-upgrading
8
+ consensus parameters, block format versions, and fee structures
9
+ through on-chain proposals.
10
+
11
+ Architecture
12
+ ------------
13
+
14
+ Smart-Contract Upgrades
15
+ ^^^^^^^^^^^^^^^^^^^^^^^
16
+ ::
17
+
18
+ ┌──────────────┐ delegatecall ┌─────────────────────┐
19
+ │ ProxyContract│ ───────────────► │ ImplementationV1/V2 │
20
+ │ (storage) │ │ (stateless logic) │
21
+ └──────────────┘ └─────────────────────┘
22
+
23
+
24
+ ┌──────────────┐
25
+ │ UpgradeManager│ version registry + access control
26
+ └──────────────┘
27
+
28
+ * ``ProxyContract`` holds all persistent storage. Calls are forwarded
29
+ via ``delegate_call`` to the current implementation.
30
+ * ``UpgradeManager`` tracks the version history, enforces role-based
31
+ access (admin / multisig), and provides rollback capability.
32
+
33
+ Chain Upgrades
34
+ ^^^^^^^^^^^^^^
35
+ ::
36
+
37
+ Proposal ──► Vote ──► Ratify ──► Apply
38
+
39
+ * ``ChainUpgradeGovernance`` lets validators propose parameter
40
+ changes (difficulty, gas limits, block time, consensus algorithm).
41
+ * A supermajority vote (configurable quorum, default 2/3+1) is
42
+ needed to ratify.
43
+ * The new parameters take effect at a specific block height, giving
44
+ all nodes time to prepare.
45
+
46
+ Security
47
+ --------
48
+ * **Upgrade delay**: configurable cool-down between upgrade
49
+ proposals to prevent rapid hostile takeover.
50
+ * **Rollback**: any upgrade can be reverted to its predecessor.
51
+ * **Storage collision protection**: implementation slots use
52
+ keccak256-based storage keys.
53
+ * **Event audit trail**: every upgrade emits logs indexable by
54
+ ``EventIndex``.
55
+ """
56
+
57
+ from __future__ import annotations
58
+
59
+ import copy
60
+ import hashlib
61
+ import json
62
+ import time
63
+ import logging
64
+ from dataclasses import dataclass, field
65
+ from enum import Enum, IntEnum
66
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple
67
+
68
+ logger = logging.getLogger("zexus.blockchain.upgradeable")
69
+
70
+
71
+ # =====================================================================
72
+ # Constants
73
+ # =====================================================================
74
+
75
+ # Storage slot for the implementation address (EIP-1967-style)
76
+ _IMPL_SLOT = hashlib.sha256(b"zexus.proxy.implementation").hexdigest()
77
+ _ADMIN_SLOT = hashlib.sha256(b"zexus.proxy.admin").hexdigest()
78
+ _VERSION_SLOT = hashlib.sha256(b"zexus.proxy.version").hexdigest()
79
+
80
+
81
+ # =====================================================================
82
+ # Upgrade Events
83
+ # =====================================================================
84
+
85
+ class UpgradeEventType(str, Enum):
86
+ CONTRACT_UPGRADED = "ContractUpgraded"
87
+ CONTRACT_ROLLED_BACK = "ContractRolledBack"
88
+ ADMIN_CHANGED = "AdminChanged"
89
+ CHAIN_PROPOSAL_CREATED = "ChainProposalCreated"
90
+ CHAIN_PROPOSAL_VOTED = "ChainProposalVoted"
91
+ CHAIN_UPGRADE_APPLIED = "ChainUpgradeApplied"
92
+ CHAIN_UPGRADE_REVERTED = "ChainUpgradeReverted"
93
+
94
+
95
+ @dataclass
96
+ class UpgradeEvent:
97
+ """Immutable audit record for an upgrade action."""
98
+ event_type: UpgradeEventType
99
+ timestamp: float = field(default_factory=time.time)
100
+ data: Dict[str, Any] = field(default_factory=dict)
101
+
102
+ def to_dict(self) -> Dict[str, Any]:
103
+ return {
104
+ "event_type": self.event_type.value,
105
+ "timestamp": self.timestamp,
106
+ "data": self.data,
107
+ }
108
+
109
+
110
+ # =====================================================================
111
+ # Implementation Version Registry
112
+ # =====================================================================
113
+
114
+ @dataclass
115
+ class ImplementationRecord:
116
+ """Metadata about a single implementation version."""
117
+ version: int
118
+ address: str # address of the implementation contract
119
+ deployer: str # who deployed it
120
+ timestamp: float = field(default_factory=time.time)
121
+ code_hash: str = "" # hash of the implementation's code/name
122
+ metadata: Dict[str, Any] = field(default_factory=dict)
123
+
124
+ def __post_init__(self) -> None:
125
+ if not self.code_hash:
126
+ # Deterministic default: bind code hash to implementation address
127
+ self.code_hash = hashlib.sha256(str(self.address).encode()).hexdigest()
128
+
129
+ def to_dict(self) -> Dict[str, Any]:
130
+ return {
131
+ "version": self.version,
132
+ "address": self.address,
133
+ "deployer": self.deployer,
134
+ "timestamp": self.timestamp,
135
+ "code_hash": self.code_hash,
136
+ "metadata": self.metadata,
137
+ }
138
+
139
+ @classmethod
140
+ def from_dict(cls, d: Dict[str, Any]) -> "ImplementationRecord":
141
+ return cls(
142
+ version=d["version"],
143
+ address=d["address"],
144
+ deployer=d["deployer"],
145
+ timestamp=d["timestamp"],
146
+ code_hash=d["code_hash"],
147
+ metadata=d.get("metadata", {}),
148
+ )
149
+
150
+
151
+ # =====================================================================
152
+ # Proxy Contract
153
+ # =====================================================================
154
+
155
+ class ProxyContract:
156
+ """Transparent proxy — stores state, delegates logic.
157
+
158
+ The proxy has its own persistent storage dictionary. All action
159
+ calls are forwarded to the *current implementation* contract via
160
+ ``ContractVM.delegate_call`` semantics (the implementation's code
161
+ runs against the proxy's storage).
162
+
163
+ Parameters
164
+ ----------
165
+ admin : str
166
+ Address authorised to upgrade.
167
+ implementation_address : str, optional
168
+ Initial implementation contract address.
169
+ proxy_address : str, optional
170
+ Explicit address for the proxy itself.
171
+ """
172
+
173
+ def __init__(
174
+ self,
175
+ admin: str,
176
+ implementation_address: str = "",
177
+ proxy_address: Optional[str] = None,
178
+ ):
179
+ self.address: str = proxy_address or hashlib.sha256(
180
+ f"proxy-{admin}-{time.time()}".encode()
181
+ ).hexdigest()[:40]
182
+
183
+ # Internal storage slots (EIP-1967 pattern)
184
+ self._storage: Dict[str, Any] = {
185
+ _IMPL_SLOT: implementation_address,
186
+ _ADMIN_SLOT: admin,
187
+ _VERSION_SLOT: 0,
188
+ }
189
+
190
+ # User-facing storage (the contract's data)
191
+ self.data: Dict[str, Any] = {}
192
+
193
+ # ── Properties ────────────────────────────────────────────────
194
+
195
+ @property
196
+ def implementation(self) -> str:
197
+ return self._storage[_IMPL_SLOT]
198
+
199
+ @implementation.setter
200
+ def implementation(self, addr: str) -> None:
201
+ self._storage[_IMPL_SLOT] = addr
202
+
203
+ @property
204
+ def admin(self) -> str:
205
+ return self._storage[_ADMIN_SLOT]
206
+
207
+ @admin.setter
208
+ def admin(self, addr: str) -> None:
209
+ self._storage[_ADMIN_SLOT] = addr
210
+
211
+ @property
212
+ def version(self) -> int:
213
+ return self._storage[_VERSION_SLOT]
214
+
215
+ @version.setter
216
+ def version(self, v: int) -> None:
217
+ self._storage[_VERSION_SLOT] = v
218
+
219
+ # ── Serialization ─────────────────────────────────────────────
220
+
221
+ def to_dict(self) -> Dict[str, Any]:
222
+ # Public shape intentionally mirrors common proxy contract APIs
223
+ # and is what the Python test suite expects.
224
+ return {
225
+ "address": self.address,
226
+ "admin": self.admin,
227
+ "implementation": self.implementation,
228
+ "version": self.version,
229
+ "data": dict(self.data),
230
+ # Preserve low-level slots for debugging/forensics
231
+ "storage": dict(self._storage),
232
+ }
233
+
234
+ @classmethod
235
+ def from_dict(cls, d: Dict[str, Any]) -> "ProxyContract":
236
+ # Backward/forward compatible loader: accepts either the public keys
237
+ # (admin/implementation/version) or the internal storage slot mapping.
238
+ address = d.get("address")
239
+ admin = d.get("admin")
240
+ impl = d.get("implementation")
241
+ version = d.get("version")
242
+ storage = d.get("storage")
243
+
244
+ p = cls.__new__(cls)
245
+ p.address = address
246
+ if isinstance(storage, dict):
247
+ p._storage = dict(storage)
248
+ else:
249
+ p._storage = {
250
+ _IMPL_SLOT: impl or "",
251
+ _ADMIN_SLOT: admin or "",
252
+ _VERSION_SLOT: int(version or 0),
253
+ }
254
+ # If explicit public keys exist, prefer them
255
+ if admin is not None:
256
+ p._storage[_ADMIN_SLOT] = admin
257
+ if impl is not None:
258
+ p._storage[_IMPL_SLOT] = impl
259
+ if version is not None:
260
+ p._storage[_VERSION_SLOT] = int(version)
261
+
262
+ p.data = dict(d.get("data", {}))
263
+ return p
264
+
265
+
266
+ # =====================================================================
267
+ # Upgrade Manager (contract-level upgrades)
268
+ # =====================================================================
269
+
270
+ class UpgradeManager:
271
+ """Manages the lifecycle of upgradeable proxy contracts.
272
+
273
+ Responsibilities:
274
+ * Register implementation versions for a proxy.
275
+ * Enforce admin-only access for upgrades.
276
+ * Maintain a full version history with rollback support.
277
+ * Emit audit events for every mutation.
278
+
279
+ Parameters
280
+ ----------
281
+ contract_vm : ContractVM, optional
282
+ The VM bridge — used for ``delegate_call`` when executing
283
+ through the proxy. Can be ``None`` for pure state management.
284
+ upgrade_delay : float
285
+ Minimum seconds between consecutive upgrades (default 0 —
286
+ no delay for dev networks; set higher for production).
287
+ """
288
+
289
+ def __init__(
290
+ self,
291
+ contract_vm=None,
292
+ upgrade_delay: float = 0.0,
293
+ ):
294
+ self._vm = contract_vm
295
+ self._upgrade_delay = upgrade_delay
296
+
297
+ # proxy_address -> list of ImplementationRecord (ordered by version)
298
+ self._versions: Dict[str, List[ImplementationRecord]] = {}
299
+
300
+ # proxy_address -> ProxyContract
301
+ self._proxies: Dict[str, ProxyContract] = {}
302
+
303
+ # Audit log
304
+ self._events: List[UpgradeEvent] = []
305
+
306
+ # Timestamp of last upgrade per proxy (for delay enforcement)
307
+ self._last_upgrade_time: Dict[str, float] = {}
308
+
309
+ # ── Proxy lifecycle ───────────────────────────────────────────
310
+
311
+ def create_proxy(
312
+ self,
313
+ admin: str,
314
+ implementation_address: str,
315
+ implementation_code_hash: str = "",
316
+ proxy_address: Optional[str] = None,
317
+ metadata: Optional[Dict[str, Any]] = None,
318
+ ) -> ProxyContract:
319
+ """Create a new upgradeable proxy pointing at an implementation.
320
+
321
+ Returns the ``ProxyContract`` instance.
322
+ """
323
+ proxy = ProxyContract(
324
+ admin=admin,
325
+ implementation_address=implementation_address,
326
+ proxy_address=proxy_address,
327
+ )
328
+ proxy.version = 1
329
+
330
+ record = ImplementationRecord(
331
+ version=1,
332
+ address=implementation_address,
333
+ deployer=admin,
334
+ timestamp=time.time(),
335
+ code_hash=implementation_code_hash or hashlib.sha256(
336
+ implementation_address.encode()
337
+ ).hexdigest(),
338
+ metadata=metadata or {},
339
+ )
340
+
341
+ self._versions[proxy.address] = [record]
342
+ self._proxies[proxy.address] = proxy
343
+ self._last_upgrade_time[proxy.address] = time.time()
344
+
345
+ self._emit(UpgradeEventType.CONTRACT_UPGRADED, {
346
+ "proxy": proxy.address,
347
+ "implementation": implementation_address,
348
+ "version": 1,
349
+ "admin": admin,
350
+ })
351
+
352
+ logger.info(
353
+ "Proxy %s created → impl %s (v1)",
354
+ proxy.address, implementation_address,
355
+ )
356
+ return proxy
357
+
358
+ def upgrade(
359
+ self,
360
+ proxy_address: str,
361
+ new_implementation: str,
362
+ caller: str,
363
+ code_hash: str = "",
364
+ metadata: Optional[Dict[str, Any]] = None,
365
+ migrate_fn: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
366
+ ) -> Tuple[bool, str]:
367
+ """Upgrade a proxy to a new implementation.
368
+
369
+ Parameters
370
+ ----------
371
+ proxy_address :
372
+ The proxy to upgrade.
373
+ new_implementation :
374
+ Address of the new implementation contract.
375
+ caller :
376
+ Must match the proxy's admin.
377
+ code_hash :
378
+ Optional hash of the new implementation code for
379
+ integrity verification.
380
+ metadata :
381
+ Arbitrary metadata (e.g. changelog, audit report hash).
382
+ migrate_fn :
383
+ Optional data-migration callback ``(old_data) -> new_data``
384
+ that transforms proxy storage during the upgrade.
385
+
386
+ Returns ``(success, message)``.
387
+ """
388
+ proxy = self._proxies.get(proxy_address)
389
+ if proxy is None:
390
+ return False, f"Proxy not found: {proxy_address}"
391
+
392
+ # Access control
393
+ if caller != proxy.admin:
394
+ return False, f"Caller {caller} is not admin ({proxy.admin})"
395
+
396
+ # Delay enforcement
397
+ last = self._last_upgrade_time.get(proxy_address, 0.0)
398
+ elapsed = time.time() - last
399
+ if elapsed < self._upgrade_delay:
400
+ remaining = self._upgrade_delay - elapsed
401
+ return False, f"Upgrade delay not met: {remaining:.1f}s remaining"
402
+
403
+ # Build new version record
404
+ new_version = proxy.version + 1
405
+ record = ImplementationRecord(
406
+ version=new_version,
407
+ address=new_implementation,
408
+ deployer=caller,
409
+ timestamp=time.time(),
410
+ code_hash=code_hash or hashlib.sha256(
411
+ new_implementation.encode()
412
+ ).hexdigest(),
413
+ metadata=metadata or {},
414
+ )
415
+
416
+ # Optional data migration
417
+ if migrate_fn is not None:
418
+ try:
419
+ proxy.data = migrate_fn(copy.deepcopy(proxy.data))
420
+ except Exception as exc:
421
+ return False, f"Migration failed: {exc}"
422
+
423
+ # Commit
424
+ old_impl = proxy.implementation
425
+ proxy.implementation = new_implementation
426
+ proxy.version = new_version
427
+ self._versions.setdefault(proxy_address, []).append(record)
428
+ self._last_upgrade_time[proxy_address] = time.time()
429
+
430
+ self._emit(UpgradeEventType.CONTRACT_UPGRADED, {
431
+ "proxy": proxy_address,
432
+ "old_implementation": old_impl,
433
+ "new_implementation": new_implementation,
434
+ "version": new_version,
435
+ "caller": caller,
436
+ })
437
+
438
+ logger.info(
439
+ "Proxy %s upgraded %s → %s (v%d)",
440
+ proxy_address, old_impl, new_implementation, new_version,
441
+ )
442
+ return True, f"Upgraded to v{new_version}"
443
+
444
+ def rollback(
445
+ self,
446
+ proxy_address: str,
447
+ caller: str,
448
+ target_version: Optional[int] = None,
449
+ ) -> Tuple[bool, str]:
450
+ """Roll back a proxy to a previous implementation version.
451
+
452
+ By default rolls back to the *immediately previous* version.
453
+ Pass ``target_version`` to jump to a specific version.
454
+ """
455
+ proxy = self._proxies.get(proxy_address)
456
+ if proxy is None:
457
+ return False, f"Proxy not found: {proxy_address}"
458
+
459
+ if caller != proxy.admin:
460
+ return False, f"Caller {caller} is not admin ({proxy.admin})"
461
+
462
+ versions = self._versions.get(proxy_address, [])
463
+ if len(versions) < 2:
464
+ return False, "No previous version to roll back to"
465
+
466
+ if target_version is not None:
467
+ target_record = None
468
+ for rec in versions:
469
+ if rec.version == target_version:
470
+ target_record = rec
471
+ break
472
+ if target_record is None:
473
+ return False, f"Version {target_version} not found"
474
+ else:
475
+ # Previous version
476
+ target_record = versions[-2]
477
+
478
+ old_impl = proxy.implementation
479
+ old_version = proxy.version
480
+
481
+ proxy.implementation = target_record.address
482
+ proxy.version = target_record.version
483
+
484
+ self._emit(UpgradeEventType.CONTRACT_ROLLED_BACK, {
485
+ "proxy": proxy_address,
486
+ "from_version": old_version,
487
+ "to_version": target_record.version,
488
+ "old_implementation": old_impl,
489
+ "new_implementation": target_record.address,
490
+ "caller": caller,
491
+ })
492
+
493
+ logger.info(
494
+ "Proxy %s rolled back v%d → v%d",
495
+ proxy_address, old_version, target_record.version,
496
+ )
497
+ return True, f"Rolled back to v{target_record.version}"
498
+
499
+ def change_admin(
500
+ self,
501
+ proxy_address: str,
502
+ new_admin: str,
503
+ caller: str,
504
+ ) -> Tuple[bool, str]:
505
+ """Transfer admin rights for a proxy."""
506
+ proxy = self._proxies.get(proxy_address)
507
+ if proxy is None:
508
+ return False, f"Proxy not found: {proxy_address}"
509
+ if caller != proxy.admin:
510
+ return False, f"Caller {caller} is not admin ({proxy.admin})"
511
+ if not new_admin:
512
+ return False, "New admin address cannot be empty"
513
+
514
+ old_admin = proxy.admin
515
+ proxy.admin = new_admin
516
+
517
+ self._emit(UpgradeEventType.ADMIN_CHANGED, {
518
+ "proxy": proxy_address,
519
+ "old_admin": old_admin,
520
+ "new_admin": new_admin,
521
+ })
522
+ return True, f"Admin changed to {new_admin}"
523
+
524
+ # ── Execute through proxy ─────────────────────────────────────
525
+
526
+ def proxy_call(
527
+ self,
528
+ proxy_address: str,
529
+ action: str,
530
+ args: Optional[Dict[str, Any]] = None,
531
+ caller: str = "",
532
+ gas_limit: Optional[int] = None,
533
+ ) -> Any:
534
+ """Execute an action on a proxy's current implementation.
535
+
536
+ This delegates to ``ContractVM.execute_contract`` on the
537
+ *implementation* address but uses the *proxy's* storage
538
+ context (similar to ``delegatecall``).
539
+ """
540
+ proxy = self._proxies.get(proxy_address)
541
+ if proxy is None:
542
+ raise RuntimeError(f"Proxy not found: {proxy_address}")
543
+
544
+ if not proxy.implementation:
545
+ raise RuntimeError("Proxy has no implementation set")
546
+
547
+ if self._vm is None:
548
+ raise RuntimeError("No ContractVM attached to UpgradeManager")
549
+
550
+ # Execute implementation code within proxy storage context
551
+ receipt = self._vm.execute_contract(
552
+ contract_address=proxy.implementation,
553
+ action=action,
554
+ args=args,
555
+ caller=caller,
556
+ gas_limit=gas_limit,
557
+ )
558
+
559
+ if not receipt.success:
560
+ raise RuntimeError(
561
+ f"Proxy call failed: {receipt.error or receipt.revert_reason}"
562
+ )
563
+ return receipt.return_value
564
+
565
+ # ── Queries ───────────────────────────────────────────────────
566
+
567
+ def get_proxy(self, proxy_address: str) -> Optional[ProxyContract]:
568
+ return self._proxies.get(proxy_address)
569
+
570
+ def get_version_history(
571
+ self, proxy_address: str
572
+ ) -> List[ImplementationRecord]:
573
+ return list(self._versions.get(proxy_address, []))
574
+
575
+ def get_current_version(self, proxy_address: str) -> Optional[int]:
576
+ proxy = self._proxies.get(proxy_address)
577
+ return proxy.version if proxy else None
578
+
579
+ def get_current_implementation(self, proxy_address: str) -> Optional[str]:
580
+ proxy = self._proxies.get(proxy_address)
581
+ return proxy.implementation if proxy else None
582
+
583
+ def list_proxies(self) -> List[str]:
584
+ return list(self._proxies.keys())
585
+
586
+ def get_events(self) -> List[UpgradeEvent]:
587
+ return list(self._events)
588
+
589
+ def get_info(self, proxy_address: str) -> Optional[Dict[str, Any]]:
590
+ proxy = self._proxies.get(proxy_address)
591
+ if proxy is None:
592
+ return None
593
+ versions = self._versions.get(proxy_address, [])
594
+ return {
595
+ "address": proxy.address,
596
+ "admin": proxy.admin,
597
+ "implementation": proxy.implementation,
598
+ "version": proxy.version,
599
+ "total_versions": len(versions),
600
+ "versions": [v.to_dict() for v in versions],
601
+ "data_keys": list(proxy.data.keys()),
602
+ }
603
+
604
+ # ── Internal ──────────────────────────────────────────────────
605
+
606
+ def _emit(self, event_type: UpgradeEventType, data: Dict[str, Any]) -> None:
607
+ self._events.append(UpgradeEvent(event_type=event_type, data=data))
608
+
609
+
610
+ # =====================================================================
611
+ # Chain Upgrade Governance
612
+ # =====================================================================
613
+
614
+ class ProposalStatus(str, Enum):
615
+ PENDING = "pending"
616
+ APPROVED = "approved"
617
+ REJECTED = "rejected"
618
+ APPLIED = "applied"
619
+ REVERTED = "reverted"
620
+
621
+
622
+ class ProposalType(str, Enum):
623
+ """Types of chain-level upgrades."""
624
+ CONSENSUS_CHANGE = "consensus_change"
625
+ DIFFICULTY_CHANGE = "difficulty_change"
626
+ GAS_LIMIT_CHANGE = "gas_limit_change"
627
+ BLOCK_TIME_CHANGE = "block_time_change"
628
+ FEE_STRUCTURE = "fee_structure"
629
+ CHAIN_PARAMETER = "chain_parameter"
630
+ HARD_FORK = "hard_fork"
631
+
632
+
633
+ @dataclass
634
+ class ChainUpgradeProposal:
635
+ """A proposal to change chain-level parameters."""
636
+ proposal_id: str
637
+ proposal_type: ProposalType
638
+ proposer: str
639
+ description: str
640
+ changes: Dict[str, Any] # key -> new value
641
+ activation_height: int # block height at which to apply
642
+ created_at: float = field(default_factory=time.time)
643
+ status: ProposalStatus = ProposalStatus.PENDING
644
+ votes_for: Set[str] = field(default_factory=set)
645
+ votes_against: Set[str] = field(default_factory=set)
646
+ applied_at: Optional[float] = None
647
+ previous_values: Dict[str, Any] = field(default_factory=dict)
648
+
649
+ @property
650
+ def total_votes(self) -> int:
651
+ return len(self.votes_for) + len(self.votes_against)
652
+
653
+ def to_dict(self) -> Dict[str, Any]:
654
+ return {
655
+ "proposal_id": self.proposal_id,
656
+ "proposal_type": self.proposal_type.value,
657
+ "proposer": self.proposer,
658
+ "description": self.description,
659
+ "changes": self.changes,
660
+ "activation_height": self.activation_height,
661
+ "created_at": self.created_at,
662
+ "status": self.status.value,
663
+ "votes_for": list(self.votes_for),
664
+ "votes_against": list(self.votes_against),
665
+ "applied_at": self.applied_at,
666
+ "previous_values": self.previous_values,
667
+ }
668
+
669
+ @classmethod
670
+ def from_dict(cls, d: Dict[str, Any]) -> "ChainUpgradeProposal":
671
+ p = cls(
672
+ proposal_id=d["proposal_id"],
673
+ proposal_type=ProposalType(d["proposal_type"]),
674
+ proposer=d["proposer"],
675
+ description=d["description"],
676
+ changes=d["changes"],
677
+ activation_height=d["activation_height"],
678
+ created_at=d.get("created_at", 0.0),
679
+ status=ProposalStatus(d.get("status", "pending")),
680
+ applied_at=d.get("applied_at"),
681
+ previous_values=d.get("previous_values", {}),
682
+ )
683
+ p.votes_for = set(d.get("votes_for", []))
684
+ p.votes_against = set(d.get("votes_against", []))
685
+ return p
686
+
687
+
688
+ class ChainUpgradeGovernance:
689
+ """On-chain governance for chain-level upgrades.
690
+
691
+ Validators can propose changes to consensus parameters,
692
+ difficulty, gas limits, block times, and fee structures.
693
+ A configurable quorum (default 2/3 + 1) determines whether
694
+ a proposal passes.
695
+
696
+ Parameters
697
+ ----------
698
+ chain : Chain
699
+ The chain being governed.
700
+ validators : set of str
701
+ Set of validator addresses allowed to propose & vote.
702
+ quorum_numerator : int
703
+ Numerator for quorum fraction (default 2).
704
+ quorum_denominator : int
705
+ Denominator for quorum fraction (default 3).
706
+ proposal_delay : float
707
+ Minimum seconds between proposals from the same proposer.
708
+ """
709
+
710
+ # Allowed chain parameter keys and their type validators
711
+ ALLOWED_PARAMETERS: Dict[str, type] = {
712
+ "difficulty": int,
713
+ "target_block_time": (int, float),
714
+ "chain_id": str,
715
+ "gas_limit_default": int,
716
+ "base_fee": int,
717
+ "max_block_gas": int,
718
+ "consensus_algorithm": str,
719
+ }
720
+
721
+ def __init__(
722
+ self,
723
+ chain=None,
724
+ validators: Optional[Set[str]] = None,
725
+ quorum_numerator: int = 2,
726
+ quorum_denominator: int = 3,
727
+ proposal_delay: float = 0.0,
728
+ ):
729
+ self._chain = chain
730
+ self._validators: Set[str] = set(validators or [])
731
+ self._quorum_num = quorum_numerator
732
+ self._quorum_den = quorum_denominator
733
+ self._proposal_delay = proposal_delay
734
+
735
+ # proposal_id -> ChainUpgradeProposal
736
+ self._proposals: Dict[str, ChainUpgradeProposal] = {}
737
+
738
+ # Audit trail
739
+ self._events: List[UpgradeEvent] = []
740
+
741
+ # proposer -> last proposal timestamp (for rate-limiting)
742
+ self._last_proposal_time: Dict[str, float] = {}
743
+
744
+ # Applied upgrade history (for revert)
745
+ self._applied_upgrades: List[str] = [] # ordered proposal_ids
746
+
747
+ # ── Validators ────────────────────────────────────────────────
748
+
749
+ def add_validator(self, address: str) -> None:
750
+ self._validators.add(address)
751
+
752
+ def remove_validator(self, address: str) -> None:
753
+ self._validators.discard(address)
754
+
755
+ @property
756
+ def validator_count(self) -> int:
757
+ return len(self._validators)
758
+
759
+ @property
760
+ def quorum_threshold(self) -> int:
761
+ """Minimum votes-for required to approve a proposal."""
762
+ n = len(self._validators)
763
+ return (n * self._quorum_num) // self._quorum_den + 1
764
+
765
+ # ── Proposals ─────────────────────────────────────────────────
766
+
767
+ def propose(
768
+ self,
769
+ proposer: str,
770
+ proposal_type: ProposalType,
771
+ description: str,
772
+ changes: Dict[str, Any],
773
+ activation_height: int,
774
+ ) -> Tuple[bool, str, Optional[str]]:
775
+ """Submit a new upgrade proposal.
776
+
777
+ Returns ``(success, message, proposal_id)``.
778
+ """
779
+ # Validate proposer is a validator
780
+ if proposer not in self._validators:
781
+ return False, f"Proposer {proposer} is not a validator", None
782
+
783
+ # Rate-limit
784
+ last = self._last_proposal_time.get(proposer, 0.0)
785
+ if time.time() - last < self._proposal_delay:
786
+ return False, "Proposal rate-limited", None
787
+
788
+ # Validate changes keys
789
+ for key in changes:
790
+ if key not in self.ALLOWED_PARAMETERS:
791
+ return False, f"Unknown parameter: {key}", None
792
+
793
+ # Validate activation height is in the future
794
+ current_height = self._chain.height if self._chain else 0
795
+ if activation_height <= current_height:
796
+ return False, (
797
+ f"Activation height {activation_height} must be > "
798
+ f"current height {current_height}"
799
+ ), None
800
+
801
+ # Generate proposal ID
802
+ pid = hashlib.sha256(
803
+ f"{proposer}-{time.time()}-{description}".encode()
804
+ ).hexdigest()[:16]
805
+
806
+ proposal = ChainUpgradeProposal(
807
+ proposal_id=pid,
808
+ proposal_type=proposal_type,
809
+ proposer=proposer,
810
+ description=description,
811
+ changes=changes,
812
+ activation_height=activation_height,
813
+ )
814
+
815
+ # Proposer auto-votes yes
816
+ proposal.votes_for.add(proposer)
817
+
818
+ self._proposals[pid] = proposal
819
+ self._last_proposal_time[proposer] = time.time()
820
+
821
+ self._emit(UpgradeEventType.CHAIN_PROPOSAL_CREATED, {
822
+ "proposal_id": pid,
823
+ "proposer": proposer,
824
+ "type": proposal_type.value,
825
+ "changes": changes,
826
+ "activation_height": activation_height,
827
+ })
828
+
829
+ # Check if the single vote already meets quorum (e.g. 1 validator)
830
+ self._check_quorum(pid)
831
+
832
+ logger.info("Chain upgrade proposal %s created by %s", pid, proposer)
833
+ return True, f"Proposal {pid} created", pid
834
+
835
+ def vote(
836
+ self,
837
+ proposal_id: str,
838
+ voter: str,
839
+ approve: bool,
840
+ ) -> Tuple[bool, str]:
841
+ """Vote on a pending proposal.
842
+
843
+ Returns ``(success, message)``.
844
+ """
845
+ if voter not in self._validators:
846
+ return False, f"Voter {voter} is not a validator"
847
+
848
+ proposal = self._proposals.get(proposal_id)
849
+ if proposal is None:
850
+ return False, f"Proposal {proposal_id} not found"
851
+ if proposal.status != ProposalStatus.PENDING:
852
+ return False, f"Proposal is {proposal.status.value}, not pending"
853
+
854
+ # Prevent double voting
855
+ if voter in proposal.votes_for or voter in proposal.votes_against:
856
+ return False, f"Voter {voter} already voted"
857
+
858
+ if approve:
859
+ proposal.votes_for.add(voter)
860
+ else:
861
+ proposal.votes_against.add(voter)
862
+
863
+ self._emit(UpgradeEventType.CHAIN_PROPOSAL_VOTED, {
864
+ "proposal_id": proposal_id,
865
+ "voter": voter,
866
+ "approve": approve,
867
+ "votes_for": len(proposal.votes_for),
868
+ "votes_against": len(proposal.votes_against),
869
+ })
870
+
871
+ # Check quorum
872
+ self._check_quorum(proposal_id)
873
+
874
+ return True, "Vote recorded"
875
+
876
+ def apply_pending(self, current_height: int) -> List[str]:
877
+ """Apply all approved proposals whose activation height has been reached.
878
+
879
+ Called by the node/consensus after adding each block.
880
+
881
+ Returns list of applied proposal IDs.
882
+ """
883
+ applied: List[str] = []
884
+
885
+ for pid, proposal in self._proposals.items():
886
+ if proposal.status != ProposalStatus.APPROVED:
887
+ continue
888
+ if current_height < proposal.activation_height:
889
+ continue
890
+
891
+ # Snapshot current values before applying
892
+ for key in proposal.changes:
893
+ if self._chain and hasattr(self._chain, key):
894
+ proposal.previous_values[key] = getattr(self._chain, key)
895
+
896
+ # Apply changes
897
+ success = self._apply_changes(proposal.changes)
898
+ if success:
899
+ proposal.status = ProposalStatus.APPLIED
900
+ proposal.applied_at = time.time()
901
+ self._applied_upgrades.append(pid)
902
+ applied.append(pid)
903
+
904
+ self._emit(UpgradeEventType.CHAIN_UPGRADE_APPLIED, {
905
+ "proposal_id": pid,
906
+ "changes": proposal.changes,
907
+ "height": current_height,
908
+ })
909
+ logger.info("Chain upgrade %s applied at height %d", pid, current_height)
910
+
911
+ return applied
912
+
913
+ def revert_upgrade(
914
+ self,
915
+ proposal_id: str,
916
+ caller: str,
917
+ ) -> Tuple[bool, str]:
918
+ """Revert a previously applied chain upgrade.
919
+
920
+ Returns ``(success, message)``.
921
+ """
922
+ if caller not in self._validators:
923
+ return False, "Only validators can revert upgrades"
924
+
925
+ proposal = self._proposals.get(proposal_id)
926
+ if proposal is None:
927
+ return False, f"Proposal {proposal_id} not found"
928
+ if proposal.status != ProposalStatus.APPLIED:
929
+ return False, f"Proposal status is {proposal.status.value}, not applied"
930
+ if not proposal.previous_values:
931
+ return False, "No previous values stored for revert"
932
+
933
+ # Restore previous values
934
+ success = self._apply_changes(proposal.previous_values)
935
+ if not success:
936
+ return False, "Failed to restore previous values"
937
+
938
+ proposal.status = ProposalStatus.REVERTED
939
+ if proposal_id in self._applied_upgrades:
940
+ self._applied_upgrades.remove(proposal_id)
941
+
942
+ self._emit(UpgradeEventType.CHAIN_UPGRADE_REVERTED, {
943
+ "proposal_id": proposal_id,
944
+ "restored_values": proposal.previous_values,
945
+ "caller": caller,
946
+ })
947
+
948
+ logger.info("Chain upgrade %s reverted by %s", proposal_id, caller)
949
+ return True, "Upgrade reverted"
950
+
951
+ # ── Queries ───────────────────────────────────────────────────
952
+
953
+ def get_proposal(self, proposal_id: str) -> Optional[ChainUpgradeProposal]:
954
+ return self._proposals.get(proposal_id)
955
+
956
+ def list_proposals(
957
+ self, status: Optional[ProposalStatus] = None
958
+ ) -> List[ChainUpgradeProposal]:
959
+ proposals = list(self._proposals.values())
960
+ if status is not None:
961
+ proposals = [p for p in proposals if p.status == status]
962
+ return proposals
963
+
964
+ def get_events(self) -> List[UpgradeEvent]:
965
+ return list(self._events)
966
+
967
+ def get_governance_info(self) -> Dict[str, Any]:
968
+ return {
969
+ "validators": list(self._validators),
970
+ "validator_count": len(self._validators),
971
+ "quorum_threshold": self.quorum_threshold,
972
+ "total_proposals": len(self._proposals),
973
+ "applied_upgrades": len(self._applied_upgrades),
974
+ "pending": len([
975
+ p for p in self._proposals.values()
976
+ if p.status == ProposalStatus.PENDING
977
+ ]),
978
+ }
979
+
980
+ # ── Internal ──────────────────────────────────────────────────
981
+
982
+ def _check_quorum(self, proposal_id: str) -> None:
983
+ """Check if a proposal has reached quorum and update status."""
984
+ proposal = self._proposals.get(proposal_id)
985
+ if proposal is None or proposal.status != ProposalStatus.PENDING:
986
+ return
987
+
988
+ threshold = self.quorum_threshold
989
+ if len(proposal.votes_for) >= threshold:
990
+ proposal.status = ProposalStatus.APPROVED
991
+ elif len(proposal.votes_against) >= threshold:
992
+ proposal.status = ProposalStatus.REJECTED
993
+
994
+ def _apply_changes(self, changes: Dict[str, Any]) -> bool:
995
+ """Apply parameter changes to the chain."""
996
+ if self._chain is None:
997
+ return False
998
+ for key, value in changes.items():
999
+ if hasattr(self._chain, key):
1000
+ setattr(self._chain, key, value)
1001
+ return True
1002
+
1003
+ def _emit(self, event_type: UpgradeEventType, data: Dict[str, Any]) -> None:
1004
+ self._events.append(UpgradeEvent(event_type=event_type, data=data))