From 6b6e806d2378c2464d8f13a3b2bb7ed34a757843 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 27 Nov 2025 15:42:19 -0800 Subject: [PATCH 1/3] add task to generate admin calldata entries to call HubPool with Signed-off-by: Ihor Farion --- hardhat.config.ts | 1 + tasks/updatevkey.ts | 247 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 tasks/updatevkey.ts diff --git a/hardhat.config.ts b/hardhat.config.ts index c16202c5b..f1dc6a81f 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -43,6 +43,7 @@ const tasks = [ "evmRelayMessageWithdrawal", "testChainAdapter", "upgradeSpokePool", + "updatevkey", ]; // eslint-disable-next-line node/no-missing-require diff --git a/tasks/updatevkey.ts b/tasks/updatevkey.ts new file mode 100644 index 000000000..2c94345a3 --- /dev/null +++ b/tasks/updatevkey.ts @@ -0,0 +1,247 @@ +import { task, types } from "hardhat/config"; +import type { HardhatRuntimeEnvironment } from "hardhat/types"; +import { ethers } from "ethers"; +import deploymentsJson from "../deployments/deployments.json"; + +// Minimal SP1Helios ABI subset needed by this task. +const SP1_HELIOS_ABI = [ + { + type: "function", + name: "VKEY_UPDATER_ROLE", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "hasRole", + inputs: [ + { name: "role", type: "bytes32", internalType: "bytes32" }, + { name: "account", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "heliosProgramVkey", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "updateHeliosProgramVkey", + inputs: [ + { + name: "newHeliosProgramVkey", + type: "bytes32", + internalType: "bytes32", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, +] as const; + +// Expected value for VKEY_UPDATER_ROLE to sanity check on-chain value. +const EXPECTED_VKEY_UPDATER_ROLE = "0x07ecc55c8d82c6f82ef86e34d1905e0f2873c085733fa96f8a6e0316b050d174"; + +type DeploymentsJson = Record>; + +const DEPLOYMENTS = deploymentsJson as DeploymentsJson; + +function parseChainList(rawChains: string): number[] { + const cleaned = (rawChains || "").replace(/\s/g, ""); + if (!cleaned) { + throw new Error("chains parameter is required and must be non-empty"); + } + + const chainIds = cleaned.split(",").map((item) => { + const n = Number(item); + if (!Number.isInteger(n) || n <= 0) { + throw new Error(`Invalid chain id: ${item}`); + } + return n; + }); + + return Array.from(new Set(chainIds)).sort((a, b) => a - b); +} + +function normalizeVkey(rawVkey: string): string { + let vkey = rawVkey.trim().toLowerCase(); + if (!vkey.startsWith("0x")) { + vkey = `0x${vkey}`; + } + if (vkey.length !== 66) { + throw new Error(`newvkey must be a 32-byte hex string (bytes32). Got length=${vkey.length}, value=${rawVkey}`); + } + if (!/^0x[0-9a-f]{64}$/.test(vkey)) { + throw new Error(`newvkey must be valid hex bytes32. Got: ${rawVkey}`); + } + return vkey; +} + +function getNetworkConfigForChainId(hre: HardhatRuntimeEnvironment, chainId: number) { + const entry = Object.entries(hre.config.networks).find( + ([, config]) => (config as any)?.chainId === chainId && (config as any)?.url + ); + if (!entry) { + throw new Error(`No Hardhat network with chainId=${chainId} and an RPC url configured`); + } + const [name, config] = entry; + return { name, url: (config as any).url as string }; +} + +task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program vkeys via Universal_SpokePool") + .addParam("newvkey", "New Helios program vkey (bytes32 hex string)", undefined, types.string) + .addParam( + "chains", + "Comma-delimited list of chain IDs whose Universal_SpokePool Helios vkey should be updated (e.g. 56,999,9745)", + undefined, + types.string + ) + .setAction(async (args, hre: HardhatRuntimeEnvironment) => { + const { ethers: hreEthers, deployments, artifacts } = hre; + const newVkey = normalizeVkey(args.newvkey); + const chainIds = parseChainList(args.chains); + + const hubPoolDeployment = await deployments.get("HubPool"); + const hubPoolInterface = new hreEthers.utils.Interface(hubPoolDeployment.abi); + + const universalSpokeArtifact = await artifacts.readArtifact("Universal_SpokePool"); + const universalSpokeInterface = new hreEthers.utils.Interface(universalSpokeArtifact.abi); + const heliosInterface = new hreEthers.utils.Interface(SP1_HELIOS_ABI); + + const calls: Array<{ + chainId: number; + target: string; + data: string; + spokePool: string; + helios: string; + }> = []; + + console.log(`Preparing Helios vkey update calldata for chains: ${chainIds.join(", ")}`); + console.log(`Using HubPool at ${hubPoolDeployment.address}\n`); + + for (const chainId of chainIds) { + const chainKey = chainId.toString(); + const chainDeployments = DEPLOYMENTS[chainKey]; + if (!chainDeployments || !chainDeployments.SpokePool?.address) { + console.warn(`Skipping chain ${chainId}: no SpokePool entry in deployments/deployments.json`); + continue; + } + + const spokePoolAddress = chainDeployments.SpokePool.address; + + let networkConfig; + try { + networkConfig = getNetworkConfigForChainId(hre, chainId); + } catch (err) { + console.warn(`Skipping chain ${chainId}: ${(err as Error).message}`); + continue; + } + + const provider = new ethers.providers.StaticJsonRpcProvider(networkConfig.url); + + const spokePool = new hreEthers.Contract(spokePoolAddress, universalSpokeInterface, provider); + + let heliosAddress: string; + try { + heliosAddress = await spokePool.helios(); + } catch (err) { + console.warn( + `Skipping chain ${chainId}: SpokePool at ${spokePoolAddress} does not expose helios() (is it a Universal_SpokePool?)` + ); + continue; + } + + if (!heliosAddress || heliosAddress === hreEthers.constants.AddressZero) { + console.warn(`Skipping chain ${chainId}: SpokePool.helios() returned zero address`); + continue; + } + + const heliosFromDeployments = chainDeployments.Helios?.address; + if (heliosFromDeployments && heliosFromDeployments.toLowerCase() !== heliosAddress.toLowerCase()) { + console.warn( + `Warning: Helios address mismatch on chain ${chainId}. deployments.json=${heliosFromDeployments}, on-chain=${heliosAddress}` + ); + } + + const helios = new hreEthers.Contract(heliosAddress, heliosInterface, provider); + + let vkeyRoleOnChain: string; + try { + vkeyRoleOnChain = await helios.VKEY_UPDATER_ROLE(); + } catch (err) { + console.warn(`Skipping chain ${chainId}: failed to read VKEY_UPDATER_ROLE from Helios at ${heliosAddress}`); + continue; + } + + if (vkeyRoleOnChain.toLowerCase() !== EXPECTED_VKEY_UPDATER_ROLE.toLowerCase()) { + console.warn( + `Warning: VKEY_UPDATER_ROLE on Helios (${vkeyRoleOnChain}) does not match expected constant on chain ${chainId}` + ); + } + + const spokeHasRole = await helios.hasRole(vkeyRoleOnChain, spokePoolAddress); + if (!spokeHasRole) { + console.warn( + `Skipping chain ${chainId}: SpokePool (${spokePoolAddress}) does not have VKEY_UPDATER_ROLE on Helios (${heliosAddress})` + ); + continue; + } + + const currentVkey: string = await helios.heliosProgramVkey(); + if (currentVkey.toLowerCase() === newVkey.toLowerCase()) { + console.log(`Chain ${chainId}: Helios already has the requested vkey; no update calldata generated`); + continue; + } + + console.log( + `Chain ${chainId}: building updateHeliosProgramVkey() call via SpokePool ${spokePoolAddress} -> Helios ${heliosAddress}` + ); + + const heliosUpdateCalldata = heliosInterface.encodeFunctionData("updateHeliosProgramVkey", [newVkey]); + + const message = hreEthers.utils.defaultAbiCoder.encode( + ["address", "bytes"], + [heliosAddress, heliosUpdateCalldata] + ); + + const spokePoolAdminCalldata = universalSpokeInterface.encodeFunctionData("executeExternalCall", [message]); + + const hubPoolCalldata = hubPoolInterface.encodeFunctionData("relaySpokePoolAdminFunction", [ + chainId, + spokePoolAdminCalldata, + ]); + + calls.push({ + chainId, + target: hubPoolDeployment.address, + data: hubPoolCalldata, + spokePool: spokePoolAddress, + helios: heliosAddress, + }); + } + + console.log(`\nGenerated ${calls.length} HubPool admin call(s).`); + if (calls.length === 0) { + console.log("No calldata generated. Check warnings above for chains that were skipped."); + return; + } + + console.log("\nPer-chain summary:"); + for (const call of calls) { + console.log(` - chainId=${call.chainId}, spokePool=${call.spokePool}, helios=${call.helios}`); + } + + console.log("\nCalldata payloads (each entry is a call to HubPool.relaySpokePoolAdminFunction):"); + console.log( + JSON.stringify( + calls.map(({ chainId, target, data }) => ({ chainId, target, data })), + null, + 2 + ) + ); + }); From 396c4173c9bd311f2aba8a50aef8e6e2d5f8f325 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Thu, 27 Nov 2025 15:56:05 -0800 Subject: [PATCH 2/3] update script output format Signed-off-by: Ihor Farion --- tasks/updatevkey.ts | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tasks/updatevkey.ts b/tasks/updatevkey.ts index 2c94345a3..3620a4db0 100644 --- a/tasks/updatevkey.ts +++ b/tasks/updatevkey.ts @@ -102,7 +102,8 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program types.string ) .setAction(async (args, hre: HardhatRuntimeEnvironment) => { - const { ethers: hreEthers, deployments, artifacts } = hre; + const hreAny = hre as any; + const { ethers: hreEthers, deployments, artifacts } = hreAny; const newVkey = normalizeVkey(args.newvkey); const chainIds = parseChainList(args.chains); @@ -120,6 +121,8 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program spokePool: string; helios: string; }> = []; + const failedChains: number[] = []; + const chainsAlreadyUpToDate: number[] = []; console.log(`Preparing Helios vkey update calldata for chains: ${chainIds.join(", ")}`); console.log(`Using HubPool at ${hubPoolDeployment.address}\n`); @@ -129,6 +132,7 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program const chainDeployments = DEPLOYMENTS[chainKey]; if (!chainDeployments || !chainDeployments.SpokePool?.address) { console.warn(`Skipping chain ${chainId}: no SpokePool entry in deployments/deployments.json`); + failedChains.push(chainId); continue; } @@ -139,6 +143,7 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program networkConfig = getNetworkConfigForChainId(hre, chainId); } catch (err) { console.warn(`Skipping chain ${chainId}: ${(err as Error).message}`); + failedChains.push(chainId); continue; } @@ -153,11 +158,13 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program console.warn( `Skipping chain ${chainId}: SpokePool at ${spokePoolAddress} does not expose helios() (is it a Universal_SpokePool?)` ); + failedChains.push(chainId); continue; } if (!heliosAddress || heliosAddress === hreEthers.constants.AddressZero) { console.warn(`Skipping chain ${chainId}: SpokePool.helios() returned zero address`); + failedChains.push(chainId); continue; } @@ -175,6 +182,7 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program vkeyRoleOnChain = await helios.VKEY_UPDATER_ROLE(); } catch (err) { console.warn(`Skipping chain ${chainId}: failed to read VKEY_UPDATER_ROLE from Helios at ${heliosAddress}`); + failedChains.push(chainId); continue; } @@ -189,12 +197,14 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program console.warn( `Skipping chain ${chainId}: SpokePool (${spokePoolAddress}) does not have VKEY_UPDATER_ROLE on Helios (${heliosAddress})` ); + failedChains.push(chainId); continue; } const currentVkey: string = await helios.heliosProgramVkey(); if (currentVkey.toLowerCase() === newVkey.toLowerCase()) { console.log(`Chain ${chainId}: Helios already has the requested vkey; no update calldata generated`); + chainsAlreadyUpToDate.push(chainId); continue; } @@ -225,9 +235,16 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program }); } + if (failedChains.length > 0) { + console.error( + `\nERROR: Failed to prepare vkey update calldata for the following chains: ${failedChains.join(", ")}` + ); + throw new Error("One or more chains failed during vkey update preparation"); + } + console.log(`\nGenerated ${calls.length} HubPool admin call(s).`); if (calls.length === 0) { - console.log("No calldata generated. Check warnings above for chains that were skipped."); + console.log("All requested chains already have the provided Helios vkey; no HubPool calldata needed."); return; } @@ -236,12 +253,15 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program console.log(` - chainId=${call.chainId}, spokePool=${call.spokePool}, helios=${call.helios}`); } - console.log("\nCalldata payloads (each entry is a call to HubPool.relaySpokePoolAdminFunction):"); + const targetChains = calls.map(({ chainId }) => chainId); + const multicallData = calls.map(({ data }) => data); + console.log( - JSON.stringify( - calls.map(({ chainId, target, data }) => ({ chainId, target, data })), - null, - 2 - ) + `\nData to use for HubPool.multicall on ${ + hubPoolDeployment.address + }. Each entry is an encoded \`relaySpokePoolAdminFunction\` call. Included destination chains: [${targetChains.join( + ", " + )}]` ); + console.log(`\n[${multicallData.join(",")}]`); }); From f713555f46547b771d39bc29b157cf628f35be3c Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Fri, 28 Nov 2025 14:59:28 -0800 Subject: [PATCH 3/3] use correct source of truth for deployed addresses Signed-off-by: Ihor Farion --- tasks/updatevkey.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tasks/updatevkey.ts b/tasks/updatevkey.ts index 3620a4db0..a5f184943 100644 --- a/tasks/updatevkey.ts +++ b/tasks/updatevkey.ts @@ -1,7 +1,7 @@ import { task, types } from "hardhat/config"; import type { HardhatRuntimeEnvironment } from "hardhat/types"; import { ethers } from "ethers"; -import deploymentsJson from "../deployments/deployments.json"; +import { getDeployedAddress } from "../src/DeploymentUtils"; // Minimal SP1Helios ABI subset needed by this task. const SP1_HELIOS_ABI = [ @@ -47,10 +47,6 @@ const SP1_HELIOS_ABI = [ // Expected value for VKEY_UPDATER_ROLE to sanity check on-chain value. const EXPECTED_VKEY_UPDATER_ROLE = "0x07ecc55c8d82c6f82ef86e34d1905e0f2873c085733fa96f8a6e0316b050d174"; -type DeploymentsJson = Record>; - -const DEPLOYMENTS = deploymentsJson as DeploymentsJson; - function parseChainList(rawChains: string): number[] { const cleaned = (rawChains || "").replace(/\s/g, ""); if (!cleaned) { @@ -128,16 +124,13 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program console.log(`Using HubPool at ${hubPoolDeployment.address}\n`); for (const chainId of chainIds) { - const chainKey = chainId.toString(); - const chainDeployments = DEPLOYMENTS[chainKey]; - if (!chainDeployments || !chainDeployments.SpokePool?.address) { - console.warn(`Skipping chain ${chainId}: no SpokePool entry in deployments/deployments.json`); + const spokePoolAddress = getDeployedAddress("SpokePool", chainId, false); + if (!spokePoolAddress) { + console.warn(`Skipping chain ${chainId}: no SpokePool entry in broadcast/deployed-addresses.json`); failedChains.push(chainId); continue; } - const spokePoolAddress = chainDeployments.SpokePool.address; - let networkConfig; try { networkConfig = getNetworkConfigForChainId(hre, chainId); @@ -168,10 +161,10 @@ task("updatevkey", "Generate HubPool admin calldata to update SP1Helios program continue; } - const heliosFromDeployments = chainDeployments.Helios?.address; + const heliosFromDeployments = getDeployedAddress("Helios", chainId, false); if (heliosFromDeployments && heliosFromDeployments.toLowerCase() !== heliosAddress.toLowerCase()) { console.warn( - `Warning: Helios address mismatch on chain ${chainId}. deployments.json=${heliosFromDeployments}, on-chain=${heliosAddress}` + `Warning: Helios address mismatch on chain ${chainId}. broadcast/deployed-addresses.json=${heliosFromDeployments}, on-chain=${heliosAddress}` ); }