Skip to content

Commit 5b02d04

Browse files
grasphopertbwebb22
andauthored
feat: add updatevkey task (#1199)
* add task to generate admin calldata entries to call HubPool with Signed-off-by: Ihor Farion <[email protected]> * update script output format Signed-off-by: Ihor Farion <[email protected]> * use correct source of truth for deployed addresses Signed-off-by: Ihor Farion <[email protected]> * Migrate vkeyUpdate task to Foundry Signed-off-by: Taylor Webb <[email protected]> * use deployedAddresses util Signed-off-by: Taylor Webb <[email protected]> --------- Signed-off-by: Ihor Farion <[email protected]> Signed-off-by: Taylor Webb <[email protected]> Co-authored-by: Taylor Webb <[email protected]>
1 parent 615f6ca commit 5b02d04

File tree

1 file changed

+284
-0
lines changed

1 file changed

+284
-0
lines changed

script/tasks/UpdateVkey.s.sol

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.0;
3+
4+
import { Script } from "forge-std/Script.sol";
5+
import { console } from "forge-std/console.sol";
6+
import { Constants } from "../utils/Constants.sol";
7+
import { DeployedAddresses } from "../utils/DeployedAddresses.sol";
8+
9+
/**
10+
* @title UpdateVkey
11+
* @notice Generates HubPool.multicall() calldata for updating SP1Helios verification keys across multiple chains.
12+
* The script queries each chain to verify Helios setup and outputs calldata for the HubPool owner (multisig) to execute.
13+
* RPC URLs are read from .env using the NODE_URL_<CHAIN_ID> convention (e.g., NODE_URL_56, NODE_URL_143).
14+
*
15+
*
16+
* Example:
17+
* forge script script/tasks/UpdateVkey.s.sol:UpdateVkey \
18+
* --sig "run(bytes32,string)" \
19+
* 0x1234567890abcdef0000000000000000000000000000000000000000000000 "56,143,999" \
20+
* -vvv
21+
*
22+
* Arguments:
23+
* - newVkey: The new Helios program vkey (bytes32)
24+
* - chains: Comma-separated list of chain IDs (e.g., "56,143,999")
25+
*/
26+
contract UpdateVkey is Script, Constants, DeployedAddresses {
27+
// Expected value for VKEY_UPDATER_ROLE to sanity check on-chain value
28+
bytes32 constant EXPECTED_VKEY_UPDATER_ROLE = 0x07ecc55c8d82c6f82ef86e34d1905e0f2873c085733fa96f8a6e0316b050d174;
29+
30+
// Struct to hold per-chain call data
31+
struct ChainCall {
32+
uint256 chainId;
33+
address spokePool;
34+
address helios;
35+
bytes hubPoolCalldata;
36+
}
37+
38+
function run(bytes32 newVkey, string calldata chainsStr) external {
39+
// Parse chain IDs from argument
40+
uint256[] memory chainIds = parseChainIds(chainsStr);
41+
42+
// Get HubPool address (from mainnet)
43+
address hubPoolAddress = getAddress(1, "HubPool");
44+
require(hubPoolAddress != address(0), "HubPool not found in deployed-addresses.json");
45+
46+
console.log("");
47+
console.log("============ Update Helios Vkey ============");
48+
console.log("New Vkey:", vm.toString(newVkey));
49+
console.log("HubPool:", hubPoolAddress);
50+
console.log("Chains:", chainsStr);
51+
console.log("--------------------------------------------");
52+
53+
// Process each chain
54+
ChainCall[] memory calls = new ChainCall[](chainIds.length);
55+
uint256 validCallCount = 0;
56+
uint256[] memory failedChains = new uint256[](chainIds.length);
57+
uint256 failedCount = 0;
58+
uint256[] memory upToDateChains = new uint256[](chainIds.length);
59+
uint256 upToDateCount = 0;
60+
61+
for (uint256 i = 0; i < chainIds.length; i++) {
62+
uint256 chainId = chainIds[i];
63+
64+
// Get SpokePool address for this chain
65+
address spokePoolAddress = getAddress(chainId, "SpokePool");
66+
if (spokePoolAddress == address(0)) {
67+
console.log("Skipping chain %d: no SpokePool in deployed-addresses.json", chainId);
68+
failedChains[failedCount++] = chainId;
69+
continue;
70+
}
71+
72+
// Get RPC URL for this chain (uses same NODE_URL_<chainId> convention as Hardhat)
73+
string memory rpcEnvVar = string.concat("NODE_URL_", vm.toString(chainId));
74+
string memory rpcUrl;
75+
try vm.envString(rpcEnvVar) returns (string memory url) {
76+
rpcUrl = url;
77+
} catch {
78+
console.log("Skipping chain %d: no %s environment variable", chainId, rpcEnvVar);
79+
failedChains[failedCount++] = chainId;
80+
continue;
81+
}
82+
83+
// Fork to this chain
84+
uint256 forkId = vm.createFork(rpcUrl);
85+
vm.selectFork(forkId);
86+
87+
// Get Helios address from SpokePool
88+
address heliosAddress;
89+
try IUniversalSpokePool(spokePoolAddress).helios() returns (address addr) {
90+
heliosAddress = addr;
91+
} catch {
92+
console.log("Skipping chain %d: SpokePool does not expose helios()", chainId);
93+
failedChains[failedCount++] = chainId;
94+
continue;
95+
}
96+
97+
if (heliosAddress == address(0)) {
98+
console.log("Skipping chain %d: SpokePool.helios() returned zero address", chainId);
99+
failedChains[failedCount++] = chainId;
100+
continue;
101+
}
102+
103+
// Verify VKEY_UPDATER_ROLE
104+
bytes32 vkeyRole;
105+
try ISP1Helios(heliosAddress).VKEY_UPDATER_ROLE() returns (bytes32 role) {
106+
vkeyRole = role;
107+
} catch {
108+
console.log("Skipping chain %d: failed to read VKEY_UPDATER_ROLE from Helios", chainId);
109+
failedChains[failedCount++] = chainId;
110+
continue;
111+
}
112+
113+
if (vkeyRole != EXPECTED_VKEY_UPDATER_ROLE) {
114+
console.log("Warning: VKEY_UPDATER_ROLE mismatch on chain %d", chainId);
115+
}
116+
117+
// Check if SpokePool has the role
118+
bool hasRole = ISP1Helios(heliosAddress).hasRole(vkeyRole, spokePoolAddress);
119+
if (!hasRole) {
120+
console.log("Skipping chain %d: SpokePool does not have VKEY_UPDATER_ROLE on Helios", chainId);
121+
failedChains[failedCount++] = chainId;
122+
continue;
123+
}
124+
125+
// Check current vkey
126+
bytes32 currentVkey = ISP1Helios(heliosAddress).heliosProgramVkey();
127+
if (currentVkey == newVkey) {
128+
console.log("Chain %d: Helios already has the requested vkey; skipping", chainId);
129+
upToDateChains[upToDateCount++] = chainId;
130+
continue;
131+
}
132+
133+
console.log("Chain %d: building updateHeliosProgramVkey() call", chainId);
134+
console.log(" SpokePool:", spokePoolAddress);
135+
console.log(" Helios:", heliosAddress);
136+
console.log(" Current Vkey:", vm.toString(currentVkey));
137+
138+
// Build nested calldata
139+
// 1. Helios.updateHeliosProgramVkey(newVkey)
140+
bytes memory heliosCalldata = abi.encodeWithSelector(
141+
ISP1Helios.updateHeliosProgramVkey.selector,
142+
newVkey
143+
);
144+
145+
// 2. SpokePool.executeExternalCall(message) where message = abi.encode(helios, heliosCalldata)
146+
bytes memory message = abi.encode(heliosAddress, heliosCalldata);
147+
bytes memory spokePoolCalldata = abi.encodeWithSelector(
148+
IUniversalSpokePool.executeExternalCall.selector,
149+
message
150+
);
151+
152+
// 3. HubPool.relaySpokePoolAdminFunction(chainId, spokePoolCalldata)
153+
bytes memory hubPoolCalldata = abi.encodeWithSelector(
154+
IHubPool.relaySpokePoolAdminFunction.selector,
155+
chainId,
156+
spokePoolCalldata
157+
);
158+
159+
calls[validCallCount++] = ChainCall({
160+
chainId: chainId,
161+
spokePool: spokePoolAddress,
162+
helios: heliosAddress,
163+
hubPoolCalldata: hubPoolCalldata
164+
});
165+
}
166+
167+
console.log("--------------------------------------------");
168+
169+
// Check for failures
170+
if (failedCount > 0) {
171+
console.log("");
172+
console.log("ERROR: Failed to prepare vkey update for chains:");
173+
for (uint256 i = 0; i < failedCount; i++) {
174+
console.log(" - %d", failedChains[i]);
175+
}
176+
revert("One or more chains failed during vkey update preparation");
177+
}
178+
179+
// Output results
180+
console.log("");
181+
console.log("Generated %d HubPool admin call(s)", validCallCount);
182+
183+
if (validCallCount == 0) {
184+
console.log("All requested chains already have the provided Helios vkey; no calldata needed.");
185+
return;
186+
}
187+
188+
console.log("");
189+
console.log("Per-chain summary:");
190+
for (uint256 i = 0; i < validCallCount; i++) {
191+
console.log(" - chainId=%d, spokePool=%s, helios=%s",
192+
calls[i].chainId,
193+
calls[i].spokePool,
194+
calls[i].helios
195+
);
196+
}
197+
198+
// Output multicall data
199+
console.log("");
200+
console.log("Data to use for HubPool.multicall on %s:", hubPoolAddress);
201+
console.log("Each entry is an encoded `relaySpokePoolAdminFunction` call.");
202+
console.log("");
203+
console.log("Included destination chains:");
204+
for (uint256 i = 0; i < validCallCount; i++) {
205+
console.log(" %d", calls[i].chainId);
206+
}
207+
console.log("");
208+
console.log("Calldata array (copy this for multicall):");
209+
console.log("[");
210+
for (uint256 i = 0; i < validCallCount; i++) {
211+
if (i < validCallCount - 1) {
212+
console.log(" %s,", vm.toString(calls[i].hubPoolCalldata));
213+
} else {
214+
console.log(" %s", vm.toString(calls[i].hubPoolCalldata));
215+
}
216+
}
217+
console.log("]");
218+
console.log("");
219+
console.log("============================================");
220+
}
221+
222+
// ============ Helper Functions ============
223+
224+
function parseChainIds(string memory chainsStr) internal pure returns (uint256[] memory) {
225+
// Count commas to determine array size
226+
bytes memory chainsBytes = bytes(chainsStr);
227+
uint256 count = 1;
228+
for (uint256 i = 0; i < chainsBytes.length; i++) {
229+
if (chainsBytes[i] == ",") {
230+
count++;
231+
}
232+
}
233+
234+
uint256[] memory chainIds = new uint256[](count);
235+
uint256 idx = 0;
236+
uint256 start = 0;
237+
238+
for (uint256 i = 0; i <= chainsBytes.length; i++) {
239+
if (i == chainsBytes.length || chainsBytes[i] == ",") {
240+
// Extract substring and parse
241+
bytes memory numBytes = new bytes(i - start);
242+
for (uint256 j = start; j < i; j++) {
243+
numBytes[j - start] = chainsBytes[j];
244+
}
245+
chainIds[idx++] = parseUint(string(numBytes));
246+
start = i + 1;
247+
}
248+
}
249+
250+
return chainIds;
251+
}
252+
253+
function parseUint(string memory s) internal pure returns (uint256) {
254+
bytes memory b = bytes(s);
255+
uint256 result = 0;
256+
for (uint256 i = 0; i < b.length; i++) {
257+
uint8 c = uint8(b[i]);
258+
// Skip whitespace
259+
if (c == 0x20) continue;
260+
require(c >= 48 && c <= 57, "Invalid character in number");
261+
result = result * 10 + (c - 48);
262+
}
263+
return result;
264+
}
265+
266+
}
267+
268+
// ============ Minimal Interfaces ============
269+
270+
interface IHubPool {
271+
function relaySpokePoolAdminFunction(uint256 chainId, bytes memory functionData) external;
272+
}
273+
274+
interface IUniversalSpokePool {
275+
function helios() external view returns (address);
276+
function executeExternalCall(bytes memory message) external;
277+
}
278+
279+
interface ISP1Helios {
280+
function VKEY_UPDATER_ROLE() external view returns (bytes32);
281+
function hasRole(bytes32 role, address account) external view returns (bool);
282+
function heliosProgramVkey() external view returns (bytes32);
283+
function updateHeliosProgramVkey(bytes32 newHeliosProgramVkey) external;
284+
}

0 commit comments

Comments
 (0)