Skip to content
This repository was archived by the owner on Apr 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
aef7514
WIP
qbzzt May 15, 2025
34376e2
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt May 15, 2025
66e2b71
WIP
qbzzt May 16, 2025
1e89523
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt May 16, 2025
cc45607
First Draft
qbzzt May 18, 2025
417af89
Lint
qbzzt May 18, 2025
5060cd2
Merge branch 'main' into 250514-attestation-relay
qbzzt May 18, 2025
492d679
Works for supersim, not devnet
qbzzt May 19, 2025
e67e13a
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt May 19, 2025
1b94904
Added expiration disclaimer
qbzzt May 23, 2025
a9252d8
typo
qbzzt May 24, 2025
6b203fd
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt May 24, 2025
b454371
Update pages/interop/tutorials/verify-messages.mdx
krofax May 28, 2025
55bbb02
Update pages/interop/tutorials/verify-messages.mdx
krofax May 28, 2025
b797f53
Update pages/interop/tutorials/verify-messages.mdx
krofax May 28, 2025
5a8f6ff
Merge branch 'main' into 250514-attestation-relay
qbzzt May 29, 2025
6a01e26
coderabbit
qbzzt May 29, 2025
f5c99e3
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt May 29, 2025
ce905fb
Merge branch 'main' into 250514-attestation-relay
qbzzt Jun 2, 2025
f7c1e87
Merge branch 'main' into 250514-attestation-relay
qbzzt Jun 9, 2025
635928f
code rabbit
qbzzt Jun 9, 2025
82bb23b
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Jun 9, 2025
51ca768
Merge remote-tracking branch 'refs/remotes/origin/250514-attestation-…
qbzzt Jun 9, 2025
268e6b9
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Jun 9, 2025
a9b3d21
coderabbit
qbzzt Jun 9, 2025
9bf1adf
Update verify-messages.mdx
qbzzt Jun 9, 2025
adda27f
WIP
qbzzt Jun 9, 2025
56d2312
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt Jun 9, 2025
66f0917
Apply suggestions from @krofax
qbzzt Jun 9, 2025
d7460b0
Fixing indentation
qbzzt Jun 9, 2025
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
1 change: 1 addition & 0 deletions pages/interop/tutorials/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"transfer-superchainERC20": "Transferring a SuperchainERC20",
"custom-superchain-erc20": "Custom SuperchainERC20 tokens",
"bridge-crosschain-eth": "Bridging native cross-chain ETH transfers",
"verify-messages": "Verifying log entries",
"contract-calls": "Making crosschain contract calls (ping pong)",
"event-reads": "Making crosschain event reads (tic-tac-toe)",
"event-contests": "Deploying crosschain event composability (contests)",
Expand Down
369 changes: 369 additions & 0 deletions pages/interop/tutorials/verify-messages.mdx

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions public/tutorials/Verifier.sol
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 {
Comment thread
qbzzt marked this conversation as resolved.
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);
Comment thread
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
}

}
96 changes: 96 additions & 0 deletions public/tutorials/attest.mjs
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
Comment thread
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"
}
Comment thread
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}`)
76 changes: 76 additions & 0 deletions public/tutorials/onchain-verification.mjs
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]
Comment thread
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"
Comment thread
qbzzt marked this conversation as resolved.
],
Comment thread
qbzzt marked this conversation as resolved.
accessList: relayMessageParams.accessList
})

console.log(`VERIFICATION_TRANSACTION_HASH=${verificationTransaction}`)
97 changes: 97 additions & 0 deletions public/tutorials/verify-attestation.mjs
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"
Comment thread
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))
Comment thread
qbzzt marked this conversation as resolved.
).length > 0

if (verified) {
console.log("Verification successful")
} else {
console.log("Verification failed")
}
Loading