This repository was archived by the owner on Apr 6, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 335
Tutorial for relaying arbitrary log messages (done with attestations) #1613
Merged
krofax
merged 30 commits into
ethereum-optimism:main
from
qbzzt:250514-attestation-relay
Jun 13, 2025
Merged
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
aef7514
WIP
qbzzt 34376e2
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt 66e2b71
WIP
qbzzt 1e89523
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt cc45607
First Draft
qbzzt 417af89
Lint
qbzzt 5060cd2
Merge branch 'main' into 250514-attestation-relay
qbzzt 492d679
Works for supersim, not devnet
qbzzt e67e13a
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt 1b94904
Added expiration disclaimer
qbzzt a9252d8
typo
qbzzt 6b203fd
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt b454371
Update pages/interop/tutorials/verify-messages.mdx
krofax 55bbb02
Update pages/interop/tutorials/verify-messages.mdx
krofax b797f53
Update pages/interop/tutorials/verify-messages.mdx
krofax 5a8f6ff
Merge branch 'main' into 250514-attestation-relay
qbzzt 6a01e26
coderabbit
qbzzt f5c99e3
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt ce905fb
Merge branch 'main' into 250514-attestation-relay
qbzzt f7c1e87
Merge branch 'main' into 250514-attestation-relay
qbzzt 635928f
code rabbit
qbzzt 82bb23b
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt 51ca768
Merge remote-tracking branch 'refs/remotes/origin/250514-attestation-…
qbzzt 268e6b9
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt a9b3d21
coderabbit
qbzzt 9bf1adf
Update verify-messages.mdx
qbzzt adda27f
WIP
qbzzt 56d2312
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt 66f0917
Apply suggestions from @krofax
qbzzt d7460b0
Fixing indentation
qbzzt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| // SPDX-License-Identifier: UNLICENSED | ||
| pragma solidity ^0.8.13; | ||
|
|
||
| import {Attestation} from "@ethereum-attestation-service/eas-contracts/contracts/Common.sol"; | ||
|
|
||
| // Code from https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/interfaces/L2/ICrossL2Inbox.sol, | ||
| // which is not in the npm package yet so we copy it. | ||
| struct Identifier { | ||
| address origin; | ||
| uint256 blockNumber; | ||
| uint256 logIndex; | ||
| uint256 timestamp; | ||
| uint256 chainId; | ||
| } | ||
|
|
||
| interface ICrossL2Inbox { | ||
| function validateMessage(Identifier calldata _id, bytes32 _msgHash) external; | ||
| } | ||
|
|
||
| contract Verifier { | ||
|
|
||
| bytes32 private constant SCHEMA_UID = 0x234dee4d3e6a625b4121e2042d6267058755e53a2ecc55555da51a1e6f06cc58; | ||
| uint256 private constant EVENT_SIG = 0x8bf46bf4cfd674fa735a3d63ec1c9ad4153f033c290341f3a588b75685141b35; | ||
| address private constant EAS_CONTRACT = 0x4200000000000000000000000000000000000021; | ||
| address private constant CROSS_L2_INBOX = 0x4200000000000000000000000000000000000022; | ||
|
|
||
| /// @dev Calculates a UID for a given attestation. | ||
| /// @param attestation The input attestation. | ||
| /// @param bump A bump value to use in case of a UID conflict. | ||
| /// @return Attestation UID. | ||
| function _getUID(Attestation memory attestation, uint32 bump) private pure returns (bytes32) { | ||
| return | ||
| keccak256( | ||
| abi.encodePacked( | ||
| attestation.schema, | ||
| attestation.recipient, | ||
| attestation.attester, | ||
| attestation.time, | ||
| attestation.expirationTime, | ||
| attestation.revocable, | ||
| attestation.refUID, | ||
| attestation.data, | ||
| bump | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| function makePayloadHash( | ||
| address recipient, | ||
| address attester, | ||
| bytes32 attestationID | ||
| ) pure private returns (bytes32) { | ||
| return keccak256( | ||
| abi.encode( | ||
| EVENT_SIG, | ||
| recipient, | ||
| attester, | ||
| SCHEMA_UID, | ||
| attestationID | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| function verifyAttestation( | ||
| address recipient, | ||
| address attester, | ||
| uint256 logIndex, | ||
| uint256 blockNumber, | ||
| uint64 timestamp, | ||
| uint256 chainId, | ||
| string memory name | ||
| ) public { | ||
| Attestation memory attestation; | ||
| attestation.schema = SCHEMA_UID; | ||
| attestation.recipient = recipient; | ||
| attestation.attester = attester; | ||
| attestation.revocable = true; | ||
| attestation.time = timestamp; | ||
| attestation.data = abi.encode(name); | ||
| bytes32 attestationUID = _getUID(attestation, 0); | ||
|
qbzzt marked this conversation as resolved.
|
||
|
|
||
| bytes32 payloadHash = makePayloadHash(recipient, attester, attestationUID); | ||
|
|
||
| Identifier memory logEntryIdentifier; | ||
| logEntryIdentifier.origin = EAS_CONTRACT; | ||
| logEntryIdentifier.blockNumber = blockNumber; | ||
| logEntryIdentifier.logIndex = logIndex; | ||
| logEntryIdentifier.timestamp = timestamp; | ||
| logEntryIdentifier.chainId = chainId; | ||
|
|
||
| // Signal that this is a cross chain call that needs to have the identifier validated | ||
| ICrossL2Inbox(CROSS_L2_INBOX).validateMessage(logEntryIdentifier, payloadHash); | ||
|
|
||
| // Code that uses the attestation goes here | ||
| } | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { | ||
| EAS, | ||
| NO_EXPIRATION, | ||
| SchemaEncoder, | ||
| SchemaRegistry, | ||
| } from "@ethereum-attestation-service/eas-sdk" | ||
|
|
||
| import { | ||
| createWalletClient, | ||
| http, | ||
| publicActions, | ||
| } from 'viem' | ||
| import { privateKeyToAccount } from 'viem/accounts' | ||
|
|
||
| import { JsonRpcProvider, Wallet } from "ethers" | ||
|
|
||
| import { interopAlpha0, supersimL2A } from '@eth-optimism/viem/chains' | ||
|
|
||
| // Contract addresses in all OP Stack blockchains | ||
| const EASContractAddress = "0x4200000000000000000000000000000000000021" | ||
| const schemaRegistryContractAddress = "0x4200000000000000000000000000000000000020" | ||
|
|
||
| // Turn a viem wallet into an ethers provider | ||
| const walletClientToSigner = walletClient => { | ||
| const chain = walletClient.chain | ||
|
|
||
| // Get the RPC URL from the chain config (or supply your own) | ||
| const rpcUrl = chain.rpcUrls?.default?.http?.[0] | ||
| if (!rpcUrl) { | ||
| throw new Error('RPC URL not found in chain configuration') | ||
| } | ||
|
|
||
| // Create a provider for the given chain | ||
| const provider = new JsonRpcProvider(rpcUrl, chain.id) | ||
|
|
||
| const signer = new Wallet(process.env.PRIVATE_KEY, provider) | ||
|
|
||
| return signer | ||
| } | ||
|
|
||
| // Initialize the sdk with the address of the EAS Schema contract address | ||
| const eas = new EAS(EASContractAddress) | ||
|
|
||
| const schemaRegistry = new SchemaRegistry(schemaRegistryContractAddress) | ||
|
|
||
| const account = privateKeyToAccount(process.env.PRIVATE_KEY) | ||
| const useSupersim = process.env.CHAIN_B_ID == 902 | ||
|
qbzzt marked this conversation as resolved.
|
||
|
|
||
| const wallet0 = createWalletClient({ | ||
| chain: useSupersim ? supersimL2A : interopAlpha0, | ||
| transport: http(), | ||
| account | ||
| }).extend(publicActions) | ||
|
|
||
| // Turn a viem wallet into an ethers provider | ||
| const signer0 = await walletClientToSigner(wallet0) | ||
| schemaRegistry.connect(signer0) | ||
| eas.connect(signer0) | ||
|
|
||
| const schema = "string name"; | ||
| let schemaTxn, schemaUID | ||
|
|
||
| // Register the schema if needed, and get the schemaUID. | ||
| try { | ||
| schemaTxn = await schemaRegistry.register({schema}) | ||
| schemaUID = await schemaTxn.wait() | ||
| } catch (err) { | ||
| // Schema is already registered | ||
| if (err.info.error.data == "0x23369fa6") | ||
| schemaUID = "0x234dee4d3e6a625b4121e2042d6267058755e53a2ecc55555da51a1e6f06cc58" | ||
| } | ||
|
qbzzt marked this conversation as resolved.
|
||
|
|
||
| const schemaEncoder = new SchemaEncoder(schema) | ||
| const attestedData = schemaEncoder.encodeData([ | ||
| { name: "name", value: "Bill Hamm", type: "string" } | ||
| ]); | ||
|
|
||
| const attestTxn = await eas.attest({ | ||
| schema: schemaUID, | ||
| data: { | ||
| recipient: "0x0123456789012345678901234567890123456789", | ||
| expirationTime: NO_EXPIRATION, | ||
| revocable: true, // Be aware that if your schema is not revocable, this MUST be false | ||
| data: attestedData, | ||
| }, | ||
| }) | ||
|
|
||
| // To get the attestation ID we'd use | ||
| // const attestationID = await transaction2.wait() | ||
| // However, here we need the attestation transaction's hash. | ||
|
|
||
| const request = await wallet0.prepareTransactionRequest(attestTxn.data) | ||
| const serializedTransaction = await wallet0.signTransaction(request) | ||
| const attestHash = await wallet0.sendRawTransaction({ serializedTransaction }) | ||
|
|
||
| console.log(`export ATTEST_TXN=${attestHash}`) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { | ||
| createWalletClient, | ||
| http, | ||
| publicActions, | ||
| getContract, | ||
| } from 'viem' | ||
| import { privateKeyToAccount } from 'viem/accounts' | ||
|
|
||
| import { interopAlpha0, interopAlpha1, supersimL2A, supersimL2B } from '@eth-optimism/viem/chains' | ||
| import { walletActionsL2, publicActionsL2 } from '@eth-optimism/viem' | ||
| import { readFile } from 'fs/promises'; | ||
|
|
||
| async function loadVerifierAbi() { | ||
| const data = await readFile('../onchain/out/Verifier.sol/Verifier.json') | ||
| return JSON.parse(data); | ||
| } | ||
|
|
||
| const verifierAbi = (await loadVerifierAbi()).abi | ||
|
|
||
| // Contract addresses in all OP Stack blockchains | ||
| const EASContractAddress = "0x4200000000000000000000000000000000000021" | ||
|
|
||
| const account = privateKeyToAccount(process.env.PRIVATE_KEY) | ||
| const useSupersim = process.env.CHAIN_B_ID == 902 | ||
|
|
||
| const wallet0 = createWalletClient({ | ||
| chain: useSupersim ? supersimL2A : interopAlpha0, | ||
| transport: http(), | ||
| account | ||
| }).extend(publicActions) | ||
| .extend(publicActionsL2()) | ||
|
|
||
| const wallet1 = createWalletClient({ | ||
| chain: useSupersim ? supersimL2B : interopAlpha1, | ||
| transport: http(), | ||
| account | ||
| }).extend(publicActions) | ||
| .extend(walletActionsL2()) | ||
|
|
||
| let receipt | ||
|
|
||
| try { | ||
| receipt = await wallet0.getTransactionReceipt({ hash: process.env.ATTEST_TXN }) | ||
| } catch(err) { | ||
| console.log(`Verification failed, there is no ${process.env.ATTEST_TXN} transaction on the source chain`) | ||
| process.exit(0) | ||
| } | ||
|
|
||
| const attestLogEntry = receipt.logs.filter(x => | ||
| (x.address == EASContractAddress) && | ||
| (x.topics[0] == "0x8bf46bf4cfd674fa735a3d63ec1c9ad4153f033c290341f3a588b75685141b35"))[0] | ||
|
qbzzt marked this conversation as resolved.
|
||
|
|
||
| const relayMessageParams = await wallet0.interop.buildExecutingMessage({ | ||
| log: attestLogEntry, | ||
| }) | ||
|
|
||
| const verifier = getContract({ | ||
| address: process.env.VERIFIER_ADDRESS, | ||
| abi: verifierAbi, | ||
| client: wallet1, | ||
| }) | ||
|
|
||
| const verificationTransaction = await verifier.write.verifyAttestation({ | ||
| args: [ | ||
| "0x" + relayMessageParams.payload.slice(90,130), | ||
| "0x" + relayMessageParams.payload.slice(154,194), | ||
| relayMessageParams.id.logIndex, | ||
| relayMessageParams.id.blockNumber, | ||
| relayMessageParams.id.timestamp, | ||
| relayMessageParams.id.chainId, | ||
| "Bill Hamm" | ||
|
qbzzt marked this conversation as resolved.
|
||
| ], | ||
|
qbzzt marked this conversation as resolved.
|
||
| accessList: relayMessageParams.accessList | ||
| }) | ||
|
|
||
| console.log(`VERIFICATION_TRANSACTION_HASH=${verificationTransaction}`) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import { | ||
| createWalletClient, | ||
| http, | ||
| publicActions, | ||
| getContract, | ||
| keccak256, | ||
| toBytes, | ||
| } from 'viem' | ||
| import { privateKeyToAccount } from 'viem/accounts' | ||
|
|
||
| import { interopAlpha0, interopAlpha1, supersimL2A, supersimL2B } from '@eth-optimism/viem/chains' | ||
| import { walletActionsL2, publicActionsL2, crossL2InboxAbi } from '@eth-optimism/viem' | ||
|
|
||
| // Contract addresses in all OP Stack blockchains | ||
| const EASContractAddress = "0x4200000000000000000000000000000000000021" | ||
|
|
||
| const account = privateKeyToAccount(process.env.PRIVATE_KEY) | ||
| const useSupersim = process.env.CHAIN_B_ID == 902 | ||
|
|
||
| const wallet0 = createWalletClient({ | ||
| chain: useSupersim ? supersimL2A : interopAlpha0, | ||
| transport: http(), | ||
| account | ||
| }).extend(publicActions) | ||
| .extend(publicActionsL2()) | ||
|
|
||
| const wallet1 = createWalletClient({ | ||
| chain: useSupersim ? supersimL2B : interopAlpha1, | ||
| transport: http(), | ||
| account | ||
| }).extend(publicActions) | ||
| .extend(walletActionsL2()) | ||
|
|
||
| let receipt | ||
|
|
||
| try { | ||
| receipt = await wallet0.getTransactionReceipt({ hash: process.env.ATTEST_TXN }) | ||
| } catch(err) { | ||
| console.log(`Verification failed, there is no ${process.env.ATTEST_TXN} transaction on the source chain`) | ||
| process.exit(0) | ||
| } | ||
|
|
||
| const attestLogEntry = receipt.logs.filter(x => | ||
| (x.address == EASContractAddress) && | ||
| (x.topics[0] == "0x8bf46bf4cfd674fa735a3d63ec1c9ad4153f033c290341f3a588b75685141b35"))[0] | ||
|
|
||
| // attestLogEntry.topics[1] = "0x8bf46bf4cfd674fa735a3d63ec1c9ad4153f033c290341f3a588b75685141b34" | ||
|
qbzzt marked this conversation as resolved.
|
||
|
|
||
| const relayMessageParams = await wallet0.interop.buildExecutingMessage({ | ||
| log: attestLogEntry, | ||
| }) | ||
|
|
||
| const crossL2Inbox = getContract({ | ||
| address: '0x4200000000000000000000000000000000000022', | ||
| abi: crossL2InboxAbi, | ||
| client: wallet1, | ||
| }) | ||
|
|
||
| let executingTransaction, executingTransactionReceipt | ||
|
|
||
| try { | ||
| executingTransaction = await crossL2Inbox.write.validateMessage( | ||
| { | ||
| args: [ | ||
| relayMessageParams.id, | ||
| keccak256(toBytes(relayMessageParams.payload)) | ||
| ], | ||
| accessList: relayMessageParams.accessList, | ||
| } | ||
| ) | ||
| } catch (err) { | ||
| console.log("Verification failed (revert)") | ||
| process.exit(0) | ||
| } | ||
|
|
||
| try { | ||
| executingTransactionReceipt = await wallet1.waitForTransactionReceipt({ | ||
| hash: executingTransaction, | ||
| timeout: 10_000 | ||
| }) | ||
| } catch (err) { | ||
| console.log("Verification failed (timeout)") | ||
| process.exit(0) | ||
| } | ||
|
|
||
| const verified = | ||
| executingTransactionReceipt.logs.filter( | ||
| x => x.address=="0x4200000000000000000000000000000000000022" && | ||
| x.topics[0] == "0x5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba7" && | ||
| x.topics[1] == keccak256(toBytes(relayMessageParams.payload)) | ||
|
qbzzt marked this conversation as resolved.
|
||
| ).length > 0 | ||
|
|
||
| if (verified) { | ||
| console.log("Verification successful") | ||
| } else { | ||
| console.log("Verification failed") | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.