Skip to content

Commit e527e38

Browse files
committed
Add diamond proxy upgrade test
1 parent 8dfb89b commit e527e38

File tree

11 files changed

+607
-0
lines changed

11 files changed

+607
-0
lines changed

l1-contracts/run_1.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Removing broadcast files...
2+
Removing cache files...

l1-contracts/run_2.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Removing broadcast files...
2+
Removing cache files...

l1-contracts/run_3.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Removing broadcast files...
2+
Removing cache files...

l1-contracts/run_4.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Removing broadcast files...
2+
Removing cache files...

l1-contracts/run_5.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Removing broadcast files...
2+
Removing cache files...
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Build outputs
2+
dist/
3+
*.js
4+
*.d.ts
5+
6+
# Dependencies
7+
node_modules/
8+
9+
# Outputs
10+
outputs/
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Dependencies
2+
node_modules/
3+
package-lock.json
4+
5+
# Build outputs
6+
dist/
7+
*.tsbuildinfo
8+
9+
# Runtime outputs
10+
outputs/
11+
12+
# Logs
13+
*.log
14+
npm-debug.log*
15+
yarn-debug.log*
16+
yarn-error.log*
17+
18+
# OS files
19+
.DS_Store
20+
Thumbs.db
21+
22+
# IDE
23+
.vscode/
24+
.idea/
25+
*.swp
26+
*.swo
27+
*~
28+
29+
# Generated configs (chain-specific)
30+
config/chain-*.toml
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import type { JsonRpcProvider } from "ethers";
2+
import { Contract, Wallet, AbiCoder } from "ethers";
3+
import type { ChainAddresses, CoreDeployedAddresses } from "./types";
4+
import { sleep } from "./utils";
5+
6+
/**
7+
* L2→L2 Cross-Chain Relayer
8+
*
9+
* Monitors L2 chains for special cross-chain message transactions and relays them
10+
* through L1 to the target L2 chain.
11+
*
12+
* Flow: L2 Source → L1 Bridgehub → L2 Target
13+
*
14+
* To send a cross-chain message from L2, send a transaction to a special marker address:
15+
* - To: 0x0000000000000000000000000000000000000420 (CROSS_CHAIN_MESSENGER)
16+
* - Data: ABI-encoded (targetChainId, targetAddress, targetCalldata)
17+
*/
18+
export class L2ToL2Relayer {
19+
private l1Provider: JsonRpcProvider;
20+
private l2Providers: Map<number, JsonRpcProvider>;
21+
private l1Wallet: Wallet;
22+
private l1Addresses: CoreDeployedAddresses;
23+
private chainAddresses: Map<number, ChainAddresses>;
24+
private isRunning: boolean = false;
25+
private pollingInterval: number = 2000; // 2 seconds
26+
private lastProcessedBlocks: Map<number, number> = new Map();
27+
private processedTxHashes: Set<string> = new Set();
28+
29+
// InteropCenter system contract address
30+
private readonly INTEROP_CENTER_ADDR = "0x000000000000000000000000000000000001000d";
31+
32+
// InteropBundleSent event signature
33+
// event InteropBundleSent(bytes32 l2l1MsgHash, bytes32 interopBundleHash, InteropBundle interopBundle)
34+
private readonly INTEROP_BUNDLE_SENT_TOPIC = "0xd5e1642d9c6ff371d1f102384c70a9a38530493e4747a53919f128685013cb6e";
35+
36+
constructor(
37+
l1Provider: JsonRpcProvider,
38+
l2Providers: Map<number, JsonRpcProvider>,
39+
privateKey: string,
40+
l1Addresses: CoreDeployedAddresses,
41+
chainAddresses: Map<number, ChainAddresses>,
42+
pollingIntervalMs: number = 2000
43+
) {
44+
this.l1Provider = l1Provider;
45+
this.l2Providers = l2Providers;
46+
this.l1Wallet = new Wallet(privateKey, l1Provider);
47+
this.l1Addresses = l1Addresses;
48+
this.chainAddresses = chainAddresses;
49+
this.pollingInterval = pollingIntervalMs;
50+
51+
// Initialize last processed blocks for each L2
52+
for (const chainId of l2Providers.keys()) {
53+
this.lastProcessedBlocks.set(chainId, 0);
54+
}
55+
}
56+
57+
async start(): Promise<void> {
58+
console.log("🌉 Starting L2→L2 Cross-Chain Relayer...");
59+
60+
// Get current block numbers as starting points
61+
for (const [chainId, provider] of this.l2Providers.entries()) {
62+
const blockNumber = await provider.getBlockNumber();
63+
this.lastProcessedBlocks.set(chainId, blockNumber);
64+
console.log(` Starting from L2 chain ${chainId} block ${blockNumber}`);
65+
}
66+
67+
this.isRunning = true;
68+
69+
// Start polling loop
70+
this.poll();
71+
72+
console.log("✅ L2→L2 Relayer started");
73+
}
74+
75+
async stop(): Promise<void> {
76+
console.log("🛑 Stopping L2→L2 Cross-Chain Relayer...");
77+
this.isRunning = false;
78+
console.log("✅ L2→L2 Relayer stopped");
79+
}
80+
81+
private async poll(): Promise<void> {
82+
while (this.isRunning) {
83+
try {
84+
await this.processAllChains();
85+
} catch (error) {
86+
console.error("❌ L2→L2 Relayer error:", error);
87+
}
88+
89+
await sleep(this.pollingInterval);
90+
}
91+
}
92+
93+
private async processAllChains(): Promise<void> {
94+
for (const [sourceChainId, provider] of this.l2Providers.entries()) {
95+
await this.processChain(sourceChainId, provider);
96+
}
97+
}
98+
99+
private async processChain(sourceChainId: number, provider: JsonRpcProvider): Promise<void> {
100+
const currentBlock = await provider.getBlockNumber();
101+
const lastProcessed = this.lastProcessedBlocks.get(sourceChainId) || 0;
102+
103+
if (currentBlock <= lastProcessed) {
104+
return;
105+
}
106+
107+
const fromBlock = lastProcessed + 1;
108+
const toBlock = currentBlock;
109+
110+
// Process blocks in batches to avoid overwhelming the RPC
111+
const batchSize = 10;
112+
for (let start = fromBlock; start <= toBlock; start += batchSize) {
113+
const end = Math.min(start + batchSize - 1, toBlock);
114+
await this.processBlockRange(sourceChainId, provider, start, end);
115+
}
116+
117+
this.lastProcessedBlocks.set(sourceChainId, currentBlock);
118+
}
119+
120+
private async processBlockRange(
121+
sourceChainId: number,
122+
provider: JsonRpcProvider,
123+
fromBlock: number,
124+
toBlock: number
125+
): Promise<void> {
126+
for (let blockNum = fromBlock; blockNum <= toBlock; blockNum++) {
127+
const block = await provider.getBlock(blockNum, true);
128+
129+
if (!block || !block.transactions) {
130+
continue;
131+
}
132+
133+
for (const txHash of block.transactions) {
134+
await this.processTransaction(sourceChainId, provider, txHash as string);
135+
}
136+
}
137+
}
138+
139+
private async processTransaction(
140+
sourceChainId: number,
141+
provider: JsonRpcProvider,
142+
txHash: string
143+
): Promise<void> {
144+
// Skip if already processed
145+
if (this.processedTxHashes.has(txHash)) {
146+
return;
147+
}
148+
149+
// Get transaction receipt to check for InteropBundleSent event
150+
const receipt = await provider.getTransactionReceipt(txHash);
151+
152+
if (!receipt || !receipt.logs) {
153+
return;
154+
}
155+
156+
// Check if any log is an InteropBundleSent event from InteropCenter
157+
let foundInteropEvent = false;
158+
let interopEventLog: any = null;
159+
160+
for (const log of receipt.logs) {
161+
if (
162+
log.address.toLowerCase() === this.INTEROP_CENTER_ADDR.toLowerCase() &&
163+
log.topics[0] === this.INTEROP_BUNDLE_SENT_TOPIC
164+
) {
165+
foundInteropEvent = true;
166+
interopEventLog = log;
167+
break;
168+
}
169+
}
170+
171+
if (!foundInteropEvent) {
172+
return;
173+
}
174+
175+
console.log(`\n 🔗 Found L2→L2 cross-chain message on chain ${sourceChainId}`);
176+
console.log(` Source Tx Hash: ${txHash}`);
177+
178+
try {
179+
await this.relayCrossChainMessage(sourceChainId, txHash, interopEventLog, provider);
180+
this.processedTxHashes.add(txHash);
181+
console.log(` ✅ Cross-chain message relayed`);
182+
} catch (error: any) {
183+
console.error(` ❌ Failed to relay message:`, error.message);
184+
}
185+
}
186+
187+
private async relayCrossChainMessage(
188+
sourceChainId: number,
189+
sourceTxHash: string,
190+
interopEventLog: any,
191+
sourceProvider: JsonRpcProvider
192+
): Promise<void> {
193+
// Parse InteropBundleSent event to extract destination chain and calls
194+
const abiCoder = AbiCoder.defaultAbiCoder();
195+
196+
// InteropBundleSent event structure:
197+
// event InteropBundleSent(bytes32 l2l1MsgHash, bytes32 interopBundleHash, InteropBundle interopBundle)
198+
// InteropBundle: (bytes32 canonicalHash, bytes32 chainTreeRoot, bytes32 destination, uint256 nonce, InteropCallStarter[] calls)
199+
// InteropCallStarter: (address target, uint256 value, bytes data)
200+
201+
let targetChainId: number;
202+
let calls: Array<{ target: string; value: bigint; data: string }>;
203+
204+
try {
205+
// The third parameter (index 2) in the event is the InteropBundle struct
206+
// Data field contains the non-indexed parameters
207+
const decodedData = abiCoder.decode(
208+
[
209+
"bytes32", // l2l1MsgHash
210+
"bytes32", // interopBundleHash
211+
"tuple(bytes32,bytes32,bytes32,uint256,tuple(address,uint256,bytes)[])", // InteropBundle
212+
],
213+
interopEventLog.data
214+
);
215+
216+
const interopBundle = decodedData[2];
217+
const destinationBytes = interopBundle[2]; // bytes32 destination
218+
const rawCalls = interopBundle[4]; // InteropCallStarter[] calls (as arrays)
219+
220+
// Decode destination (uint256 encoded as bytes32)
221+
targetChainId = Number(abiCoder.decode(["uint256"], destinationBytes)[0]);
222+
223+
// Convert tuple arrays to objects
224+
calls = rawCalls.map((call: any) => ({
225+
target: call[0], // address
226+
value: call[1], // uint256
227+
data: call[2], // bytes
228+
}));
229+
230+
console.log(` From Chain: ${sourceChainId}`);
231+
console.log(` To Chain: ${targetChainId}`);
232+
console.log(` Calls: ${calls.length}`);
233+
234+
for (let i = 0; i < calls.length; i++) {
235+
console.log(` Call ${i + 1}: ${calls[i].target} with ${calls[i].data.length} bytes data`);
236+
}
237+
} catch (error) {
238+
console.error(` Failed to decode InteropBundleSent event:`, error);
239+
return;
240+
}
241+
242+
// Verify target chain exists
243+
const targetProvider = this.l2Providers.get(targetChainId);
244+
if (!targetProvider) {
245+
console.error(` Target chain ${targetChainId} not found`);
246+
return;
247+
}
248+
249+
console.log(` Executing ${calls.length} call(s) on target L2 chain...`);
250+
251+
// Direct execution on target L2 (bypassing L1 for Anvil testing)
252+
const targetWallet = new Wallet(this.l1Wallet.privateKey, targetProvider);
253+
254+
// Execute each call in the bundle
255+
for (let i = 0; i < calls.length; i++) {
256+
const call = calls[i];
257+
const tx = await targetWallet.sendTransaction({
258+
to: call.target,
259+
value: call.value,
260+
data: call.data,
261+
gasLimit: 1000000,
262+
});
263+
264+
console.log(` L2 Target Tx ${i + 1}: ${tx.hash}`);
265+
266+
const receipt = await tx.wait();
267+
console.log(` Confirmed in L2 block ${receipt?.blockNumber}`);
268+
}
269+
}
270+
271+
getStats(): { processedMessages: number; chainsMonitored: number } {
272+
return {
273+
processedMessages: this.processedTxHashes.size,
274+
chainsMonitored: this.l2Providers.size,
275+
};
276+
}
277+
}

0 commit comments

Comments
 (0)