Skip to content

feat: add finalizer support for messages from HubPool to UniversalSpoke #2203

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: master
Choose a base branch
from

Conversation

grasphoper
Copy link

@grasphoper grasphoper commented Apr 30, 2025

This PR adds an "adapter" for a finalizer to be able to finalize messages from HubPool to "Universal chains" using ZK proofs: helios.ts. This finalization only works in L1 -> L2 direction.

Like for other adapters, there's a main function: heliosL1toL2Finalizer , which does roughly the following:

Step 1: identify messages that require finalizing

  1. get events on Source Chain HubPoolStore that were recently stored (up to lookback)
  2. get events on Dest. Chain SP1Helios that signify recently verified (via ZK proofs) Source Chain storage slots
  3. get events on Dest. Chain Universal_SpokePool that signify execution of messages that were sent.

Using this information, we can identify which messages have been executed (3.) -- skip those. For other messages, if we received an event on (2.) but not (3.), the message only needs execution, and not proof verification -- so we do only that.

For all the other messages we do the full flow: ZK proof generation + Sp1Helios.update + Universal_SpokePool.executeMessage

Step 2: request ZK proofs for the messages that have yet to be verified

  1. get_proof from ZK_API
    1. If not found
      • Call request_proof()
      • Exit (will retry next run)
    2. If found, check proofState.status
      1. pending
        • Exit
      2. success
        • Take the proof
        • Save it for Step 3
      3. errored
        • Produce a warning
        • Call request_proof()
        • Exit

Step 3: generate transactions that need to be executed.

For all messages that only require executeMessage -> generate a transaction.
For all messages that require both proof and executeMessage, only if there's proof data available already, generate 2 transactions.

grasphoper and others added 21 commits April 28, 2025 20:18
Signed-off-by: Ihor Farion <[email protected]>
…lticall2Call | AugmentedTransaction) to enable returning individual txns from finalizer adapters

Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
Signed-off-by: Ihor Farion <[email protected]>
@grasphoper grasphoper marked this pull request as ready for review May 1, 2025 22:55
proofId,
errorMessage: proofState.error_message,
});
try {
Copy link
Member

Choose a reason for hiding this comment

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

Why re-try here? Should we just log an error and then wait for the next finalizer run to re-request?

Copy link
Author

Choose a reason for hiding this comment

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

Here, ZK API tried to generate a proof but failed. We re-request it here and log an error. In practice, we should never have proofs for which generation errors. The next finalizer run will not re-request it unless we explicitly do so here

// todo: (by not making them to wait for finality longer than needed) if our blockNumber that we need a proved slot for is older than this head.
const currentHead = headBn.toNumber();
const currentHeader = await sp1HeliosContract.headers(headBn);
if (!currentHeader || currentHeader === ethers.constants.HashZero) {
Copy link
Member

Choose a reason for hiding this comment

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

i'm not sure calling a contract can ever return undefined-- it should either error or return you a type that you expect--in this case a BN

Copy link
Author

Choose a reason for hiding this comment

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

Not sure how to check this. IMO this is a nice check that prevents us from sending a request with dst_chain_contract_from_header: undefined down below

Copy link
Member

Choose a reason for hiding this comment

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

ok i think its unneccessary but your call

});
} else {
// todo? Might want to log an error here
logger.warn({
Copy link
Member

Choose a reason for hiding this comment

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

Would this ever happen? Don't we have a guarantee about the API's response shape, and if state == "success" then update_calldata must exist?

I'm wary any time I see a 4x nested if {} block that we can simplify somehow

Copy link
Author

Choose a reason for hiding this comment

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

It can happen if API returns an incorrectly formatted response. The thinking is, "it must exist" yeah. But networking stuff is weird

Copy link
Member

Choose a reason for hiding this comment

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

i think we should refactor and check the response shape of proofState when we first get it and throw if its ill formatted. And then work off the assumption that its correct on all following lines

I think we can reduce this extra if-else statement which makes the code less readable

* Retrieves an ethers.Contract instance for the SP1Helios contract on the specified chain.
* @throws {Error} If the SP1Helios contract address or ABI is not found for the given chainId in CONTRACT_ADDRESSES.
*/
function getSp1HeliosContract(chainId: number, signerOrProvider: Signer | Provider): ethers.Contract {
Copy link
Member

Choose a reason for hiding this comment

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

nit: should we move getSp1HeliosContract and getHubPoolStoreContract into UniversalUtils?

Copy link
Author

Choose a reason for hiding this comment

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

Well. It's nuanced. These fns are opinionated in the sense that they throw if the contract is not there. This throwing opinion is really of helios.ts, not of utils package. But I agree that we should probably always throw on missing addr / abi so we can move?

Copy link
Author

Choose a reason for hiding this comment

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

Also, I don't feel like these fns are super-useful to reuse. I don't mind either way

Copy link
Member

@nicholaspai nicholaspai May 2, 2025

Choose a reason for hiding this comment

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

i don't think the opinionated throwing is a bad thing to have in a utility function named as such. We should move it to a utility folder if only because this file is long and maintaining these utility functions willl be more difficult over time.

@grasphoper grasphoper requested a review from nicholaspai May 2, 2025 21:31
@grasphoper
Copy link
Author

grasphoper commented May 2, 2025

@nicholaspai I addressed most of your comments -- resolved the addressed ones. What else should I 100% address before we can merge?

P.S. the failing test is just flakiness I think

chainId: l2ChainId,
method: "executeMessage",
args: executeArgs,
unpermissioned: true,
Copy link
Member

Choose a reason for hiding this comment

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

This should be false right? Only the EOA with the updater role can call this right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants