zexus 1.7.1 → 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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/__init__.py +7 -0
- package/src/zexus/__init__.py +1 -1
- package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
- package/src/zexus/advanced_types.py +17 -2
- package/src/zexus/blockchain/__init__.py +411 -0
- package/src/zexus/blockchain/accelerator.py +1160 -0
- package/src/zexus/blockchain/chain.py +660 -0
- package/src/zexus/blockchain/consensus.py +821 -0
- package/src/zexus/blockchain/contract_vm.py +1019 -0
- package/src/zexus/blockchain/crypto.py +79 -14
- package/src/zexus/blockchain/events.py +526 -0
- package/src/zexus/blockchain/loadtest.py +721 -0
- package/src/zexus/blockchain/monitoring.py +350 -0
- package/src/zexus/blockchain/mpt.py +716 -0
- package/src/zexus/blockchain/multichain.py +951 -0
- package/src/zexus/blockchain/multiprocess_executor.py +338 -0
- package/src/zexus/blockchain/network.py +886 -0
- package/src/zexus/blockchain/node.py +666 -0
- package/src/zexus/blockchain/rpc.py +1203 -0
- package/src/zexus/blockchain/rust_bridge.py +421 -0
- package/src/zexus/blockchain/storage.py +423 -0
- package/src/zexus/blockchain/tokens.py +750 -0
- package/src/zexus/blockchain/upgradeable.py +1004 -0
- package/src/zexus/blockchain/verification.py +1602 -0
- package/src/zexus/blockchain/wallet.py +621 -0
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +300 -20
- package/src/zexus/cli/zpm.py +1 -1
- package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
- package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
- package/src/zexus/compiler/lexer.py +10 -5
- package/src/zexus/concurrency_system.py +79 -0
- package/src/zexus/config.py +54 -0
- package/src/zexus/crypto_bridge.py +244 -8
- package/src/zexus/dap/__init__.py +10 -0
- package/src/zexus/dap/__main__.py +4 -0
- package/src/zexus/dap/dap_server.py +391 -0
- package/src/zexus/dap/debug_engine.py +298 -0
- package/src/zexus/environment.py +10 -1
- package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
- package/src/zexus/evaluator/bytecode_compiler.py +441 -37
- package/src/zexus/evaluator/core.py +560 -49
- package/src/zexus/evaluator/expressions.py +122 -49
- package/src/zexus/evaluator/functions.py +417 -16
- package/src/zexus/evaluator/statements.py +521 -118
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -486
- package/src/zexus/lsp/__init__.py +1 -1
- package/src/zexus/lsp/definition_provider.py +163 -9
- package/src/zexus/lsp/server.py +22 -8
- package/src/zexus/lsp/symbol_provider.py +182 -9
- package/src/zexus/module_cache.py +237 -9
- package/src/zexus/object.py +64 -6
- package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
- package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
- package/src/zexus/parser/parser.py +786 -285
- package/src/zexus/parser/strategy_context.py +407 -66
- package/src/zexus/parser/strategy_structural.py +117 -19
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +15 -0
- package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
- package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
- package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
- package/src/zexus/runtime/file_flags.py +137 -0
- package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
- package/src/zexus/security.py +424 -34
- package/src/zexus/stdlib/fs.py +23 -18
- package/src/zexus/stdlib/http.py +289 -186
- package/src/zexus/stdlib/sockets.py +207 -163
- package/src/zexus/stdlib/websockets.py +282 -0
- package/src/zexus/stdlib_integration.py +369 -2
- package/src/zexus/strategy_recovery.py +6 -3
- package/src/zexus/type_checker.py +423 -0
- package/src/zexus/virtual_filesystem.py +189 -2
- package/src/zexus/vm/__init__.py +113 -3
- package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
- package/src/zexus/vm/async_optimizer.py +14 -1
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +28 -1
- package/src/zexus/vm/bytecode_converter.py +26 -12
- package/src/zexus/vm/cabi.c +1985 -0
- package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/cabi.h +127 -0
- package/src/zexus/vm/cache.py +557 -17
- package/src/zexus/vm/compiler.py +703 -5
- package/src/zexus/vm/fastops.c +15743 -0
- package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/fastops.pyx +288 -0
- package/src/zexus/vm/gas_metering.py +50 -9
- package/src/zexus/vm/jit.py +83 -2
- package/src/zexus/vm/native_jit_backend.py +1816 -0
- package/src/zexus/vm/native_runtime.cpp +1388 -0
- package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
- package/src/zexus/vm/optimizer.py +161 -11
- package/src/zexus/vm/parallel_vm.py +118 -42
- package/src/zexus/vm/peephole_optimizer.py +82 -4
- package/src/zexus/vm/profiler.py +38 -18
- package/src/zexus/vm/register_allocator.py +16 -5
- package/src/zexus/vm/register_vm.py +8 -5
- package/src/zexus/vm/vm.py +3411 -573
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +63 -11
- package/src/zexus/zexus_token.py +13 -5
- package/src/zexus/zpm/installer.py +55 -15
- package/src/zexus/zpm/package_manager.py +1 -1
- package/src/zexus/zpm/registry.py +257 -28
- package/src/zexus.egg-info/PKG-INFO +7 -4
- package/src/zexus.egg-info/SOURCES.txt +116 -9
- package/src/zexus.egg-info/entry_points.txt +1 -0
- 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))
|