Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/common/abi/SP1Helios.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,12 @@
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "heliosProgramVkey",
"outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }],
"stateMutability": "view",
"type": "function"
}
]
51 changes: 46 additions & 5 deletions src/finalizer/utils/helios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import { FinalizerPromise, CrossChainMessage } from "../types";
import axios from "axios";
import UNIVERSAL_SPOKE_ABI from "../../common/abi/Universal_SpokePool.json";
import { RelayedCallDataEvent, StoredCallDataEvent } from "../../interfaces/Universal";
import { ApiProofRequest, ProofOutputs, ProofStateResponse, SP1HeliosProofData } from "../../interfaces/ZkApi";
import {
ApiProofRequest,
ProofOutputs,
ProofStateResponse,
SP1HeliosProofData,
VkeyResponse,
} from "../../interfaces/ZkApi";
import { StorageSlotVerifiedEvent, HeadUpdateEvent } from "../../interfaces/Helios";
import { calculateProofId, decodeProofOutputs } from "../../utils/ZkApiUtils";
import { calculateHubPoolStoreStorageSlot, getHubPoolStoreContract } from "../../utils/UniversalUtils";
Expand Down Expand Up @@ -69,6 +75,15 @@ export async function heliosL1toL2Finalizer(
const l1ChainId = hubPoolClient.chainId;
const l2ChainId = l2SpokePoolClient.chainId;
const sp1HeliosL2 = await getSp1HeliosContractEVM(l2SpokePoolClient.spokePool, l2SpokePoolClient.spokePool.signer);

const apiBaseUrl = process.env.HELIOS_PROOF_API_URL;
if (!apiBaseUrl) {
throw new Error("[heliosL1toL2Finalizer] HELIOS_PROOF_API_URL environment variable not set.");
}

// Early vkey mismatch check to avoid requesting wrong proofs
await ensureVkeysMatch(apiBaseUrl, sp1HeliosL2);

const { sp1HeliosHead, sp1HeliosHeader } = await getSp1HeliosHeadData(sp1HeliosL2);

// --- Step 1: Identify all actions needed (pending L1 -> L2 messages to finalize & keep-alive) ---
Expand All @@ -95,6 +110,7 @@ export async function heliosL1toL2Finalizer(
// --- Step 2: Enrich actions with ZK proofs. Return messages that are ready to submit on-chain ---
const readyActions = await enrichHeliosActions(
logger,
apiBaseUrl,
actions,
l2SpokePoolClient,
l1SpokePoolClient,
Expand Down Expand Up @@ -267,17 +283,14 @@ async function shouldGenerateKeepAliveAction(
// returns helios messages ready for on-chain execution enriched with proof data
async function enrichHeliosActions(
logger: winston.Logger,
apiBaseUrl: string,
actions: HeliosAction[],
l2SpokePoolClient: EVMSpokePoolClient,
l1SpokePoolClient: EVMSpokePoolClient,
currentL2HeliosHeadNumber: number,
currentL2HeliosHeader: string
): Promise<HeliosAction[]> {
const l2ChainId = l2SpokePoolClient.chainId;
const apiBaseUrl = process.env.HELIOS_PROOF_API_URL;
if (!apiBaseUrl) {
throw new Error("[enrichHeliosActions] HELIOS_PROOF_API_URL environment variable not set.");
}
const hubPoolStoreAddress = getHubPoolStoreContract(
l1SpokePoolClient.chainId,
l1SpokePoolClient.spokePool.provider
Expand Down Expand Up @@ -762,3 +775,31 @@ function addUpdateOnlyTxn(
destinationChainId: l2ChainId,
});
}

/**
*
* @notice This function ensures that there's a match between `ZK API.vkey` _and_ `SP1Helios.vkey`. If these two don't
* match, the generated proof cannot be used to confirm messages on the destination chain.
*
* When upgrading the ZK Helios setup, ZK API will always have to be upgraded _after_ we send an upgrade message to all
* the V4 chains, because ZK API has to _first_ generate all the proofs for "upgrade message" to get confirmed on the
* destination chain. This means that ELF vkey might become stale in the ZK API (say some contract already upgraded but
* we didn't yet re-deploy the ZK API with the new ELF). This check eases the operational overhead to the upgrade by not
* allowing to request a proof with an incorrect(stale) ELF vkey.
*/
async function ensureVkeysMatch(apiBaseUrl: string, sp1Helios: ethers.Contract): Promise<void> {
const [apiResp, contractVkeyRaw] = await Promise.all([
axios.get<VkeyResponse>(`${apiBaseUrl}/v1/api/vkey`),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future, we probably want 1 - 2 retries on this to avoid sporadic issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added with axios-retry here daf7cea

sp1Helios.heliosProgramVkey(),
]);

const apiVkeyRaw = apiResp?.data?.vkey;
const apiVkey = apiVkeyRaw !== undefined ? apiVkeyRaw.toLowerCase() : undefined;
const contractVkey = contractVkeyRaw !== undefined ? contractVkeyRaw.toLowerCase() : undefined;

if (apiVkey === undefined || contractVkey === undefined || apiVkey !== contractVkey) {
throw new Error(
`SP1Helios vkey check failed: api=${apiVkey} contract=${contractVkey} address=${sp1Helios.address}`
);
}
}
4 changes: 4 additions & 0 deletions src/interfaces/ZkApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export interface ProofStateResponse {
error_message?: string; // Present only if status is "errored"
}

export interface VkeyResponse {
vkey: string;
}

// ABI for `public_values` returned from ZK API as part of `SP1HeliosProofData`
export const PROOF_OUTPUTS_ABI_TUPLE = `tuple(
bytes32 executionStateRoot,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/HeliosUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ export async function getSp1HeliosContractEVM(
evmSpokePool.provider
);
const heliosAddress = await universalSpokePoolContract.helios();
return new ethers.Contract(heliosAddress, SP1_HELIOS_ABI as any, signerOrProvider);
return new ethers.Contract(heliosAddress, SP1_HELIOS_ABI, signerOrProvider);
}