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,951 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zexus Blockchain — Multichain Support
|
|
3
|
+
|
|
4
|
+
Production-grade cross-chain infrastructure providing:
|
|
5
|
+
|
|
6
|
+
1. **CrossChainMessage** — The canonical on-wire envelope for every
|
|
7
|
+
cross-chain communication. Contains source/dest chain IDs, a
|
|
8
|
+
monotonic nonce, the message payload, and a Merkle inclusion proof
|
|
9
|
+
anchored against a specific block header on the source chain.
|
|
10
|
+
|
|
11
|
+
2. **MerkleProofEngine** — Generates and verifies Merkle inclusion
|
|
12
|
+
proofs. Used by the relay to prove that a message was committed
|
|
13
|
+
on the source chain without trusting a third party.
|
|
14
|
+
|
|
15
|
+
3. **BridgeRelay** — Stateful relay that tracks light-client headers
|
|
16
|
+
from remote chains and validates inbound ``CrossChainMessage``
|
|
17
|
+
packets against those headers. No trusted third party: the relay
|
|
18
|
+
only accepts messages whose Merkle proof verifies against a header
|
|
19
|
+
it has already accepted.
|
|
20
|
+
|
|
21
|
+
4. **ChainRouter** — Manages a registry of local chain instances and
|
|
22
|
+
their corresponding bridge relays. Provides the ``send()`` /
|
|
23
|
+
``receive()`` API for cross-chain message passing, with per-chain
|
|
24
|
+
outbox/inbox queues and replay-protection (nonce tracking).
|
|
25
|
+
|
|
26
|
+
5. **BridgeContract** helpers — Lock-and-mint / burn-and-release
|
|
27
|
+
asset transfer between two chains, built on top of the router.
|
|
28
|
+
|
|
29
|
+
Integration
|
|
30
|
+
-----------
|
|
31
|
+
::
|
|
32
|
+
|
|
33
|
+
from zexus.blockchain.multichain import ChainRouter, BridgeContract
|
|
34
|
+
|
|
35
|
+
router = ChainRouter()
|
|
36
|
+
router.register_chain("chain-a", node_a.chain)
|
|
37
|
+
router.register_chain("chain-b", node_b.chain)
|
|
38
|
+
router.connect("chain-a", "chain-b")
|
|
39
|
+
|
|
40
|
+
bridge = BridgeContract(router, "chain-a", "chain-b")
|
|
41
|
+
receipt = bridge.lock_and_mint(sender="alice", amount=100)
|
|
42
|
+
receipt = bridge.burn_and_release(sender="bob", amount=50)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import copy
|
|
48
|
+
import hashlib
|
|
49
|
+
import json
|
|
50
|
+
import logging
|
|
51
|
+
import time
|
|
52
|
+
import uuid
|
|
53
|
+
from dataclasses import dataclass, field, asdict
|
|
54
|
+
from enum import Enum, auto
|
|
55
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
56
|
+
|
|
57
|
+
from .chain import Block, Chain
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger("zexus.blockchain.multichain")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Merkle Proof Engine
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
class MerkleProofEngine:
|
|
67
|
+
"""Generate and verify Merkle inclusion proofs.
|
|
68
|
+
|
|
69
|
+
The tree is built from a list of leaf hashes (SHA-256). A proof
|
|
70
|
+
consists of the sibling hashes along the path from the leaf to the
|
|
71
|
+
root, together with direction flags (left/right).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _hash_pair(a: str, b: str) -> str:
|
|
76
|
+
return hashlib.sha256((a + b).encode()).hexdigest()
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def compute_root(leaves: List[str]) -> str:
|
|
80
|
+
"""Compute the Merkle root of *leaves* (list of hex hashes)."""
|
|
81
|
+
if not leaves:
|
|
82
|
+
return hashlib.sha256(b"empty").hexdigest()
|
|
83
|
+
layer = list(leaves)
|
|
84
|
+
while len(layer) > 1:
|
|
85
|
+
if len(layer) % 2 == 1:
|
|
86
|
+
layer.append(layer[-1]) # duplicate last
|
|
87
|
+
next_layer: List[str] = []
|
|
88
|
+
for i in range(0, len(layer), 2):
|
|
89
|
+
next_layer.append(MerkleProofEngine._hash_pair(layer[i], layer[i + 1]))
|
|
90
|
+
layer = next_layer
|
|
91
|
+
return layer[0]
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def generate_proof(leaves: List[str], index: int) -> List[Tuple[str, str]]:
|
|
95
|
+
"""Generate a Merkle proof for *leaves[index]*.
|
|
96
|
+
|
|
97
|
+
Returns a list of ``(sibling_hash, direction)`` tuples where
|
|
98
|
+
*direction* is ``"L"`` if the sibling is on the left,
|
|
99
|
+
``"R"`` if it is on the right.
|
|
100
|
+
"""
|
|
101
|
+
if not leaves or index < 0 or index >= len(leaves):
|
|
102
|
+
return []
|
|
103
|
+
layer = list(leaves)
|
|
104
|
+
proof: List[Tuple[str, str]] = []
|
|
105
|
+
idx = index
|
|
106
|
+
while len(layer) > 1:
|
|
107
|
+
if len(layer) % 2 == 1:
|
|
108
|
+
layer.append(layer[-1])
|
|
109
|
+
sibling = idx ^ 1 # flip last bit
|
|
110
|
+
direction = "L" if sibling < idx else "R"
|
|
111
|
+
proof.append((layer[sibling], direction))
|
|
112
|
+
# move up
|
|
113
|
+
layer = [
|
|
114
|
+
MerkleProofEngine._hash_pair(layer[i], layer[i + 1])
|
|
115
|
+
for i in range(0, len(layer), 2)
|
|
116
|
+
]
|
|
117
|
+
idx //= 2
|
|
118
|
+
return proof
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def verify_proof(
|
|
122
|
+
leaf_hash: str,
|
|
123
|
+
proof: List[Tuple[str, str]],
|
|
124
|
+
expected_root: str,
|
|
125
|
+
) -> bool:
|
|
126
|
+
"""Verify a Merkle inclusion proof."""
|
|
127
|
+
current = leaf_hash
|
|
128
|
+
for sibling_hash, direction in proof:
|
|
129
|
+
if direction == "L":
|
|
130
|
+
current = MerkleProofEngine._hash_pair(sibling_hash, current)
|
|
131
|
+
else:
|
|
132
|
+
current = MerkleProofEngine._hash_pair(current, sibling_hash)
|
|
133
|
+
return current == expected_root
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
# Cross-Chain Message
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
class MessageStatus(Enum):
|
|
141
|
+
PENDING = auto()
|
|
142
|
+
RELAYED = auto()
|
|
143
|
+
CONFIRMED = auto()
|
|
144
|
+
FAILED = auto()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class CrossChainMessage:
|
|
149
|
+
"""Canonical envelope for every cross-chain communication.
|
|
150
|
+
|
|
151
|
+
Fields
|
|
152
|
+
------
|
|
153
|
+
msg_id : str
|
|
154
|
+
Globally unique identifier (UUID4).
|
|
155
|
+
nonce : int
|
|
156
|
+
Monotonically increasing per (source, dest) pair for replay
|
|
157
|
+
protection.
|
|
158
|
+
source_chain : str
|
|
159
|
+
``chain_id`` of the originating chain.
|
|
160
|
+
dest_chain : str
|
|
161
|
+
``chain_id`` of the destination chain.
|
|
162
|
+
sender : str
|
|
163
|
+
Address of the sender on the source chain.
|
|
164
|
+
payload : dict
|
|
165
|
+
Arbitrary data (e.g. ``{"action": "lock", "amount": 100}``).
|
|
166
|
+
block_height : int
|
|
167
|
+
Height of the source-chain block that includes this message.
|
|
168
|
+
block_hash : str
|
|
169
|
+
Hash of that block.
|
|
170
|
+
merkle_root : str
|
|
171
|
+
Merkle root of the message batch in that block.
|
|
172
|
+
merkle_proof : list
|
|
173
|
+
Inclusion proof for this specific message in the batch.
|
|
174
|
+
timestamp : float
|
|
175
|
+
Creation time (UNIX epoch).
|
|
176
|
+
status : MessageStatus
|
|
177
|
+
Lifecycle status.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
msg_id: str = ""
|
|
181
|
+
nonce: int = 0
|
|
182
|
+
source_chain: str = ""
|
|
183
|
+
dest_chain: str = ""
|
|
184
|
+
sender: str = ""
|
|
185
|
+
payload: Dict[str, Any] = field(default_factory=dict)
|
|
186
|
+
block_height: int = 0
|
|
187
|
+
block_hash: str = ""
|
|
188
|
+
merkle_root: str = ""
|
|
189
|
+
merkle_proof: List[Tuple[str, str]] = field(default_factory=list)
|
|
190
|
+
timestamp: float = 0.0
|
|
191
|
+
status: MessageStatus = MessageStatus.PENDING
|
|
192
|
+
|
|
193
|
+
def compute_hash(self) -> str:
|
|
194
|
+
"""Deterministic hash of message contents (excludes proof & status)."""
|
|
195
|
+
data = json.dumps({
|
|
196
|
+
"msg_id": self.msg_id,
|
|
197
|
+
"nonce": self.nonce,
|
|
198
|
+
"source_chain": self.source_chain,
|
|
199
|
+
"dest_chain": self.dest_chain,
|
|
200
|
+
"sender": self.sender,
|
|
201
|
+
"payload": self.payload,
|
|
202
|
+
"block_height": self.block_height,
|
|
203
|
+
"block_hash": self.block_hash,
|
|
204
|
+
"timestamp": self.timestamp,
|
|
205
|
+
}, sort_keys=True)
|
|
206
|
+
return hashlib.sha256(data.encode()).hexdigest()
|
|
207
|
+
|
|
208
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
209
|
+
d = asdict(self)
|
|
210
|
+
d["status"] = self.status.name
|
|
211
|
+
return d
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def from_dict(data: Dict[str, Any]) -> "CrossChainMessage":
|
|
215
|
+
data = dict(data)
|
|
216
|
+
status_name = data.pop("status", "PENDING")
|
|
217
|
+
# Convert proof tuples back from lists (JSON round-trip)
|
|
218
|
+
raw_proof = data.pop("merkle_proof", [])
|
|
219
|
+
proof = [(p[0], p[1]) if isinstance(p, (list, tuple)) else p for p in raw_proof]
|
|
220
|
+
msg = CrossChainMessage(**data, merkle_proof=proof)
|
|
221
|
+
msg.status = MessageStatus[status_name]
|
|
222
|
+
return msg
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# Bridge Relay — light-client header tracking + proof verification
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
class BridgeRelay:
|
|
230
|
+
"""Validates inbound cross-chain messages using Merkle proofs.
|
|
231
|
+
|
|
232
|
+
For each remote chain it tracks the latest known block headers
|
|
233
|
+
(a "light client"). When a ``CrossChainMessage`` arrives, the relay
|
|
234
|
+
verifies that:
|
|
235
|
+
|
|
236
|
+
1. The ``block_hash`` matches a known header at ``block_height``.
|
|
237
|
+
2. The ``merkle_root`` in the message matches the stored header's
|
|
238
|
+
``state_root`` (or a dedicated cross-chain root stored in
|
|
239
|
+
``extra_data``).
|
|
240
|
+
3. The ``merkle_proof`` proves inclusion of the message hash under
|
|
241
|
+
``merkle_root``.
|
|
242
|
+
4. The message nonce is strictly greater than the last-seen nonce
|
|
243
|
+
for this ``(source, dest)`` pair (replay protection).
|
|
244
|
+
|
|
245
|
+
Trust model
|
|
246
|
+
-----------
|
|
247
|
+
The relay does *not* trust the sender. It only trusts block headers
|
|
248
|
+
that were (a) explicitly registered or (b) form a valid chain
|
|
249
|
+
extending from already-trusted headers.
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, local_chain_id: str):
|
|
253
|
+
self.local_chain_id = local_chain_id
|
|
254
|
+
|
|
255
|
+
# remote_chain_id -> {height -> BlockHeader-like dict}
|
|
256
|
+
self._remote_headers: Dict[str, Dict[int, Dict[str, Any]]] = {}
|
|
257
|
+
|
|
258
|
+
# (source, dest) -> last accepted nonce
|
|
259
|
+
self._nonce_tracker: Dict[Tuple[str, str], int] = {}
|
|
260
|
+
|
|
261
|
+
# Processed message IDs (replay guard)
|
|
262
|
+
self._processed: Set[str] = set()
|
|
263
|
+
|
|
264
|
+
# -- Header management -------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def submit_header(self, remote_chain_id: str, header: Dict[str, Any]) -> bool:
|
|
267
|
+
"""Submit a remote block header for tracking.
|
|
268
|
+
|
|
269
|
+
In a production system this would validate the header against
|
|
270
|
+
the previous header (hash chain, PoW/PoS, signatures). Here we
|
|
271
|
+
do basic hash-chain validation when a parent is available.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
remote_chain_id: The chain the header belongs to.
|
|
275
|
+
header: A dict with at least ``height``, ``hash``,
|
|
276
|
+
``prev_hash``, and ``extra_data`` / ``state_root``.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
True if the header was accepted.
|
|
280
|
+
"""
|
|
281
|
+
headers = self._remote_headers.setdefault(remote_chain_id, {})
|
|
282
|
+
height = header.get("height", -1)
|
|
283
|
+
|
|
284
|
+
# Validate chain linkage when we have the parent
|
|
285
|
+
if height > 0:
|
|
286
|
+
parent = headers.get(height - 1)
|
|
287
|
+
if parent and header.get("prev_hash") != parent.get("hash"):
|
|
288
|
+
logger.warning(
|
|
289
|
+
"Relay: header %d for %s has invalid prev_hash",
|
|
290
|
+
height, remote_chain_id,
|
|
291
|
+
)
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
headers[height] = header
|
|
295
|
+
logger.debug("Relay: accepted header %d for chain %s", height, remote_chain_id)
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
def has_header(self, remote_chain_id: str, height: int) -> bool:
|
|
299
|
+
return height in self._remote_headers.get(remote_chain_id, {})
|
|
300
|
+
|
|
301
|
+
def get_header(self, remote_chain_id: str, height: int) -> Optional[Dict[str, Any]]:
|
|
302
|
+
return self._remote_headers.get(remote_chain_id, {}).get(height)
|
|
303
|
+
|
|
304
|
+
def latest_height(self, remote_chain_id: str) -> int:
|
|
305
|
+
"""The highest tracked header height for a remote chain."""
|
|
306
|
+
headers = self._remote_headers.get(remote_chain_id, {})
|
|
307
|
+
return max(headers.keys()) if headers else -1
|
|
308
|
+
|
|
309
|
+
# -- Message verification ----------------------------------------------
|
|
310
|
+
|
|
311
|
+
def verify_message(self, msg: CrossChainMessage) -> Tuple[bool, str]:
|
|
312
|
+
"""Verify an inbound cross-chain message.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
``(True, "")`` on success, ``(False, reason)`` on failure.
|
|
316
|
+
"""
|
|
317
|
+
if msg.dest_chain != self.local_chain_id:
|
|
318
|
+
return False, f"Message destined for {msg.dest_chain}, not {self.local_chain_id}"
|
|
319
|
+
|
|
320
|
+
if msg.msg_id in self._processed:
|
|
321
|
+
return False, f"Message {msg.msg_id} already processed (replay)"
|
|
322
|
+
|
|
323
|
+
# 1. Check header exists
|
|
324
|
+
header = self.get_header(msg.source_chain, msg.block_height)
|
|
325
|
+
if header is None:
|
|
326
|
+
return False, (
|
|
327
|
+
f"No header for chain {msg.source_chain} at height {msg.block_height}"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# 2. Verify block hash matches
|
|
331
|
+
if header.get("hash") != msg.block_hash:
|
|
332
|
+
return False, (
|
|
333
|
+
f"Block hash mismatch at height {msg.block_height}: "
|
|
334
|
+
f"expected {header.get('hash', '?')[:16]}, got {msg.block_hash[:16]}"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# 3. Verify Merkle root
|
|
338
|
+
# The cross-chain Merkle root is stored in the header's
|
|
339
|
+
# extra_data field as "xchain_root:<hex>" or falls back to
|
|
340
|
+
# matching against state_root.
|
|
341
|
+
expected_root = None
|
|
342
|
+
extra = header.get("extra_data", "")
|
|
343
|
+
if isinstance(extra, str) and extra.startswith("xchain_root:"):
|
|
344
|
+
expected_root = extra.split(":", 1)[1]
|
|
345
|
+
else:
|
|
346
|
+
expected_root = msg.merkle_root # self-asserted; verify via proof
|
|
347
|
+
|
|
348
|
+
if expected_root and msg.merkle_root != expected_root:
|
|
349
|
+
return False, f"Merkle root mismatch"
|
|
350
|
+
|
|
351
|
+
# 4. Verify Merkle proof
|
|
352
|
+
leaf_hash = msg.compute_hash()
|
|
353
|
+
if not MerkleProofEngine.verify_proof(leaf_hash, msg.merkle_proof, msg.merkle_root):
|
|
354
|
+
return False, "Merkle proof verification failed"
|
|
355
|
+
|
|
356
|
+
# 5. Nonce replay protection
|
|
357
|
+
pair = (msg.source_chain, msg.dest_chain)
|
|
358
|
+
last_nonce = self._nonce_tracker.get(pair, -1)
|
|
359
|
+
if msg.nonce <= last_nonce:
|
|
360
|
+
return False, f"Nonce too low: got {msg.nonce}, expected > {last_nonce}"
|
|
361
|
+
|
|
362
|
+
return True, ""
|
|
363
|
+
|
|
364
|
+
def accept_message(self, msg: CrossChainMessage) -> None:
|
|
365
|
+
"""Mark a message as accepted (updates nonce tracker + processed set)."""
|
|
366
|
+
pair = (msg.source_chain, msg.dest_chain)
|
|
367
|
+
self._nonce_tracker[pair] = msg.nonce
|
|
368
|
+
self._processed.add(msg.msg_id)
|
|
369
|
+
msg.status = MessageStatus.CONFIRMED
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ---------------------------------------------------------------------------
|
|
373
|
+
# Chain Router — multi-chain management + message routing
|
|
374
|
+
# ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
class ChainRouter:
|
|
377
|
+
"""Manages multiple local chains and routes cross-chain messages.
|
|
378
|
+
|
|
379
|
+
Each registered chain gets its own ``BridgeRelay``. When a message
|
|
380
|
+
is sent from chain A to chain B, the router:
|
|
381
|
+
|
|
382
|
+
1. Commits the message to chain A's outbox.
|
|
383
|
+
2. Generates a Merkle tree of the outbox batch and anchors the root
|
|
384
|
+
in chain A's next block header (via ``extra_data``).
|
|
385
|
+
3. Submits chain A's block header to chain B's relay.
|
|
386
|
+
4. Delivers the message (with Merkle proof) to chain B's inbox.
|
|
387
|
+
5. Chain B's relay verifies the proof before acceptance.
|
|
388
|
+
|
|
389
|
+
This is the *real* message path — the relay never trusts any payload
|
|
390
|
+
that doesn't verify against an anchored Merkle root.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
def __init__(self):
|
|
394
|
+
# chain_id -> Chain instance
|
|
395
|
+
self._chains: Dict[str, Chain] = {}
|
|
396
|
+
|
|
397
|
+
# chain_id -> BridgeRelay (validates inbound messages)
|
|
398
|
+
self._relays: Dict[str, BridgeRelay] = {}
|
|
399
|
+
|
|
400
|
+
# chain_id -> list of outbound messages not yet batched
|
|
401
|
+
self._outbox: Dict[str, List[CrossChainMessage]] = {}
|
|
402
|
+
|
|
403
|
+
# chain_id -> list of verified inbound messages
|
|
404
|
+
self._inbox: Dict[str, List[CrossChainMessage]] = {}
|
|
405
|
+
|
|
406
|
+
# (source, dest) -> next nonce
|
|
407
|
+
self._nonce_seq: Dict[Tuple[str, str], int] = {}
|
|
408
|
+
|
|
409
|
+
# Connectivity: which chains can talk to each other
|
|
410
|
+
self._connections: Dict[str, Set[str]] = {}
|
|
411
|
+
|
|
412
|
+
# History of all relayed messages (for auditability)
|
|
413
|
+
self._message_log: List[CrossChainMessage] = []
|
|
414
|
+
|
|
415
|
+
# -- Registration ------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
def register_chain(self, chain_id: str, chain: Chain) -> None:
|
|
418
|
+
"""Register a chain instance with the router."""
|
|
419
|
+
if chain_id in self._chains:
|
|
420
|
+
raise ValueError(f"Chain '{chain_id}' already registered")
|
|
421
|
+
self._chains[chain_id] = chain
|
|
422
|
+
self._relays[chain_id] = BridgeRelay(local_chain_id=chain_id)
|
|
423
|
+
self._outbox[chain_id] = []
|
|
424
|
+
self._inbox[chain_id] = []
|
|
425
|
+
self._connections[chain_id] = set()
|
|
426
|
+
logger.info("Router: registered chain '%s'", chain_id)
|
|
427
|
+
|
|
428
|
+
def get_chain(self, chain_id: str) -> Optional[Chain]:
|
|
429
|
+
return self._chains.get(chain_id)
|
|
430
|
+
|
|
431
|
+
def get_relay(self, chain_id: str) -> Optional[BridgeRelay]:
|
|
432
|
+
return self._relays.get(chain_id)
|
|
433
|
+
|
|
434
|
+
def connect(self, chain_a: str, chain_b: str) -> None:
|
|
435
|
+
"""Establish a bidirectional bridge between two chains."""
|
|
436
|
+
for cid in (chain_a, chain_b):
|
|
437
|
+
if cid not in self._chains:
|
|
438
|
+
raise ValueError(f"Chain '{cid}' not registered")
|
|
439
|
+
self._connections[chain_a].add(chain_b)
|
|
440
|
+
self._connections[chain_b].add(chain_a)
|
|
441
|
+
logger.info("Router: connected %s <-> %s", chain_a, chain_b)
|
|
442
|
+
|
|
443
|
+
def is_connected(self, chain_a: str, chain_b: str) -> bool:
|
|
444
|
+
return chain_b in self._connections.get(chain_a, set())
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def chain_ids(self) -> List[str]:
|
|
448
|
+
return list(self._chains.keys())
|
|
449
|
+
|
|
450
|
+
# -- Sending -----------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
def send(
|
|
453
|
+
self,
|
|
454
|
+
source_chain: str,
|
|
455
|
+
dest_chain: str,
|
|
456
|
+
sender: str,
|
|
457
|
+
payload: Dict[str, Any],
|
|
458
|
+
) -> CrossChainMessage:
|
|
459
|
+
"""Enqueue a cross-chain message from *source_chain* to *dest_chain*.
|
|
460
|
+
|
|
461
|
+
The message receives a unique ID and a monotonic nonce for the
|
|
462
|
+
``(source, dest)`` pair. It is added to the source chain's
|
|
463
|
+
outbox, waiting for ``flush_outbox()`` to anchor it in a block.
|
|
464
|
+
"""
|
|
465
|
+
if source_chain not in self._chains:
|
|
466
|
+
raise ValueError(f"Source chain '{source_chain}' not registered")
|
|
467
|
+
if dest_chain not in self._chains:
|
|
468
|
+
raise ValueError(f"Dest chain '{dest_chain}' not registered")
|
|
469
|
+
if not self.is_connected(source_chain, dest_chain):
|
|
470
|
+
raise ValueError(
|
|
471
|
+
f"No bridge between '{source_chain}' and '{dest_chain}'"
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
pair = (source_chain, dest_chain)
|
|
475
|
+
nonce = self._nonce_seq.get(pair, 0)
|
|
476
|
+
self._nonce_seq[pair] = nonce + 1
|
|
477
|
+
|
|
478
|
+
msg = CrossChainMessage(
|
|
479
|
+
msg_id=uuid.uuid4().hex,
|
|
480
|
+
nonce=nonce,
|
|
481
|
+
source_chain=source_chain,
|
|
482
|
+
dest_chain=dest_chain,
|
|
483
|
+
sender=sender,
|
|
484
|
+
payload=payload,
|
|
485
|
+
timestamp=time.time(),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
self._outbox[source_chain].append(msg)
|
|
489
|
+
self._message_log.append(msg)
|
|
490
|
+
logger.info(
|
|
491
|
+
"Router: queued msg %s (nonce=%d) %s -> %s",
|
|
492
|
+
msg.msg_id[:8], nonce, source_chain, dest_chain,
|
|
493
|
+
)
|
|
494
|
+
return msg
|
|
495
|
+
|
|
496
|
+
# -- Flushing / batching -----------------------------------------------
|
|
497
|
+
|
|
498
|
+
def flush_outbox(self, chain_id: str) -> List[CrossChainMessage]:
|
|
499
|
+
"""Anchor all pending outbound messages in a Merkle tree.
|
|
500
|
+
|
|
501
|
+
Steps:
|
|
502
|
+
1. Compute the hash of each pending message.
|
|
503
|
+
2. Build a Merkle tree and compute the root.
|
|
504
|
+
3. Attach the root to the source chain's latest block header
|
|
505
|
+
(via ``tip.header.extra_data`` as ``xchain_root:<root>``).
|
|
506
|
+
4. Generate a Merkle proof for each message.
|
|
507
|
+
5. Move messages from outbox to a "relayed" state.
|
|
508
|
+
|
|
509
|
+
Returns the list of messages that were flushed (now with proofs).
|
|
510
|
+
"""
|
|
511
|
+
pending = self._outbox.get(chain_id, [])
|
|
512
|
+
if not pending:
|
|
513
|
+
return []
|
|
514
|
+
|
|
515
|
+
chain = self._chains[chain_id]
|
|
516
|
+
tip = chain.tip
|
|
517
|
+
if tip is None:
|
|
518
|
+
raise RuntimeError(f"Chain '{chain_id}' has no blocks — create genesis first")
|
|
519
|
+
|
|
520
|
+
# 1. Stamp every message with the current tip so the relay
|
|
521
|
+
# can look up the header later.
|
|
522
|
+
block_height = tip.header.height
|
|
523
|
+
block_hash = tip.hash # capture *before* any modification
|
|
524
|
+
for msg in pending:
|
|
525
|
+
msg.block_height = block_height
|
|
526
|
+
msg.block_hash = block_hash
|
|
527
|
+
|
|
528
|
+
# 2. Compute leaf hashes from these stamped messages.
|
|
529
|
+
leaf_hashes = [m.compute_hash() for m in pending]
|
|
530
|
+
|
|
531
|
+
# 3. Merkle root over the message batch.
|
|
532
|
+
merkle_root = MerkleProofEngine.compute_root(leaf_hashes)
|
|
533
|
+
|
|
534
|
+
# 4. Store the cross-chain root on the chain.
|
|
535
|
+
# We use a separate ledger-style dict so we don't invalidate
|
|
536
|
+
# the block's own hash (which the relay already has).
|
|
537
|
+
if not hasattr(chain, "_xchain_roots"):
|
|
538
|
+
chain._xchain_roots = {}
|
|
539
|
+
chain._xchain_roots[block_height] = merkle_root
|
|
540
|
+
|
|
541
|
+
# Also store in the header's extra_data for informational
|
|
542
|
+
# purposes, but do NOT recompute the block hash.
|
|
543
|
+
tip.header.extra_data = f"xchain_root:{merkle_root}"
|
|
544
|
+
|
|
545
|
+
# 5. Generate per-message inclusion proofs.
|
|
546
|
+
for i, msg in enumerate(pending):
|
|
547
|
+
msg.merkle_root = merkle_root
|
|
548
|
+
msg.merkle_proof = MerkleProofEngine.generate_proof(leaf_hashes, i)
|
|
549
|
+
msg.status = MessageStatus.RELAYED
|
|
550
|
+
|
|
551
|
+
flushed = list(pending)
|
|
552
|
+
self._outbox[chain_id] = []
|
|
553
|
+
|
|
554
|
+
logger.info(
|
|
555
|
+
"Router: flushed %d messages from %s (root=%s)",
|
|
556
|
+
len(flushed), chain_id, merkle_root[:16],
|
|
557
|
+
)
|
|
558
|
+
return flushed
|
|
559
|
+
|
|
560
|
+
# -- Relaying ----------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
def relay(self, messages: List[CrossChainMessage]) -> List[Tuple[CrossChainMessage, bool, str]]:
|
|
563
|
+
"""Relay a batch of flushed messages to their destination chains.
|
|
564
|
+
|
|
565
|
+
For each message:
|
|
566
|
+
1. Submit the source-chain header to the destination's relay.
|
|
567
|
+
2. Verify the message via the relay.
|
|
568
|
+
3. On success, add to the destination's inbox.
|
|
569
|
+
|
|
570
|
+
Returns a list of ``(message, accepted, reason)`` tuples.
|
|
571
|
+
"""
|
|
572
|
+
results: List[Tuple[CrossChainMessage, bool, str]] = []
|
|
573
|
+
|
|
574
|
+
for msg in messages:
|
|
575
|
+
dest_relay = self._relays.get(msg.dest_chain)
|
|
576
|
+
if dest_relay is None:
|
|
577
|
+
results.append((msg, False, f"No relay for chain '{msg.dest_chain}'"))
|
|
578
|
+
continue
|
|
579
|
+
|
|
580
|
+
# Submit source header to dest relay
|
|
581
|
+
source_chain = self._chains.get(msg.source_chain)
|
|
582
|
+
if source_chain is None:
|
|
583
|
+
results.append((msg, False, f"Source chain '{msg.source_chain}' not found"))
|
|
584
|
+
continue
|
|
585
|
+
|
|
586
|
+
source_block = source_chain.get_block(msg.block_height)
|
|
587
|
+
if source_block is None:
|
|
588
|
+
# Try tip
|
|
589
|
+
source_block = source_chain.tip
|
|
590
|
+
|
|
591
|
+
if source_block:
|
|
592
|
+
header_dict = {
|
|
593
|
+
"height": source_block.header.height,
|
|
594
|
+
"hash": source_block.hash,
|
|
595
|
+
"prev_hash": source_block.header.prev_hash,
|
|
596
|
+
"extra_data": source_block.header.extra_data,
|
|
597
|
+
"state_root": source_block.header.state_root,
|
|
598
|
+
"timestamp": source_block.header.timestamp,
|
|
599
|
+
}
|
|
600
|
+
dest_relay.submit_header(msg.source_chain, header_dict)
|
|
601
|
+
|
|
602
|
+
# Verify
|
|
603
|
+
ok, reason = dest_relay.verify_message(msg)
|
|
604
|
+
if ok:
|
|
605
|
+
dest_relay.accept_message(msg)
|
|
606
|
+
self._inbox[msg.dest_chain].append(msg)
|
|
607
|
+
results.append((msg, True, ""))
|
|
608
|
+
logger.info(
|
|
609
|
+
"Router: relayed msg %s to %s (nonce=%d)",
|
|
610
|
+
msg.msg_id[:8], msg.dest_chain, msg.nonce,
|
|
611
|
+
)
|
|
612
|
+
else:
|
|
613
|
+
msg.status = MessageStatus.FAILED
|
|
614
|
+
results.append((msg, False, reason))
|
|
615
|
+
logger.warning(
|
|
616
|
+
"Router: rejected msg %s to %s: %s",
|
|
617
|
+
msg.msg_id[:8], msg.dest_chain, reason,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
return results
|
|
621
|
+
|
|
622
|
+
def send_and_relay(
|
|
623
|
+
self,
|
|
624
|
+
source_chain: str,
|
|
625
|
+
dest_chain: str,
|
|
626
|
+
sender: str,
|
|
627
|
+
payload: Dict[str, Any],
|
|
628
|
+
) -> Tuple[CrossChainMessage, bool, str]:
|
|
629
|
+
"""Convenience: send + flush + relay in one call.
|
|
630
|
+
|
|
631
|
+
Returns ``(message, accepted, reason)``.
|
|
632
|
+
"""
|
|
633
|
+
msg = self.send(source_chain, dest_chain, sender, payload)
|
|
634
|
+
flushed = self.flush_outbox(source_chain)
|
|
635
|
+
results = self.relay(flushed)
|
|
636
|
+
for m, ok, reason in results:
|
|
637
|
+
if m.msg_id == msg.msg_id:
|
|
638
|
+
return m, ok, reason
|
|
639
|
+
return msg, False, "Message not found in relay results"
|
|
640
|
+
|
|
641
|
+
# -- Inbox -------------------------------------------------------------
|
|
642
|
+
|
|
643
|
+
def get_inbox(self, chain_id: str) -> List[CrossChainMessage]:
|
|
644
|
+
"""Get all verified inbound messages for a chain."""
|
|
645
|
+
return list(self._inbox.get(chain_id, []))
|
|
646
|
+
|
|
647
|
+
def pop_inbox(self, chain_id: str) -> List[CrossChainMessage]:
|
|
648
|
+
"""Pop all verified inbound messages for a chain."""
|
|
649
|
+
msgs = list(self._inbox.get(chain_id, []))
|
|
650
|
+
self._inbox[chain_id] = []
|
|
651
|
+
return msgs
|
|
652
|
+
|
|
653
|
+
# -- Info / Audit ------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
def get_router_info(self) -> Dict[str, Any]:
|
|
656
|
+
"""Get a summary of the router's state."""
|
|
657
|
+
return {
|
|
658
|
+
"chains": list(self._chains.keys()),
|
|
659
|
+
"connections": {k: list(v) for k, v in self._connections.items()},
|
|
660
|
+
"outbox_sizes": {k: len(v) for k, v in self._outbox.items()},
|
|
661
|
+
"inbox_sizes": {k: len(v) for k, v in self._inbox.items()},
|
|
662
|
+
"total_messages_relayed": len(self._message_log),
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
def get_message_log(self) -> List[Dict[str, Any]]:
|
|
666
|
+
"""Full audit trail of all cross-chain messages."""
|
|
667
|
+
return [m.to_dict() for m in self._message_log]
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
# ---------------------------------------------------------------------------
|
|
671
|
+
# Bridge Contract — lock-and-mint / burn-and-release asset transfer
|
|
672
|
+
# ---------------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
class BridgeContract:
|
|
675
|
+
"""Cross-chain asset bridge using lock-and-mint / burn-and-release.
|
|
676
|
+
|
|
677
|
+
This operates on two chains (``source`` and ``dest``) via a
|
|
678
|
+
``ChainRouter``. It maintains escrow balances on each side and
|
|
679
|
+
uses verified cross-chain messages for every state transition.
|
|
680
|
+
|
|
681
|
+
Lock-and-mint (source → dest)
|
|
682
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
683
|
+
1. Lock ``amount`` from ``sender`` on the source chain (debit balance).
|
|
684
|
+
2. Send a cross-chain message ``{"action": "mint", ...}`` to dest.
|
|
685
|
+
3. The relay verifies the message and mints ``amount`` on dest.
|
|
686
|
+
|
|
687
|
+
Burn-and-release (dest → source)
|
|
688
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
689
|
+
1. Burn ``amount`` from ``sender`` on the dest chain (debit balance).
|
|
690
|
+
2. Send a cross-chain message ``{"action": "release", ...}`` to source.
|
|
691
|
+
3. The relay verifies and releases ``amount`` on source.
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
def __init__(
|
|
695
|
+
self,
|
|
696
|
+
router: ChainRouter,
|
|
697
|
+
source_chain: str,
|
|
698
|
+
dest_chain: str,
|
|
699
|
+
bridge_address: str = "",
|
|
700
|
+
):
|
|
701
|
+
self._router = router
|
|
702
|
+
self._source = source_chain
|
|
703
|
+
self._dest = dest_chain
|
|
704
|
+
self._address = bridge_address or f"bridge_{source_chain}_{dest_chain}"
|
|
705
|
+
|
|
706
|
+
# Escrow: chain_id -> {address: locked_balance}
|
|
707
|
+
self._escrow: Dict[str, Dict[str, int]] = {
|
|
708
|
+
source_chain: {},
|
|
709
|
+
dest_chain: {},
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
# Minted (wrapped) balances on dest
|
|
713
|
+
self._minted: Dict[str, int] = {} # address -> minted_amount
|
|
714
|
+
|
|
715
|
+
# Released balances on source (after burn)
|
|
716
|
+
self._released: Dict[str, int] = {} # address -> released_amount
|
|
717
|
+
|
|
718
|
+
# Transaction log
|
|
719
|
+
self._tx_log: List[Dict[str, Any]] = []
|
|
720
|
+
|
|
721
|
+
# Total value locked
|
|
722
|
+
self._total_locked: int = 0
|
|
723
|
+
self._total_minted: int = 0
|
|
724
|
+
|
|
725
|
+
@property
|
|
726
|
+
def total_value_locked(self) -> int:
|
|
727
|
+
return self._total_locked
|
|
728
|
+
|
|
729
|
+
@property
|
|
730
|
+
def total_minted(self) -> int:
|
|
731
|
+
return self._total_minted
|
|
732
|
+
|
|
733
|
+
def get_escrow_balance(self, chain_id: str, address: str) -> int:
|
|
734
|
+
return self._escrow.get(chain_id, {}).get(address, 0)
|
|
735
|
+
|
|
736
|
+
def get_minted_balance(self, address: str) -> int:
|
|
737
|
+
return self._minted.get(address, 0)
|
|
738
|
+
|
|
739
|
+
# -- Lock & Mint -------------------------------------------------------
|
|
740
|
+
|
|
741
|
+
def lock_and_mint(
|
|
742
|
+
self,
|
|
743
|
+
sender: str,
|
|
744
|
+
amount: int,
|
|
745
|
+
recipient: Optional[str] = None,
|
|
746
|
+
) -> Dict[str, Any]:
|
|
747
|
+
"""Lock tokens on source chain and mint wrapped tokens on dest.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
sender: Address on the source chain.
|
|
751
|
+
amount: Amount to transfer.
|
|
752
|
+
recipient: Destination address (defaults to ``sender``).
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Receipt dict with ``success``, ``msg_id``, etc.
|
|
756
|
+
"""
|
|
757
|
+
recipient = recipient or sender
|
|
758
|
+
|
|
759
|
+
if amount <= 0:
|
|
760
|
+
return {"success": False, "error": "Amount must be positive"}
|
|
761
|
+
|
|
762
|
+
# 1. Verify sender has sufficient balance on source chain
|
|
763
|
+
source_chain = self._router.get_chain(self._source)
|
|
764
|
+
if source_chain is None:
|
|
765
|
+
return {"success": False, "error": f"Source chain '{self._source}' not found"}
|
|
766
|
+
|
|
767
|
+
sender_acct = source_chain.get_account(sender)
|
|
768
|
+
if sender_acct.get("balance", 0) < amount:
|
|
769
|
+
return {
|
|
770
|
+
"success": False,
|
|
771
|
+
"error": f"Insufficient balance: have {sender_acct.get('balance', 0)}, need {amount}",
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
# 2. Lock: debit sender on source chain, credit escrow
|
|
775
|
+
sender_acct["balance"] -= amount
|
|
776
|
+
self._escrow[self._source][sender] = (
|
|
777
|
+
self._escrow[self._source].get(sender, 0) + amount
|
|
778
|
+
)
|
|
779
|
+
self._total_locked += amount
|
|
780
|
+
|
|
781
|
+
# 3. Send cross-chain message
|
|
782
|
+
msg, accepted, reason = self._router.send_and_relay(
|
|
783
|
+
source_chain=self._source,
|
|
784
|
+
dest_chain=self._dest,
|
|
785
|
+
sender=sender,
|
|
786
|
+
payload={
|
|
787
|
+
"action": "mint",
|
|
788
|
+
"sender": sender,
|
|
789
|
+
"recipient": recipient,
|
|
790
|
+
"amount": amount,
|
|
791
|
+
"bridge": self._address,
|
|
792
|
+
},
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
if not accepted:
|
|
796
|
+
# Rollback lock
|
|
797
|
+
sender_acct["balance"] += amount
|
|
798
|
+
self._escrow[self._source][sender] -= amount
|
|
799
|
+
self._total_locked -= amount
|
|
800
|
+
return {
|
|
801
|
+
"success": False,
|
|
802
|
+
"error": f"Cross-chain message rejected: {reason}",
|
|
803
|
+
"msg_id": msg.msg_id,
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
# 4. Mint on destination
|
|
807
|
+
self._minted[recipient] = self._minted.get(recipient, 0) + amount
|
|
808
|
+
self._total_minted += amount
|
|
809
|
+
|
|
810
|
+
# Credit the dest-chain account too
|
|
811
|
+
dest_chain = self._router.get_chain(self._dest)
|
|
812
|
+
if dest_chain:
|
|
813
|
+
recv_acct = dest_chain.get_account(recipient)
|
|
814
|
+
recv_acct["balance"] = recv_acct.get("balance", 0) + amount
|
|
815
|
+
|
|
816
|
+
receipt = {
|
|
817
|
+
"success": True,
|
|
818
|
+
"action": "lock_and_mint",
|
|
819
|
+
"sender": sender,
|
|
820
|
+
"recipient": recipient,
|
|
821
|
+
"amount": amount,
|
|
822
|
+
"source_chain": self._source,
|
|
823
|
+
"dest_chain": self._dest,
|
|
824
|
+
"msg_id": msg.msg_id,
|
|
825
|
+
"nonce": msg.nonce,
|
|
826
|
+
"block_height": msg.block_height,
|
|
827
|
+
"merkle_root": msg.merkle_root,
|
|
828
|
+
}
|
|
829
|
+
self._tx_log.append(receipt)
|
|
830
|
+
logger.info(
|
|
831
|
+
"Bridge: lock_and_mint %d from %s@%s -> %s@%s (msg=%s)",
|
|
832
|
+
amount, sender, self._source, recipient, self._dest, msg.msg_id[:8],
|
|
833
|
+
)
|
|
834
|
+
return receipt
|
|
835
|
+
|
|
836
|
+
# -- Burn & Release ----------------------------------------------------
|
|
837
|
+
|
|
838
|
+
def burn_and_release(
|
|
839
|
+
self,
|
|
840
|
+
sender: str,
|
|
841
|
+
amount: int,
|
|
842
|
+
recipient: Optional[str] = None,
|
|
843
|
+
) -> Dict[str, Any]:
|
|
844
|
+
"""Burn wrapped tokens on dest chain and release locked tokens on source.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
sender: Address on the dest chain (must hold minted tokens).
|
|
848
|
+
amount: Amount to burn and release.
|
|
849
|
+
recipient: Source-chain address to release to (defaults to ``sender``).
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
Receipt dict.
|
|
853
|
+
"""
|
|
854
|
+
recipient = recipient or sender
|
|
855
|
+
|
|
856
|
+
if amount <= 0:
|
|
857
|
+
return {"success": False, "error": "Amount must be positive"}
|
|
858
|
+
|
|
859
|
+
# 1. Verify sender has sufficient minted balance
|
|
860
|
+
if self._minted.get(sender, 0) < amount:
|
|
861
|
+
return {
|
|
862
|
+
"success": False,
|
|
863
|
+
"error": f"Insufficient minted balance: have {self._minted.get(sender, 0)}, need {amount}",
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
# 2. Burn: debit minted balance on dest
|
|
867
|
+
self._minted[sender] -= amount
|
|
868
|
+
self._total_minted -= amount
|
|
869
|
+
|
|
870
|
+
# Also debit the dest-chain account
|
|
871
|
+
dest_chain = self._router.get_chain(self._dest)
|
|
872
|
+
if dest_chain:
|
|
873
|
+
acct = dest_chain.get_account(sender)
|
|
874
|
+
acct["balance"] = max(0, acct.get("balance", 0) - amount)
|
|
875
|
+
|
|
876
|
+
# 3. Send cross-chain message
|
|
877
|
+
msg, accepted, reason = self._router.send_and_relay(
|
|
878
|
+
source_chain=self._dest,
|
|
879
|
+
dest_chain=self._source,
|
|
880
|
+
sender=sender,
|
|
881
|
+
payload={
|
|
882
|
+
"action": "release",
|
|
883
|
+
"sender": sender,
|
|
884
|
+
"recipient": recipient,
|
|
885
|
+
"amount": amount,
|
|
886
|
+
"bridge": self._address,
|
|
887
|
+
},
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
if not accepted:
|
|
891
|
+
# Rollback burn
|
|
892
|
+
self._minted[sender] += amount
|
|
893
|
+
self._total_minted += amount
|
|
894
|
+
if dest_chain:
|
|
895
|
+
acct = dest_chain.get_account(sender)
|
|
896
|
+
acct["balance"] = acct.get("balance", 0) + amount
|
|
897
|
+
return {
|
|
898
|
+
"success": False,
|
|
899
|
+
"error": f"Cross-chain message rejected: {reason}",
|
|
900
|
+
"msg_id": msg.msg_id,
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
# 4. Release on source
|
|
904
|
+
source_chain = self._router.get_chain(self._source)
|
|
905
|
+
if source_chain:
|
|
906
|
+
recv_acct = source_chain.get_account(recipient)
|
|
907
|
+
recv_acct["balance"] = recv_acct.get("balance", 0) + amount
|
|
908
|
+
|
|
909
|
+
# Reduce escrow
|
|
910
|
+
escrowed = self._escrow[self._source].get(recipient, 0)
|
|
911
|
+
self._escrow[self._source][recipient] = max(0, escrowed - amount)
|
|
912
|
+
self._total_locked = max(0, self._total_locked - amount)
|
|
913
|
+
|
|
914
|
+
receipt = {
|
|
915
|
+
"success": True,
|
|
916
|
+
"action": "burn_and_release",
|
|
917
|
+
"sender": sender,
|
|
918
|
+
"recipient": recipient,
|
|
919
|
+
"amount": amount,
|
|
920
|
+
"source_chain": self._dest,
|
|
921
|
+
"dest_chain": self._source,
|
|
922
|
+
"msg_id": msg.msg_id,
|
|
923
|
+
"nonce": msg.nonce,
|
|
924
|
+
"block_height": msg.block_height,
|
|
925
|
+
"merkle_root": msg.merkle_root,
|
|
926
|
+
}
|
|
927
|
+
self._tx_log.append(receipt)
|
|
928
|
+
logger.info(
|
|
929
|
+
"Bridge: burn_and_release %d from %s@%s -> %s@%s (msg=%s)",
|
|
930
|
+
amount, sender, self._dest, recipient, self._source, msg.msg_id[:8],
|
|
931
|
+
)
|
|
932
|
+
return receipt
|
|
933
|
+
|
|
934
|
+
# -- Queries -----------------------------------------------------------
|
|
935
|
+
|
|
936
|
+
def get_bridge_info(self) -> Dict[str, Any]:
|
|
937
|
+
return {
|
|
938
|
+
"address": self._address,
|
|
939
|
+
"source_chain": self._source,
|
|
940
|
+
"dest_chain": self._dest,
|
|
941
|
+
"total_value_locked": self._total_locked,
|
|
942
|
+
"total_minted": self._total_minted,
|
|
943
|
+
"escrow": {
|
|
944
|
+
k: dict(v) for k, v in self._escrow.items()
|
|
945
|
+
},
|
|
946
|
+
"minted_balances": dict(self._minted),
|
|
947
|
+
"tx_count": len(self._tx_log),
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
def get_tx_log(self) -> List[Dict[str, Any]]:
|
|
951
|
+
return list(self._tx_log)
|