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.
- package/README.md +12 -5
- 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/capability_system.py +184 -9
- package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
- package/src/zexus/cli/main.py +383 -34
- 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/bytecode.py +124 -7
- package/src/zexus/compiler/compat_runtime.py +6 -2
- package/src/zexus/compiler/lexer.py +16 -5
- package/src/zexus/compiler/parser.py +108 -7
- package/src/zexus/compiler/semantic.py +18 -19
- package/src/zexus/compiler/zexus_ast.py +26 -1
- 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 +112 -9
- 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 +457 -37
- package/src/zexus/evaluator/core.py +644 -50
- package/src/zexus/evaluator/expressions.py +358 -62
- package/src/zexus/evaluator/functions.py +458 -20
- package/src/zexus/evaluator/resource_limiter.py +4 -4
- package/src/zexus/evaluator/statements.py +774 -122
- package/src/zexus/evaluator/unified_execution.py +573 -72
- package/src/zexus/evaluator/utils.py +14 -2
- package/src/zexus/evaluator_original.py +1 -1
- package/src/zexus/event_loop.py +186 -0
- package/src/zexus/lexer.py +742 -458
- 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 +239 -9
- package/src/zexus/module_manager.py +129 -1
- package/src/zexus/object.py +76 -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 +1349 -408
- package/src/zexus/parser/strategy_context.py +755 -58
- package/src/zexus/parser/strategy_structural.py +121 -21
- package/src/zexus/persistence.py +15 -1
- package/src/zexus/renderer/__init__.py +61 -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/backend.py +261 -0
- package/src/zexus/renderer/canvas.py +78 -0
- package/src/zexus/renderer/color_system.py +201 -0
- package/src/zexus/renderer/graphics.py +31 -0
- package/src/zexus/renderer/layout.py +222 -0
- package/src/zexus/renderer/main_renderer.py +66 -0
- package/src/zexus/renderer/painter.py +30 -0
- package/src/zexus/renderer/tk_backend.py +208 -0
- package/src/zexus/renderer/web_backend.py +260 -0
- package/src/zexus/runtime/__init__.py +10 -2
- 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/runtime/load_manager.py +368 -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 +80 -6
- package/src/zexus/vm/binary_bytecode.py +659 -0
- package/src/zexus/vm/bytecode.py +59 -11
- 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 +561 -17
- package/src/zexus/vm/compiler.py +818 -51
- 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 +364 -20
- 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 +140 -45
- 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 +3581 -531
- package/src/zexus/vm/wasm_compiler.py +658 -0
- package/src/zexus/zexus_ast.py +137 -11
- package/src/zexus/zexus_token.py +16 -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 +16 -6
- package/src/zexus.egg-info/SOURCES.txt +129 -17
- package/src/zexus.egg-info/entry_points.txt +1 -0
- package/src/zexus.egg-info/requires.txt +4 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zexus Blockchain — Node
|
|
3
|
+
|
|
4
|
+
The Node is the top-level integration point that ties together:
|
|
5
|
+
- Chain (block storage + state)
|
|
6
|
+
- Mempool (pending transactions)
|
|
7
|
+
- P2P Network (peer connectivity)
|
|
8
|
+
- Consensus Engine (block production + validation)
|
|
9
|
+
|
|
10
|
+
It provides the public API used by the Zexus evaluator, CLI, and RPC.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import hashlib
|
|
16
|
+
import time
|
|
17
|
+
import threading
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
20
|
+
|
|
21
|
+
from .chain import Block, BlockHeader, Chain, Mempool, Transaction, TransactionReceipt
|
|
22
|
+
from .network import P2PNetwork, Message, MessageType, PeerConnection, PeerReputationManager
|
|
23
|
+
from .consensus import ConsensusEngine, ProofOfWork, create_consensus
|
|
24
|
+
|
|
25
|
+
# Multichain bridge (optional)
|
|
26
|
+
try:
|
|
27
|
+
from .multichain import ChainRouter, BridgeContract, CrossChainMessage
|
|
28
|
+
_MULTICHAIN_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
_MULTICHAIN_AVAILABLE = False
|
|
31
|
+
ChainRouter = None # type: ignore
|
|
32
|
+
BridgeContract = None # type: ignore
|
|
33
|
+
|
|
34
|
+
# RPC server (optional — requires aiohttp)
|
|
35
|
+
try:
|
|
36
|
+
from .rpc import RPCServer
|
|
37
|
+
_RPC_AVAILABLE = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
_RPC_AVAILABLE = False
|
|
40
|
+
RPCServer = None # type: ignore
|
|
41
|
+
|
|
42
|
+
# Contract VM bridge (optional — only if the VM module is present)
|
|
43
|
+
try:
|
|
44
|
+
from .contract_vm import ContractVM, ContractExecutionReceipt
|
|
45
|
+
_CONTRACT_VM_AVAILABLE = True
|
|
46
|
+
except ImportError:
|
|
47
|
+
_CONTRACT_VM_AVAILABLE = False
|
|
48
|
+
ContractVM = None # type: ignore
|
|
49
|
+
ContractExecutionReceipt = None # type: ignore
|
|
50
|
+
|
|
51
|
+
# Event indexing (optional)
|
|
52
|
+
try:
|
|
53
|
+
from .events import EventIndex, LogFilter, BloomFilter, EventLog
|
|
54
|
+
_EVENTS_AVAILABLE = True
|
|
55
|
+
except ImportError:
|
|
56
|
+
_EVENTS_AVAILABLE = False
|
|
57
|
+
EventIndex = None # type: ignore
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger("zexus.blockchain.node")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class NodeConfig:
|
|
63
|
+
"""Configuration for a blockchain node."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, **kwargs):
|
|
66
|
+
self.chain_id: str = kwargs.get("chain_id", "zexus-mainnet")
|
|
67
|
+
self.host: str = kwargs.get("host", "0.0.0.0")
|
|
68
|
+
self.port: int = kwargs.get("port", 30303)
|
|
69
|
+
self.data_dir: Optional[str] = kwargs.get("data_dir", None)
|
|
70
|
+
self.consensus: str = kwargs.get("consensus", "pow")
|
|
71
|
+
self.consensus_params: Dict[str, Any] = kwargs.get("consensus_params", {})
|
|
72
|
+
self.miner_address: str = kwargs.get("miner_address", "")
|
|
73
|
+
self.mining_enabled: bool = kwargs.get("mining_enabled", False)
|
|
74
|
+
self.mining_interval: float = kwargs.get("mining_interval", 5.0)
|
|
75
|
+
self.max_peers: int = kwargs.get("max_peers", 25)
|
|
76
|
+
self.bootstrap_nodes: List[Dict[str, Any]] = kwargs.get("bootstrap_nodes", [])
|
|
77
|
+
self.initial_balances: Dict[str, int] = kwargs.get("initial_balances", {})
|
|
78
|
+
self.rpc_enabled: bool = kwargs.get("rpc_enabled", False)
|
|
79
|
+
self.rpc_port: int = kwargs.get("rpc_port", 8545)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class BlockchainNode:
|
|
83
|
+
"""Full blockchain node.
|
|
84
|
+
|
|
85
|
+
Coordinates chain storage, transaction processing, P2P networking,
|
|
86
|
+
consensus, and mining into a single cohesive system.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, config: Optional[NodeConfig] = None):
|
|
90
|
+
self.config = config or NodeConfig()
|
|
91
|
+
|
|
92
|
+
# Core components
|
|
93
|
+
self.chain = Chain(
|
|
94
|
+
chain_id=self.config.chain_id,
|
|
95
|
+
data_dir=self.config.data_dir,
|
|
96
|
+
)
|
|
97
|
+
self.mempool = Mempool()
|
|
98
|
+
self.consensus_engine: ConsensusEngine = create_consensus(
|
|
99
|
+
self.config.consensus,
|
|
100
|
+
**self.config.consensus_params,
|
|
101
|
+
)
|
|
102
|
+
self.network = P2PNetwork(
|
|
103
|
+
host=self.config.host,
|
|
104
|
+
port=self.config.port,
|
|
105
|
+
chain_id=self.config.chain_id,
|
|
106
|
+
max_peers=self.config.max_peers,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Mining state
|
|
110
|
+
self._mining = False
|
|
111
|
+
self._mining_task: Optional[asyncio.Task] = None
|
|
112
|
+
|
|
113
|
+
# Contract VM bridge — enables smart-contract execution via
|
|
114
|
+
# the Zexus VM with real chain-backed state.
|
|
115
|
+
self.contract_vm: Optional["ContractVM"] = None
|
|
116
|
+
if _CONTRACT_VM_AVAILABLE:
|
|
117
|
+
self.contract_vm = ContractVM(
|
|
118
|
+
chain=self.chain,
|
|
119
|
+
gas_limit=self.config.consensus_params.get("gas_limit", 10_000_000),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Event listeners
|
|
123
|
+
self._event_handlers: Dict[str, List[Callable]] = {}
|
|
124
|
+
|
|
125
|
+
# RPC server
|
|
126
|
+
self.rpc_server: Optional["RPCServer"] = None
|
|
127
|
+
|
|
128
|
+
# Event indexer
|
|
129
|
+
self.event_index: Optional["EventIndex"] = None
|
|
130
|
+
if _EVENTS_AVAILABLE:
|
|
131
|
+
self.event_index = EventIndex(data_dir=self.config.data_dir)
|
|
132
|
+
|
|
133
|
+
# Node lifecycle
|
|
134
|
+
self._running = False
|
|
135
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
136
|
+
self._bg_thread: Optional[threading.Thread] = None
|
|
137
|
+
|
|
138
|
+
# ── Lifecycle ──────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
async def start(self):
|
|
141
|
+
"""Start the node: initialize chain, start networking, begin mining."""
|
|
142
|
+
if self._running:
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
self._loop = asyncio.get_event_loop()
|
|
146
|
+
self._running = True
|
|
147
|
+
|
|
148
|
+
# Initialize chain with genesis if empty
|
|
149
|
+
if not self.chain.blocks:
|
|
150
|
+
self.chain.create_genesis(
|
|
151
|
+
miner=self.config.miner_address or "0x" + "0" * 40,
|
|
152
|
+
initial_balances=self.config.initial_balances or None,
|
|
153
|
+
)
|
|
154
|
+
logger.info("Genesis block created for chain '%s'", self.config.chain_id)
|
|
155
|
+
|
|
156
|
+
# Set up network handlers
|
|
157
|
+
self._register_network_handlers()
|
|
158
|
+
|
|
159
|
+
# Start P2P network
|
|
160
|
+
for bn in self.config.bootstrap_nodes:
|
|
161
|
+
self.network.add_bootstrap_node(bn.get("host", ""), bn.get("port", 0))
|
|
162
|
+
await self.network.start()
|
|
163
|
+
|
|
164
|
+
# Start mining if enabled
|
|
165
|
+
if self.config.mining_enabled and self.config.miner_address:
|
|
166
|
+
self.start_mining()
|
|
167
|
+
|
|
168
|
+
# Start RPC server if enabled
|
|
169
|
+
if self.config.rpc_enabled and _RPC_AVAILABLE:
|
|
170
|
+
self.rpc_server = RPCServer(
|
|
171
|
+
node=self,
|
|
172
|
+
host=self.config.host,
|
|
173
|
+
port=self.config.rpc_port,
|
|
174
|
+
)
|
|
175
|
+
await self.rpc_server.start()
|
|
176
|
+
logger.info("RPC server started on port %d", self.config.rpc_port)
|
|
177
|
+
|
|
178
|
+
logger.info("Node started: chain=%s height=%d peers=%d",
|
|
179
|
+
self.config.chain_id, self.chain.height, self.network.peer_count)
|
|
180
|
+
|
|
181
|
+
async def stop(self):
|
|
182
|
+
"""Stop the node gracefully."""
|
|
183
|
+
self._running = False
|
|
184
|
+
self.stop_mining()
|
|
185
|
+
if self.rpc_server and self.rpc_server.is_running:
|
|
186
|
+
await self.rpc_server.stop()
|
|
187
|
+
await self.network.stop()
|
|
188
|
+
self.chain.close()
|
|
189
|
+
logger.info("Node stopped")
|
|
190
|
+
|
|
191
|
+
def start_sync(self):
|
|
192
|
+
"""Start the node in a background thread (for non-async callers)."""
|
|
193
|
+
if self._bg_thread and self._bg_thread.is_alive():
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
def _run():
|
|
197
|
+
loop = asyncio.new_event_loop()
|
|
198
|
+
asyncio.set_event_loop(loop)
|
|
199
|
+
self._loop = loop
|
|
200
|
+
loop.run_until_complete(self.start())
|
|
201
|
+
loop.run_forever()
|
|
202
|
+
|
|
203
|
+
self._bg_thread = threading.Thread(target=_run, daemon=True)
|
|
204
|
+
self._bg_thread.start()
|
|
205
|
+
|
|
206
|
+
def stop_sync(self):
|
|
207
|
+
"""Stop the node from the synchronous world."""
|
|
208
|
+
if self._loop and self._running:
|
|
209
|
+
asyncio.run_coroutine_threadsafe(self.stop(), self._loop)
|
|
210
|
+
|
|
211
|
+
# ── Events ─────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
def on(self, event: str, handler: Callable):
|
|
214
|
+
"""Register an event handler. Events: new_block, new_tx, mined, sync."""
|
|
215
|
+
self._event_handlers.setdefault(event, []).append(handler)
|
|
216
|
+
|
|
217
|
+
def _emit(self, event: str, data: Any = None):
|
|
218
|
+
for handler in self._event_handlers.get(event, []):
|
|
219
|
+
try:
|
|
220
|
+
handler(data)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error("Event handler error (%s): %s", event, e)
|
|
223
|
+
|
|
224
|
+
# ── Transaction API ────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
def submit_transaction(self, tx: Transaction) -> Dict[str, Any]:
|
|
227
|
+
"""Submit a transaction to the node.
|
|
228
|
+
|
|
229
|
+
Validates, adds to mempool, and broadcasts to peers.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
{"success": bool, "tx_hash": str, "error": str}
|
|
233
|
+
"""
|
|
234
|
+
# Ensure hash
|
|
235
|
+
if not tx.tx_hash:
|
|
236
|
+
tx.compute_hash()
|
|
237
|
+
|
|
238
|
+
# Basic validation
|
|
239
|
+
if not tx.sender:
|
|
240
|
+
return {"success": False, "tx_hash": "", "error": "missing sender"}
|
|
241
|
+
|
|
242
|
+
sender_acct = self.chain.get_account(tx.sender)
|
|
243
|
+
cost = tx.value + (tx.gas_limit * tx.gas_price)
|
|
244
|
+
if sender_acct["balance"] < cost:
|
|
245
|
+
return {"success": False, "tx_hash": tx.tx_hash,
|
|
246
|
+
"error": f"insufficient balance: need {cost}, have {sender_acct['balance']}"}
|
|
247
|
+
|
|
248
|
+
if tx.nonce < sender_acct["nonce"]:
|
|
249
|
+
return {"success": False, "tx_hash": tx.tx_hash,
|
|
250
|
+
"error": f"nonce too low: expected >= {sender_acct['nonce']}, got {tx.nonce}"}
|
|
251
|
+
|
|
252
|
+
# Add to mempool
|
|
253
|
+
if not self.mempool.add(tx):
|
|
254
|
+
return {"success": False, "tx_hash": tx.tx_hash, "error": "mempool rejected"}
|
|
255
|
+
|
|
256
|
+
# Broadcast to peers
|
|
257
|
+
if self._loop and self.network.is_running:
|
|
258
|
+
asyncio.run_coroutine_threadsafe(
|
|
259
|
+
self.network.gossip(Message(
|
|
260
|
+
type=MessageType.NEW_TX,
|
|
261
|
+
payload=tx.to_dict(),
|
|
262
|
+
)),
|
|
263
|
+
self._loop,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
self._emit("new_tx", tx)
|
|
267
|
+
return {"success": True, "tx_hash": tx.tx_hash, "error": ""}
|
|
268
|
+
|
|
269
|
+
def create_transaction(self, sender: str, recipient: str, value: int,
|
|
270
|
+
data: str = "", gas_limit: int = 21_000,
|
|
271
|
+
gas_price: int = 1) -> Transaction:
|
|
272
|
+
"""Convenience: create and submit a transaction."""
|
|
273
|
+
acct = self.chain.get_account(sender)
|
|
274
|
+
tx = Transaction(
|
|
275
|
+
sender=sender,
|
|
276
|
+
recipient=recipient,
|
|
277
|
+
value=value,
|
|
278
|
+
data=data,
|
|
279
|
+
nonce=acct["nonce"],
|
|
280
|
+
gas_limit=gas_limit,
|
|
281
|
+
gas_price=gas_price,
|
|
282
|
+
timestamp=time.time(),
|
|
283
|
+
)
|
|
284
|
+
tx.compute_hash()
|
|
285
|
+
return tx
|
|
286
|
+
|
|
287
|
+
# ── Block API ──────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
def get_block(self, hash_or_height) -> Optional[Dict[str, Any]]:
|
|
290
|
+
"""Get a block by hash or height."""
|
|
291
|
+
block = self.chain.get_block(hash_or_height)
|
|
292
|
+
return block.to_dict() if block else None
|
|
293
|
+
|
|
294
|
+
def get_latest_block(self) -> Optional[Dict[str, Any]]:
|
|
295
|
+
"""Get the latest block."""
|
|
296
|
+
tip = self.chain.tip
|
|
297
|
+
return tip.to_dict() if tip else None
|
|
298
|
+
|
|
299
|
+
def get_chain_info(self) -> Dict[str, Any]:
|
|
300
|
+
"""Get combined chain + network info."""
|
|
301
|
+
info = self.chain.get_chain_info()
|
|
302
|
+
info["network"] = self.network.get_network_info()
|
|
303
|
+
info["mempool_size"] = self.mempool.size
|
|
304
|
+
info["mining"] = self._mining
|
|
305
|
+
info["consensus"] = self.config.consensus
|
|
306
|
+
return info
|
|
307
|
+
|
|
308
|
+
# ── Account API ────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
def get_balance(self, address: str) -> int:
|
|
311
|
+
return self.chain.get_account(address)["balance"]
|
|
312
|
+
|
|
313
|
+
def get_nonce(self, address: str) -> int:
|
|
314
|
+
return self.chain.get_account(address)["nonce"]
|
|
315
|
+
|
|
316
|
+
def get_account(self, address: str) -> Dict[str, Any]:
|
|
317
|
+
return self.chain.get_account(address)
|
|
318
|
+
|
|
319
|
+
def fund_account(self, address: str, amount: int):
|
|
320
|
+
"""Fund an account (for testnets / development)."""
|
|
321
|
+
acct = self.chain.get_account(address)
|
|
322
|
+
acct["balance"] += amount
|
|
323
|
+
|
|
324
|
+
# ── Smart Contract API ─────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
def deploy_contract(self, contract, deployer: str,
|
|
327
|
+
gas_limit: int = 10_000_000,
|
|
328
|
+
initial_value: int = 0) -> Dict[str, Any]:
|
|
329
|
+
"""Deploy a SmartContract onto the chain via the ContractVM.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
{"success": bool, "address": str, "error": str}
|
|
333
|
+
"""
|
|
334
|
+
if self.contract_vm is None:
|
|
335
|
+
return {"success": False, "address": "", "error": "ContractVM not available"}
|
|
336
|
+
|
|
337
|
+
receipt = self.contract_vm.deploy_contract(
|
|
338
|
+
contract=contract,
|
|
339
|
+
deployer=deployer,
|
|
340
|
+
gas_limit=gas_limit,
|
|
341
|
+
initial_value=initial_value,
|
|
342
|
+
)
|
|
343
|
+
return {
|
|
344
|
+
"success": receipt.success,
|
|
345
|
+
"address": contract.address,
|
|
346
|
+
"error": receipt.error,
|
|
347
|
+
"gas_used": receipt.gas_used,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
def call_contract(self, contract_address: str, action: str,
|
|
351
|
+
args: Optional[Dict[str, Any]] = None,
|
|
352
|
+
caller: str = "",
|
|
353
|
+
gas_limit: int = 500_000,
|
|
354
|
+
value: int = 0) -> Dict[str, Any]:
|
|
355
|
+
"""Execute a contract action (state-mutating).
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
{"success": bool, "return_value": Any, "gas_used": int, ...}
|
|
359
|
+
"""
|
|
360
|
+
if self.contract_vm is None:
|
|
361
|
+
return {"success": False, "error": "ContractVM not available"}
|
|
362
|
+
|
|
363
|
+
receipt = self.contract_vm.execute_contract(
|
|
364
|
+
contract_address=contract_address,
|
|
365
|
+
action=action,
|
|
366
|
+
args=args,
|
|
367
|
+
caller=caller,
|
|
368
|
+
gas_limit=gas_limit,
|
|
369
|
+
value=value,
|
|
370
|
+
)
|
|
371
|
+
return receipt.to_dict()
|
|
372
|
+
|
|
373
|
+
def static_call_contract(self, contract_address: str, action: str,
|
|
374
|
+
args: Optional[Dict[str, Any]] = None,
|
|
375
|
+
caller: str = "") -> Dict[str, Any]:
|
|
376
|
+
"""Execute a read-only contract call (no state changes committed).
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
{"success": bool, "return_value": Any, "gas_used": int, ...}
|
|
380
|
+
"""
|
|
381
|
+
if self.contract_vm is None:
|
|
382
|
+
return {"success": False, "error": "ContractVM not available"}
|
|
383
|
+
|
|
384
|
+
receipt = self.contract_vm.static_call(
|
|
385
|
+
contract_address=contract_address,
|
|
386
|
+
action=action,
|
|
387
|
+
args=args,
|
|
388
|
+
caller=caller,
|
|
389
|
+
)
|
|
390
|
+
return receipt.to_dict()
|
|
391
|
+
|
|
392
|
+
# ── Mining ─────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
def start_mining(self):
|
|
395
|
+
"""Start the mining loop."""
|
|
396
|
+
if self._mining:
|
|
397
|
+
return
|
|
398
|
+
if not self.config.miner_address:
|
|
399
|
+
raise RuntimeError("Cannot mine without a miner_address")
|
|
400
|
+
self._mining = True
|
|
401
|
+
if self._loop:
|
|
402
|
+
self._mining_task = asyncio.run_coroutine_threadsafe(
|
|
403
|
+
self._mining_loop(), self._loop
|
|
404
|
+
)
|
|
405
|
+
logger.info("Mining started (miner=%s)", self.config.miner_address[:8])
|
|
406
|
+
|
|
407
|
+
def stop_mining(self):
|
|
408
|
+
"""Stop the mining loop."""
|
|
409
|
+
self._mining = False
|
|
410
|
+
if self._mining_task:
|
|
411
|
+
self._mining_task.cancel()
|
|
412
|
+
self._mining_task = None
|
|
413
|
+
logger.info("Mining stopped")
|
|
414
|
+
|
|
415
|
+
async def _mining_loop(self):
|
|
416
|
+
"""Continuous mining loop."""
|
|
417
|
+
while self._mining and self._running:
|
|
418
|
+
try:
|
|
419
|
+
block = await asyncio.get_event_loop().run_in_executor(
|
|
420
|
+
None, self._mine_one_block
|
|
421
|
+
)
|
|
422
|
+
if block:
|
|
423
|
+
success, err = self.chain.add_block(block)
|
|
424
|
+
if success:
|
|
425
|
+
logger.info("Mined block %d: %s", block.header.height, block.hash[:16])
|
|
426
|
+
self._emit("mined", block)
|
|
427
|
+
# Broadcast new block
|
|
428
|
+
await self.network.gossip(Message(
|
|
429
|
+
type=MessageType.NEW_BLOCK,
|
|
430
|
+
payload=block.to_dict(),
|
|
431
|
+
))
|
|
432
|
+
else:
|
|
433
|
+
logger.warning("Mined block rejected: %s", err)
|
|
434
|
+
else:
|
|
435
|
+
await asyncio.sleep(self.config.mining_interval)
|
|
436
|
+
except asyncio.CancelledError:
|
|
437
|
+
break
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.error("Mining error: %s", e)
|
|
440
|
+
await asyncio.sleep(1)
|
|
441
|
+
|
|
442
|
+
def _mine_one_block(self) -> Optional[Block]:
|
|
443
|
+
"""Attempt to mine a single block."""
|
|
444
|
+
if self.mempool.size == 0:
|
|
445
|
+
return None
|
|
446
|
+
return self.consensus_engine.seal(
|
|
447
|
+
self.chain, self.mempool, self.config.miner_address
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def mine_block_sync(self) -> Optional[Block]:
|
|
451
|
+
"""Mine a single block synchronously (for testing/scripting)."""
|
|
452
|
+
block = self.consensus_engine.seal(
|
|
453
|
+
self.chain, self.mempool, self.config.miner_address
|
|
454
|
+
)
|
|
455
|
+
if block:
|
|
456
|
+
success, err = self.chain.add_block(block)
|
|
457
|
+
if success:
|
|
458
|
+
self._emit("mined", block)
|
|
459
|
+
# Index events from the new block
|
|
460
|
+
if self.event_index:
|
|
461
|
+
self.event_index.index_block(block)
|
|
462
|
+
return block
|
|
463
|
+
else:
|
|
464
|
+
logger.warning("Block rejected: %s", err)
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
# ── Chain Sync (Network) ───────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
def _register_network_handlers(self):
|
|
470
|
+
"""Set up P2P message handlers for blockchain protocol."""
|
|
471
|
+
self.network.on(MessageType.NEW_BLOCK, self._on_new_block)
|
|
472
|
+
self.network.on(MessageType.NEW_TX, self._on_new_tx)
|
|
473
|
+
self.network.on(MessageType.GET_BLOCKS, self._on_get_blocks)
|
|
474
|
+
self.network.on(MessageType.BLOCKS, self._on_blocks)
|
|
475
|
+
self.network.on(MessageType.GET_HEADERS, self._on_get_headers)
|
|
476
|
+
self.network.on(MessageType.HEADERS, self._on_headers)
|
|
477
|
+
|
|
478
|
+
async def _on_new_block(self, msg: Message, conn: PeerConnection):
|
|
479
|
+
"""Handle a NEW_BLOCK message from a peer."""
|
|
480
|
+
try:
|
|
481
|
+
block = Block.from_dict(msg.payload)
|
|
482
|
+
except Exception as e:
|
|
483
|
+
logger.debug("Invalid block from %s: %s", msg.sender[:8], e)
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
# Skip if we already have this block
|
|
487
|
+
if block.hash in self.chain.block_index:
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
# Validate via consensus
|
|
491
|
+
if not self.consensus_engine.verify(block, self.chain):
|
|
492
|
+
logger.debug("Block %d from %s failed consensus", block.header.height, msg.sender[:8])
|
|
493
|
+
# Penalize peer for invalid block
|
|
494
|
+
if conn.peer_info.peer_id:
|
|
495
|
+
self.network.reputation.update(
|
|
496
|
+
conn.peer_info, PeerReputationManager.INVALID_BLOCK,
|
|
497
|
+
reason="failed consensus verification")
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
# Try to add to chain
|
|
501
|
+
success, err = self.chain.add_block(block)
|
|
502
|
+
if success:
|
|
503
|
+
logger.info("Accepted block %d from peer %s", block.header.height, msg.sender[:8])
|
|
504
|
+
self._emit("new_block", block)
|
|
505
|
+
# Reward peer for valid block
|
|
506
|
+
if conn.peer_info.peer_id:
|
|
507
|
+
self.network.reputation.update(
|
|
508
|
+
conn.peer_info, PeerReputationManager.VALID_BLOCK,
|
|
509
|
+
reason="valid block accepted")
|
|
510
|
+
# Remove block's txs from our mempool
|
|
511
|
+
for tx in block.transactions:
|
|
512
|
+
self.mempool.remove(tx.tx_hash)
|
|
513
|
+
# Relay to other peers
|
|
514
|
+
await self.network.gossip(msg, exclude={msg.sender})
|
|
515
|
+
else:
|
|
516
|
+
logger.debug("Block %d rejected: %s", block.header.height, err)
|
|
517
|
+
# If block is ahead of us, we might need to sync
|
|
518
|
+
if block.header.height > self.chain.height + 1:
|
|
519
|
+
await self._request_sync(conn)
|
|
520
|
+
|
|
521
|
+
async def _on_new_tx(self, msg: Message, conn: PeerConnection):
|
|
522
|
+
"""Handle a NEW_TX message from a peer."""
|
|
523
|
+
try:
|
|
524
|
+
tx = Transaction(**msg.payload)
|
|
525
|
+
except Exception as e:
|
|
526
|
+
logger.debug("Invalid TX from %s: %s", msg.sender[:8], e)
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
if self.mempool.add(tx):
|
|
530
|
+
self._emit("new_tx", tx)
|
|
531
|
+
# Reward peer for valid transaction
|
|
532
|
+
if conn.peer_info.peer_id:
|
|
533
|
+
self.network.reputation.update(
|
|
534
|
+
conn.peer_info, PeerReputationManager.VALID_TX,
|
|
535
|
+
reason="valid transaction")
|
|
536
|
+
# Relay
|
|
537
|
+
await self.network.gossip(msg, exclude={msg.sender})
|
|
538
|
+
|
|
539
|
+
async def _on_get_blocks(self, msg: Message, conn: PeerConnection):
|
|
540
|
+
"""Respond to a GET_BLOCKS request."""
|
|
541
|
+
start_height = msg.payload.get("start", 0)
|
|
542
|
+
count = min(msg.payload.get("count", 50), 100) # Max 100 blocks per request
|
|
543
|
+
|
|
544
|
+
blocks_data = []
|
|
545
|
+
for h in range(start_height, min(start_height + count, self.chain.height + 1)):
|
|
546
|
+
block = self.chain.get_block(h)
|
|
547
|
+
if block:
|
|
548
|
+
blocks_data.append(block.to_dict())
|
|
549
|
+
|
|
550
|
+
await conn.send(Message(
|
|
551
|
+
type=MessageType.BLOCKS,
|
|
552
|
+
payload={"blocks": blocks_data},
|
|
553
|
+
))
|
|
554
|
+
|
|
555
|
+
async def _on_blocks(self, msg: Message, conn: PeerConnection):
|
|
556
|
+
"""Handle received blocks (sync response)."""
|
|
557
|
+
blocks_data = msg.payload.get("blocks", [])
|
|
558
|
+
added = 0
|
|
559
|
+
for bd in blocks_data:
|
|
560
|
+
try:
|
|
561
|
+
block = Block.from_dict(bd)
|
|
562
|
+
if block.hash not in self.chain.block_index:
|
|
563
|
+
if self.consensus_engine.verify(block, self.chain):
|
|
564
|
+
success, _ = self.chain.add_block(block)
|
|
565
|
+
if success:
|
|
566
|
+
added += 1
|
|
567
|
+
except Exception as e:
|
|
568
|
+
logger.debug("Error processing sync block: %s", e)
|
|
569
|
+
continue
|
|
570
|
+
|
|
571
|
+
if added > 0:
|
|
572
|
+
logger.info("Synced %d blocks from peer %s (height now %d)",
|
|
573
|
+
added, msg.sender[:8], self.chain.height)
|
|
574
|
+
|
|
575
|
+
async def _on_get_headers(self, msg: Message, conn: PeerConnection):
|
|
576
|
+
"""Respond to header requests."""
|
|
577
|
+
start = msg.payload.get("start", 0)
|
|
578
|
+
count = min(msg.payload.get("count", 100), 500)
|
|
579
|
+
|
|
580
|
+
headers = []
|
|
581
|
+
for h in range(start, min(start + count, self.chain.height + 1)):
|
|
582
|
+
block = self.chain.get_block(h)
|
|
583
|
+
if block:
|
|
584
|
+
from dataclasses import asdict
|
|
585
|
+
headers.append(asdict(block.header))
|
|
586
|
+
|
|
587
|
+
await conn.send(Message(
|
|
588
|
+
type=MessageType.HEADERS,
|
|
589
|
+
payload={"headers": headers},
|
|
590
|
+
))
|
|
591
|
+
|
|
592
|
+
async def _on_headers(self, msg: Message, conn: PeerConnection):
|
|
593
|
+
"""Handle received headers — decide if we need full blocks."""
|
|
594
|
+
headers = msg.payload.get("headers", [])
|
|
595
|
+
if headers:
|
|
596
|
+
last = headers[-1]
|
|
597
|
+
remote_height = last.get("height", 0)
|
|
598
|
+
if remote_height > self.chain.height:
|
|
599
|
+
# Request missing blocks
|
|
600
|
+
await conn.send(Message(
|
|
601
|
+
type=MessageType.GET_BLOCKS,
|
|
602
|
+
payload={"start": self.chain.height + 1, "count": 50},
|
|
603
|
+
))
|
|
604
|
+
|
|
605
|
+
async def _request_sync(self, conn: PeerConnection):
|
|
606
|
+
"""Request blocks from a peer to catch up."""
|
|
607
|
+
await conn.send(Message(
|
|
608
|
+
type=MessageType.GET_BLOCKS,
|
|
609
|
+
payload={"start": self.chain.height + 1, "count": 50},
|
|
610
|
+
))
|
|
611
|
+
|
|
612
|
+
# ── Utility ────────────────────────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
def validate_chain(self) -> Dict[str, Any]:
|
|
615
|
+
"""Full chain integrity check."""
|
|
616
|
+
valid, err = self.chain.validate_chain()
|
|
617
|
+
return {"valid": valid, "error": err, "height": self.chain.height}
|
|
618
|
+
|
|
619
|
+
def export_chain(self) -> List[Dict[str, Any]]:
|
|
620
|
+
"""Export the full chain as a list of block dicts."""
|
|
621
|
+
return [b.to_dict() for b in self.chain.blocks]
|
|
622
|
+
|
|
623
|
+
def __repr__(self):
|
|
624
|
+
return (f"BlockchainNode(chain_id={self.config.chain_id!r}, "
|
|
625
|
+
f"height={self.chain.height}, peers={self.network.peer_count})")
|
|
626
|
+
|
|
627
|
+
# ── Multichain / Cross-chain Bridge ────────────────────────────────
|
|
628
|
+
|
|
629
|
+
def join_router(self, router: "ChainRouter") -> None:
|
|
630
|
+
"""Register this node's chain with an external ChainRouter."""
|
|
631
|
+
if not _MULTICHAIN_AVAILABLE:
|
|
632
|
+
raise RuntimeError("Multichain module not available")
|
|
633
|
+
router.register_chain(self.config.chain_id, self.chain)
|
|
634
|
+
self._router = router
|
|
635
|
+
logger.info("Node %s joined ChainRouter", self.config.chain_id)
|
|
636
|
+
|
|
637
|
+
def bridge_to(
|
|
638
|
+
self,
|
|
639
|
+
other_node: "BlockchainNode",
|
|
640
|
+
router: Optional["ChainRouter"] = None,
|
|
641
|
+
) -> "BridgeContract":
|
|
642
|
+
"""Create a bridge contract between this node and *other_node*.
|
|
643
|
+
|
|
644
|
+
If no *router* is provided, a new one is created.
|
|
645
|
+
|
|
646
|
+
Returns a ``BridgeContract`` for lock-and-mint / burn-and-release.
|
|
647
|
+
"""
|
|
648
|
+
if not _MULTICHAIN_AVAILABLE:
|
|
649
|
+
raise RuntimeError("Multichain module not available")
|
|
650
|
+
if router is None:
|
|
651
|
+
router = ChainRouter()
|
|
652
|
+
if self.config.chain_id not in router.chain_ids:
|
|
653
|
+
self.join_router(router)
|
|
654
|
+
if other_node.config.chain_id not in router.chain_ids:
|
|
655
|
+
other_node.join_router(router)
|
|
656
|
+
router.connect(self.config.chain_id, other_node.config.chain_id)
|
|
657
|
+
bridge = BridgeContract(
|
|
658
|
+
router=router,
|
|
659
|
+
source_chain=self.config.chain_id,
|
|
660
|
+
dest_chain=other_node.config.chain_id,
|
|
661
|
+
)
|
|
662
|
+
logger.info(
|
|
663
|
+
"Bridge created: %s <-> %s",
|
|
664
|
+
self.config.chain_id, other_node.config.chain_id,
|
|
665
|
+
)
|
|
666
|
+
return bridge
|