zexus 1.6.8 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +12 -5
  2. package/package.json +1 -1
  3. package/src/__init__.py +7 -0
  4. package/src/zexus/__init__.py +1 -1
  5. package/src/zexus/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/src/zexus/__pycache__/capability_system.cpython-312.pyc +0 -0
  7. package/src/zexus/__pycache__/debug_sanitizer.cpython-312.pyc +0 -0
  8. package/src/zexus/__pycache__/environment.cpython-312.pyc +0 -0
  9. package/src/zexus/__pycache__/error_reporter.cpython-312.pyc +0 -0
  10. package/src/zexus/__pycache__/input_validation.cpython-312.pyc +0 -0
  11. package/src/zexus/__pycache__/lexer.cpython-312.pyc +0 -0
  12. package/src/zexus/__pycache__/module_cache.cpython-312.pyc +0 -0
  13. package/src/zexus/__pycache__/module_manager.cpython-312.pyc +0 -0
  14. package/src/zexus/__pycache__/object.cpython-312.pyc +0 -0
  15. package/src/zexus/__pycache__/security.cpython-312.pyc +0 -0
  16. package/src/zexus/__pycache__/security_enforcement.cpython-312.pyc +0 -0
  17. package/src/zexus/__pycache__/syntax_validator.cpython-312.pyc +0 -0
  18. package/src/zexus/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  19. package/src/zexus/__pycache__/zexus_token.cpython-312.pyc +0 -0
  20. package/src/zexus/access_control_system/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/src/zexus/access_control_system/__pycache__/access_control.cpython-312.pyc +0 -0
  22. package/src/zexus/advanced_types.py +17 -2
  23. package/src/zexus/blockchain/__init__.py +411 -0
  24. package/src/zexus/blockchain/accelerator.py +1160 -0
  25. package/src/zexus/blockchain/chain.py +660 -0
  26. package/src/zexus/blockchain/consensus.py +821 -0
  27. package/src/zexus/blockchain/contract_vm.py +1019 -0
  28. package/src/zexus/blockchain/crypto.py +79 -14
  29. package/src/zexus/blockchain/events.py +526 -0
  30. package/src/zexus/blockchain/loadtest.py +721 -0
  31. package/src/zexus/blockchain/monitoring.py +350 -0
  32. package/src/zexus/blockchain/mpt.py +716 -0
  33. package/src/zexus/blockchain/multichain.py +951 -0
  34. package/src/zexus/blockchain/multiprocess_executor.py +338 -0
  35. package/src/zexus/blockchain/network.py +886 -0
  36. package/src/zexus/blockchain/node.py +666 -0
  37. package/src/zexus/blockchain/rpc.py +1203 -0
  38. package/src/zexus/blockchain/rust_bridge.py +421 -0
  39. package/src/zexus/blockchain/storage.py +423 -0
  40. package/src/zexus/blockchain/tokens.py +750 -0
  41. package/src/zexus/blockchain/upgradeable.py +1004 -0
  42. package/src/zexus/blockchain/verification.py +1602 -0
  43. package/src/zexus/blockchain/wallet.py +621 -0
  44. package/src/zexus/capability_system.py +184 -9
  45. package/src/zexus/cli/__pycache__/main.cpython-312.pyc +0 -0
  46. package/src/zexus/cli/main.py +383 -34
  47. package/src/zexus/cli/zpm.py +1 -1
  48. package/src/zexus/compiler/__pycache__/bytecode.cpython-312.pyc +0 -0
  49. package/src/zexus/compiler/__pycache__/lexer.cpython-312.pyc +0 -0
  50. package/src/zexus/compiler/__pycache__/parser.cpython-312.pyc +0 -0
  51. package/src/zexus/compiler/__pycache__/semantic.cpython-312.pyc +0 -0
  52. package/src/zexus/compiler/__pycache__/zexus_ast.cpython-312.pyc +0 -0
  53. package/src/zexus/compiler/bytecode.py +124 -7
  54. package/src/zexus/compiler/compat_runtime.py +6 -2
  55. package/src/zexus/compiler/lexer.py +16 -5
  56. package/src/zexus/compiler/parser.py +108 -7
  57. package/src/zexus/compiler/semantic.py +18 -19
  58. package/src/zexus/compiler/zexus_ast.py +26 -1
  59. package/src/zexus/concurrency_system.py +79 -0
  60. package/src/zexus/config.py +54 -0
  61. package/src/zexus/crypto_bridge.py +244 -8
  62. package/src/zexus/dap/__init__.py +10 -0
  63. package/src/zexus/dap/__main__.py +4 -0
  64. package/src/zexus/dap/dap_server.py +391 -0
  65. package/src/zexus/dap/debug_engine.py +298 -0
  66. package/src/zexus/environment.py +112 -9
  67. package/src/zexus/evaluator/__pycache__/bytecode_compiler.cpython-312.pyc +0 -0
  68. package/src/zexus/evaluator/__pycache__/core.cpython-312.pyc +0 -0
  69. package/src/zexus/evaluator/__pycache__/expressions.cpython-312.pyc +0 -0
  70. package/src/zexus/evaluator/__pycache__/functions.cpython-312.pyc +0 -0
  71. package/src/zexus/evaluator/__pycache__/resource_limiter.cpython-312.pyc +0 -0
  72. package/src/zexus/evaluator/__pycache__/statements.cpython-312.pyc +0 -0
  73. package/src/zexus/evaluator/__pycache__/unified_execution.cpython-312.pyc +0 -0
  74. package/src/zexus/evaluator/__pycache__/utils.cpython-312.pyc +0 -0
  75. package/src/zexus/evaluator/bytecode_compiler.py +457 -37
  76. package/src/zexus/evaluator/core.py +644 -50
  77. package/src/zexus/evaluator/expressions.py +358 -62
  78. package/src/zexus/evaluator/functions.py +458 -20
  79. package/src/zexus/evaluator/resource_limiter.py +4 -4
  80. package/src/zexus/evaluator/statements.py +774 -122
  81. package/src/zexus/evaluator/unified_execution.py +573 -72
  82. package/src/zexus/evaluator/utils.py +14 -2
  83. package/src/zexus/evaluator_original.py +1 -1
  84. package/src/zexus/event_loop.py +186 -0
  85. package/src/zexus/lexer.py +742 -458
  86. package/src/zexus/lsp/__init__.py +1 -1
  87. package/src/zexus/lsp/definition_provider.py +163 -9
  88. package/src/zexus/lsp/server.py +22 -8
  89. package/src/zexus/lsp/symbol_provider.py +182 -9
  90. package/src/zexus/module_cache.py +239 -9
  91. package/src/zexus/module_manager.py +129 -1
  92. package/src/zexus/object.py +76 -6
  93. package/src/zexus/parser/__pycache__/parser.cpython-312.pyc +0 -0
  94. package/src/zexus/parser/__pycache__/strategy_context.cpython-312.pyc +0 -0
  95. package/src/zexus/parser/__pycache__/strategy_structural.cpython-312.pyc +0 -0
  96. package/src/zexus/parser/parser.py +1349 -408
  97. package/src/zexus/parser/strategy_context.py +755 -58
  98. package/src/zexus/parser/strategy_structural.py +121 -21
  99. package/src/zexus/persistence.py +15 -1
  100. package/src/zexus/renderer/__init__.py +61 -0
  101. package/src/zexus/renderer/__pycache__/__init__.cpython-312.pyc +0 -0
  102. package/src/zexus/renderer/__pycache__/backend.cpython-312.pyc +0 -0
  103. package/src/zexus/renderer/__pycache__/canvas.cpython-312.pyc +0 -0
  104. package/src/zexus/renderer/__pycache__/color_system.cpython-312.pyc +0 -0
  105. package/src/zexus/renderer/__pycache__/layout.cpython-312.pyc +0 -0
  106. package/src/zexus/renderer/__pycache__/main_renderer.cpython-312.pyc +0 -0
  107. package/src/zexus/renderer/__pycache__/painter.cpython-312.pyc +0 -0
  108. package/src/zexus/renderer/backend.py +261 -0
  109. package/src/zexus/renderer/canvas.py +78 -0
  110. package/src/zexus/renderer/color_system.py +201 -0
  111. package/src/zexus/renderer/graphics.py +31 -0
  112. package/src/zexus/renderer/layout.py +222 -0
  113. package/src/zexus/renderer/main_renderer.py +66 -0
  114. package/src/zexus/renderer/painter.py +30 -0
  115. package/src/zexus/renderer/tk_backend.py +208 -0
  116. package/src/zexus/renderer/web_backend.py +260 -0
  117. package/src/zexus/runtime/__init__.py +10 -2
  118. package/src/zexus/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  119. package/src/zexus/runtime/__pycache__/async_runtime.cpython-312.pyc +0 -0
  120. package/src/zexus/runtime/__pycache__/load_manager.cpython-312.pyc +0 -0
  121. package/src/zexus/runtime/file_flags.py +137 -0
  122. package/src/zexus/runtime/load_manager.py +368 -0
  123. package/src/zexus/safety/__pycache__/__init__.cpython-312.pyc +0 -0
  124. package/src/zexus/safety/__pycache__/memory_safety.cpython-312.pyc +0 -0
  125. package/src/zexus/security.py +424 -34
  126. package/src/zexus/stdlib/fs.py +23 -18
  127. package/src/zexus/stdlib/http.py +289 -186
  128. package/src/zexus/stdlib/sockets.py +207 -163
  129. package/src/zexus/stdlib/websockets.py +282 -0
  130. package/src/zexus/stdlib_integration.py +369 -2
  131. package/src/zexus/strategy_recovery.py +6 -3
  132. package/src/zexus/type_checker.py +423 -0
  133. package/src/zexus/virtual_filesystem.py +189 -2
  134. package/src/zexus/vm/__init__.py +113 -3
  135. package/src/zexus/vm/__pycache__/async_optimizer.cpython-312.pyc +0 -0
  136. package/src/zexus/vm/__pycache__/bytecode.cpython-312.pyc +0 -0
  137. package/src/zexus/vm/__pycache__/bytecode_converter.cpython-312.pyc +0 -0
  138. package/src/zexus/vm/__pycache__/cache.cpython-312.pyc +0 -0
  139. package/src/zexus/vm/__pycache__/compiler.cpython-312.pyc +0 -0
  140. package/src/zexus/vm/__pycache__/gas_metering.cpython-312.pyc +0 -0
  141. package/src/zexus/vm/__pycache__/jit.cpython-312.pyc +0 -0
  142. package/src/zexus/vm/__pycache__/parallel_vm.cpython-312.pyc +0 -0
  143. package/src/zexus/vm/__pycache__/vm.cpython-312.pyc +0 -0
  144. package/src/zexus/vm/async_optimizer.py +80 -6
  145. package/src/zexus/vm/binary_bytecode.py +659 -0
  146. package/src/zexus/vm/bytecode.py +59 -11
  147. package/src/zexus/vm/bytecode_converter.py +26 -12
  148. package/src/zexus/vm/cabi.c +1985 -0
  149. package/src/zexus/vm/cabi.cpython-312-x86_64-linux-gnu.so +0 -0
  150. package/src/zexus/vm/cabi.h +127 -0
  151. package/src/zexus/vm/cache.py +561 -17
  152. package/src/zexus/vm/compiler.py +818 -51
  153. package/src/zexus/vm/fastops.c +15743 -0
  154. package/src/zexus/vm/fastops.cpython-312-x86_64-linux-gnu.so +0 -0
  155. package/src/zexus/vm/fastops.pyx +288 -0
  156. package/src/zexus/vm/gas_metering.py +50 -9
  157. package/src/zexus/vm/jit.py +364 -20
  158. package/src/zexus/vm/native_jit_backend.py +1816 -0
  159. package/src/zexus/vm/native_runtime.cpp +1388 -0
  160. package/src/zexus/vm/native_runtime.cpython-312-x86_64-linux-gnu.so +0 -0
  161. package/src/zexus/vm/optimizer.py +161 -11
  162. package/src/zexus/vm/parallel_vm.py +140 -45
  163. package/src/zexus/vm/peephole_optimizer.py +82 -4
  164. package/src/zexus/vm/profiler.py +38 -18
  165. package/src/zexus/vm/register_allocator.py +16 -5
  166. package/src/zexus/vm/register_vm.py +8 -5
  167. package/src/zexus/vm/vm.py +3581 -531
  168. package/src/zexus/vm/wasm_compiler.py +658 -0
  169. package/src/zexus/zexus_ast.py +137 -11
  170. package/src/zexus/zexus_token.py +16 -5
  171. package/src/zexus/zpm/installer.py +55 -15
  172. package/src/zexus/zpm/package_manager.py +1 -1
  173. package/src/zexus/zpm/registry.py +257 -28
  174. package/src/zexus.egg-info/PKG-INFO +16 -6
  175. package/src/zexus.egg-info/SOURCES.txt +129 -17
  176. package/src/zexus.egg-info/entry_points.txt +1 -0
  177. package/src/zexus.egg-info/requires.txt +4 -0
@@ -0,0 +1,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)