Skip to content

Instantly share code, notes, and snippets.

@zengzengzenghuy
Last active September 20, 2026 17:12
Show Gist options
  • Select an option

  • Save zengzengzenghuy/5e1e01a0ce829de1dee78ee992e8d49f to your computer and use it in GitHub Desktop.

Select an option

Save zengzengzenghuy/5e1e01a0ce829de1dee78ee992e8d49f to your computer and use it in GitHub Desktop.
// 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);
});
{
"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