Last active
September 20, 2026 17:12
-
-
Save zengzengzenghuy/5e1e01a0ce829de1dee78ee992e8d49f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // executeAffirmation.js | |
| // | |
| // Relays Ethereum -> Gnosis Chain Omnibridge messages by calling | |
| // executeAffirmation() on the AMB home bridge as a bridge validator. | |
| // | |
| // Direction note: this is the *opposite* of callExecuteSignature.js / | |
| // claimAll.js. Those handle GC -> ETH, where validators sign on GC and anyone | |
| // submits the collected signatures on ETH. Here the user request originates on | |
| // ETH (UserRequestForAffirmation on the foreign AMB) and each validator | |
| // independently confirms it on GC with executeAffirmation(bytes). There is no | |
| // signature blob to assemble or repair — the only input is the raw | |
| // `encodedData` emitted on Ethereum, which is baked into the table below. | |
| // Only a Gnosis Chain RPC is needed. | |
| // | |
| // Usage: | |
| // node executeAffirmation.js <privateKey> [options] | |
| // | |
| // Options: | |
| // --gc-rpc <url> Gnosis Chain RPC (default: GC_RPC_URL env or the | |
| // Tenderly gateway below). Point this at a local | |
| // anvil fork to rehearse the whole run against forked | |
| // state before doing it for real. | |
| // | |
| // Each transaction is sent strictly one at a time: the script waits for the | |
| // previous executeAffirmation receipt (and asserts status === "success") | |
| // before simulating the next one. | |
| import { | |
| createPublicClient, | |
| createWalletClient, | |
| encodePacked, | |
| http, | |
| keccak256, | |
| } from "viem"; | |
| import { privateKeyToAccount } from "viem/accounts"; | |
| import { gnosis } from "viem/chains"; | |
| const DEFAULT_GC_RPC = "https://rpc.gnosischain.com"; | |
| // The AMB home bridge on Gnosis Chain, which consumes the affirmations. | |
| const HOME_AMB = "0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59"; | |
| // --- message table ----------------------------------------------------------- | |
| // | |
| // Ethereum tx hash -> the `encodedData` its UserRequestForAffirmation log | |
| // carried. Taken from the foreign AMB (0x4C36d2919e407f0Cc2Ee3c993ccF8ac26d9CE64e) | |
| // logs of the transactions listed in executeAffirmation20260920.md, messageIds | |
| // 0x...3017d through 0x...30189. The messageId is the first 32-byte word of | |
| // each payload. | |
| // | |
| // Order matters: it is the order the affirmations are submitted in. | |
| const MESSAGES = { | |
| "0x728332727c7ff48a1e32b50a4d06de421adb6c1c734e1b9a01c08b50a840ad8c": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003017d88ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb000000000000000000000000def1ca1fb7fbcdc777520aa7f396b4e015f497ab000000000000000000000000f44217a8b6b3f258bffead635c226528aa516aea00000000000000000000000000000000000000000000073310f0b2db24710000", | |
| "0x80510a71e94d2c908908bf21a49a3bfddd3d5c8eaf9a2e9fbe9bdb2eae4bbe87": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003017e88ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000007d9d0da1ad30d4a707a473c9e91ecc100ff065ef00000000000000000000000000000000000000000000000000000000b2d05e00", | |
| "0x38e0ea21adef67e8904af0a8caeed416813d83ab2965a04e82d4c87186f3d1aa": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003017f88ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000007d9d0da1ad30d4a707a473c9e91ecc100ff065ef0000000000000000000000000000000000000000000000000000000077359400", | |
| "0x86e972828b8b5a7bcb85f07b4994a241a8de10eddb15aaa90266e58c5000dd70": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018088ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb0000000000000000000000000001a500a6b18995b03f44bb040a5ffc28e45cb000000000000000000000000012ff6728e395a5d1194cc864e553065786be6e170000000000000000000000000000000000000000000000022b1c8c1227a00000", | |
| "0xcb7a4dcf5f8ddbc9f6f4623478e0fcb01aee4929ea9c6b98ecec5f37e40cc336": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018188ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb0000000000000000000000000001a500a6b18995b03f44bb040a5ffc28e45cb000000000000000000000000012ff6728e395a5d1194cc864e553065786be6e170000000000000000000000000000000000000000000000022b1c8c1227a00000", | |
| "0x9b9d333f1abf91de20af4633b8a9174842fafd819b5ecd5b9e86eda23e21819b": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018288ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb00000000000000000000000064d0f55cd8c7133a9d7102b13987235f486f222400000000000000000000000049719d256a5ea16bfa579ea16e95bea9fa41a452000000000000000000000000000000000000000000000013c74d7e8476bed6af", | |
| "0x6a7acd2697a0b7a5ef4aeb8d76b3db8209ca277878c9c56a238e135a3a5cbc7f": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018388ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb000000000000000000000000514910771af9ca656af840dff83e8264ecf986ca00000000000000000000000049719d256a5ea16bfa579ea16e95bea9fa41a4520000000000000000000000000000000000000000000000008262afb413335b0c", | |
| "0xb775836bed242e5bd3d349f1a403a3d26d214a6d0b661604ad977f8c5c83cb83": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018488ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb0000000000000000000000006810e776880c02933d47db1b9fc05908e5386b96000000000000000000000000298542744149d89ee87df4018b8fe3e53cd3ce540000000000000000000000000000000000000000000000003b47d31e3dcbbad0", | |
| "0xe9c35331880c285c0e8aec1ffd8321b2151414a5264a818226240e2ce3bbbd5d": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018588ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb000000000000000000000000aa7a9ca87d3694b5755f213b5d04094b8d0f0a6f000000000000000000000000019ccc74b07b5f264206dde9688e350379b67de30000000000000000000000000000000000000000000001da56a4b0835bf80000", | |
| "0xafb36d33a00619087bc155bb474717bfc27c2c88c6a4b3ff5ad12a45f4b0f1b4": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018688ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb000000000000000000000000c770eefad204b5180df6a14ee197d99d808ee52d00000000000000000000000049719d256a5ea16bfa579ea16e95bea9fa41a4520000000000000000000000000000000000000000000004bc5d55c5b82cdaba82", | |
| "0x2a82dcc41f704cd2c2300bf6f931ea7809585d6347d0e3050df07f06da3ef623": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018788ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb0000000000000000000000000001a500a6b18995b03f44bb040a5ffc28e45cb00000000000000000000000008678f58ac6c4748b5289d0db70e627eef395dead00000000000000000000000000000000000000000000032e89314419d29ea95c", | |
| "0x1dda08447c3153eca81921653a3510c83f355a3354558bc3986acd3d3156a4c2": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018888ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000fdd0248fbf79ad7ed75c73e8150b8f2f0656db780000000000000000000000000000000000000000000000000000000008293bc0", | |
| "0x111e1f8068922482cd78dafd11c727a0d0502e5fe46399cce45c736035033319": | |
| "0x000500004ac82b41bd819dd871590b510316f2385cb196fb000000000003018988ad09518695c6c3712ac10a214be5109a655671f6a78083ca3e2a662d6dd1703c939c8ace2e268d001e84800101000164125e4cfb000000000000000000000000f5581dfefd8fb0e4aec526be659cfab1f8c781da0000000000000000000000008678f58ac6c4748b5289d0db70e627eef395dead00000000000000000000000000000000000000000000030ad96a108b00a842dc", | |
| }; | |
| // --- ABI fragments and constants --------------------------------------------- | |
| const homeAmbAbi = [ | |
| { | |
| name: "executeAffirmation", | |
| type: "function", | |
| stateMutability: "nonpayable", | |
| inputs: [{ name: "_message", type: "bytes" }], | |
| outputs: [], | |
| }, | |
| { | |
| name: "numAffirmationsSigned", | |
| type: "function", | |
| stateMutability: "view", | |
| inputs: [{ type: "bytes32" }], | |
| outputs: [{ type: "uint256" }], | |
| }, | |
| { | |
| name: "affirmationsSigned", | |
| type: "function", | |
| stateMutability: "view", | |
| inputs: [{ type: "bytes32" }], | |
| outputs: [{ type: "bool" }], | |
| }, | |
| { | |
| name: "messageCallStatus", | |
| type: "function", | |
| stateMutability: "view", | |
| inputs: [{ type: "bytes32" }], | |
| outputs: [{ type: "bool" }], | |
| }, | |
| ]; | |
| // BasicHomeAMB packs a "processed" flag into the top bit of the affirmation | |
| // counter once the message has been executed, so the raw counter has to be | |
| // masked before it means anything. | |
| const PROCESSED_BIT = 1n << 255n; | |
| const isAlreadyProcessed = (n) => (n & PROCESSED_BIT) === PROCESSED_BIT; | |
| const affirmationCount = (n) => n & ~PROCESSED_BIT; | |
| // --- CLI --------------------------------------------------------------------- | |
| function parseArgs(argv) { | |
| const positional = []; | |
| const opts = { | |
| gcRpc: process.env.GC_RPC_URL || DEFAULT_GC_RPC, | |
| }; | |
| for (let i = 0; i < argv.length; i++) { | |
| const a = argv[i]; | |
| switch (a) { | |
| case "--gc-rpc": | |
| opts.gcRpc = argv[++i]; | |
| break; | |
| default: | |
| if (a.startsWith("--")) throw new Error(`Unknown option: ${a}`); | |
| positional.push(a); | |
| } | |
| } | |
| opts.privateKey = positional[0]; | |
| return opts; | |
| } | |
| // --- message handling -------------------------------------------------------- | |
| // Everything the home bridge knows about this message right now. | |
| async function readHomeState(gcClient, messageId, encodedData, signer) { | |
| const hashMsg = keccak256(encodedData); | |
| const hashSender = keccak256( | |
| encodePacked(["address", "bytes32"], [signer, hashMsg]), | |
| ); | |
| const [executed, rawSigned, signedByUs] = await Promise.all([ | |
| gcClient.readContract({ | |
| address: HOME_AMB, | |
| abi: homeAmbAbi, | |
| functionName: "messageCallStatus", | |
| args: [messageId], | |
| }), | |
| gcClient.readContract({ | |
| address: HOME_AMB, | |
| abi: homeAmbAbi, | |
| functionName: "numAffirmationsSigned", | |
| args: [hashMsg], | |
| }), | |
| gcClient.readContract({ | |
| address: HOME_AMB, | |
| abi: homeAmbAbi, | |
| functionName: "affirmationsSigned", | |
| args: [hashSender], | |
| }), | |
| ]); | |
| return { | |
| executed, | |
| processed: isAlreadyProcessed(rawSigned), | |
| signatures: affirmationCount(rawSigned), | |
| signedByUs, | |
| }; | |
| } | |
| // --- main -------------------------------------------------------------------- | |
| async function main() { | |
| const opts = parseArgs(process.argv.slice(2)); | |
| if (!opts.privateKey) { | |
| console.error( | |
| "Usage: node executeAffirmation.js <privateKey> [--gc-rpc url]", | |
| ); | |
| process.exit(1); | |
| } | |
| const pk = opts.privateKey.startsWith("0x") | |
| ? opts.privateKey | |
| : `0x${opts.privateKey}`; | |
| const account = privateKeyToAccount(pk); | |
| const signer = account.address; | |
| const gcClient = createPublicClient({ | |
| chain: gnosis, | |
| transport: http(opts.gcRpc), | |
| }); | |
| const walletClient = createWalletClient({ | |
| account, | |
| chain: gnosis, | |
| transport: http(opts.gcRpc), | |
| }); | |
| const entries = Object.entries(MESSAGES); | |
| console.log(`Transactions : ${entries.length}`); | |
| console.log(`Gnosis Chain RPC: ${opts.gcRpc}`); | |
| console.log(`Signer : ${signer}`); | |
| const balance = await gcClient.getBalance({ address: signer }); | |
| console.log(`xDAI balance : ${balance}\n`); | |
| if (balance === 0n) { | |
| console.error("Signer has no xDAI to pay for gas."); | |
| process.exit(1); | |
| } | |
| let sent = 0, | |
| skipped = 0, | |
| failed = 0; | |
| for (const [i, [txHash, encodedData]] of entries.entries()) { | |
| const messageId = `0x${encodedData.slice(2, 66)}`; | |
| console.log(`[${i + 1}/${entries.length}] ${txHash}`); | |
| console.log(` messageId ${messageId}`); | |
| try { | |
| const state = await readHomeState( | |
| gcClient, | |
| messageId, | |
| encodedData, | |
| signer, | |
| ); | |
| console.log( | |
| ` affirmations ${state.signatures}` + | |
| `${state.processed ? " (processed)" : ""}${state.signedByUs ? " (already signed by us)" : ""}`, | |
| ); | |
| if (state.executed || state.processed) { | |
| console.log(` -> skip: already relayed on Gnosis Chain\n`); | |
| skipped++; | |
| continue; | |
| } | |
| if (state.signedByUs) { | |
| console.log( | |
| ` -> skip: this validator already affirmed this message\n`, | |
| ); | |
| skipped++; | |
| continue; | |
| } | |
| // Simulate first, always — both to catch reverts before spending gas and | |
| // to produce the request object that is actually broadcast. | |
| const { request } = await gcClient.simulateContract({ | |
| address: HOME_AMB, | |
| abi: homeAmbAbi, | |
| functionName: "executeAffirmation", | |
| args: [encodedData], | |
| account: signer, | |
| }); | |
| // The final affirmation triggers the mediator call, so this is much the | |
| // larger estimate; a 25% head-room keeps it from running out. | |
| const gas = await gcClient.estimateContractGas({ | |
| address: HOME_AMB, | |
| abi: homeAmbAbi, | |
| functionName: "executeAffirmation", | |
| args: [encodedData], | |
| account: signer, | |
| }); | |
| const gasLimit = (gas * 125n) / 100n; | |
| const gcTxHash = await walletClient.writeContract({ | |
| ...request, | |
| gas: gasLimit, | |
| }); | |
| console.log( | |
| ` -> sent ${gcTxHash} (gas limit ${gasLimit}), waiting for confirmation...`, | |
| ); | |
| // Block here until this one is mined. Nothing else is submitted in the | |
| // meantime, so the affirmations land in table order and the signer's | |
| // nonce can never collide. | |
| const receipt = await gcClient.waitForTransactionReceipt({ | |
| hash: gcTxHash, | |
| }); | |
| if (receipt.status !== "success") { | |
| console.log(` -> REVERTED in block ${receipt.blockNumber}\n`); | |
| failed++; | |
| continue; | |
| } | |
| const after = await readHomeState( | |
| gcClient, | |
| messageId, | |
| encodedData, | |
| signer, | |
| ); | |
| console.log( | |
| ` -> confirmed in block ${receipt.blockNumber} (gas used ${receipt.gasUsed})`, | |
| ); | |
| console.log( | |
| ` -> ${ | |
| after.executed | |
| ? "message relayed on Gnosis Chain" | |
| : `${after.signatures} affirmations` | |
| }\n`, | |
| ); | |
| sent++; | |
| } catch (err) { | |
| console.log(` -> ERROR: ${err.shortMessage || err.message}\n`); | |
| failed++; | |
| } | |
| } | |
| console.log("--------------------------------------------------"); | |
| console.log(`Sent: ${sent} Skipped: ${skipped} Failed: ${failed}`); | |
| if (failed > 0) process.exitCode = 1; | |
| } | |
| main().catch((err) => { | |
| console.error("Fatal:", err.shortMessage || err.message); | |
| process.exit(1); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "execute-affirmation", | |
| "version": "1.0.0", | |
| "private": true, | |
| "description": "Relay Ethereum -> Gnosis Chain Omnibridge messages by calling executeAffirmation on the AMB home bridge as a validator.", | |
| "type": "module", | |
| "engines": { | |
| "node": ">=18" | |
| }, | |
| "scripts": { | |
| "claim": "node executeAffirmation.js" | |
| }, | |
| "dependencies": { | |
| "viem": "^2.23.2" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment