Interoperability
Verifying log entries

Verifying log entries

This guide shows how to verify log entries from a specific chain on a different chain in an interop cluster. This lets you use interoperability with applications on a different chain that was never written with interoperability in mind.

To demonstrate this functionality, we'll use an attestation (opens in a new tab) from one chain in another.

Overview

About this tutorial

What you'll learn

  • How to verify log messages from one blockchain on another.
  • How to use the EAS SDK to attest for facts and add schemas from offchain code.

Technical knowledge

  • Intermediate JavaScript knowledge
  • Understanding of smart contract development
  • Familiarity with blockchain concepts

Development environment

  • Unix-like operating system (Linux, macOS, or WSL for Windows)
  • Node.js version 16 or higher
  • Git for version control

Required tools

The tutorial uses these primary tools:

  • Node: For running TypeScript code from the command line
  • Viem: For blockchain interaction
  • Foundry: For smart contract development

What you'll build

  • A JavaScript program to create an attestation and then verify it on a different chain.
  • A Solidity contract that can verify attestations onchain.

Directions

Preparation

  1. If you are using Supersim, setup the SuperchainERC20 starter kit. The pnpm dev step also starts Supersim.

  2. Store the configuration in environment variables.

    export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    URL_CHAIN_A=http://127.0.0.1:9545
    URL_CHAIN_B=http://127.0.0.1:9546
    export CHAIN_B_ID=`cast chain-id --rpc-url $URL_CHAIN_B`            

Create an attestation

There are already attestations in the production chains. However, that may not be the case in the devnets, and it is definitely not the case in the Supersim instance you just started.

So the first step is to actually send an attestation to one of the chains.

  1. Create a new JavaScript project.

    mkdir -p verify-messages/offchain
    cd verify-messages/offchain
    npm init -y
    npm install @eth-optimism/viem @ethereum-attestation-service/eas-sdk ethers viem
  2. Create a file, attest.mjs.

    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
     
    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"
    }
     
    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}`)
    Explanation
    // 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
    }

    The EAS SDK (opens in a new tab) we use for attestations uses Ethers (opens in a new tab) rather than Viem (opens in a new tab). This function lets us use a Viem wallet (opens in a new tab) as an Ethers signer (opens in a new tab).

    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"
    }

    Register the EAS Schema (opens in a new tab) if necessary. This schema ties Ethereum addresses to names.

    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,
      },
    })

    Attest (opens in a new tab) that the name for Ethereum address 0x0123456789012345678901234567890123456789 is Bill Hamm.

    // To get the attestation ID we'd use
    // const attestationID = await transaction2.wait()
    // However, here we need the attestation transaction's hash.

    This is the recommended way to execute the attestation transaction, which provides you with the attestation's UID. However, for our purpose we need the transaction receipt, which you do not get. We can either use this and search for the log entry or just use the EAS SDK to create the transaction and send it ourselves. Here we use the second solution.

    const request = await wallet0.prepareTransactionRequest(attestTxn.data)
    const serializedTransaction = await wallet0.signTransaction(request)
    const attestHash = await wallet0.sendRawTransaction({ serializedTransaction })

    Here we send the attestation transaction created by the EAS SDK ourselves (using viem's sendRawTransaction (opens in a new tab)), so we'll have the transaction hash.

  3. Run the attestation program.

    node attest.mjs
  4. The program output includes an export ATTEST_TXN line. Run it to inform our verification code where to find the attestation.

Offchain verification

The next step is to call CrossL2Inbox.verifyMessage. For testing purposes, we'll start by calling it offchain.

  1. Create a file, verify-attestation.mjs.

    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"
     
    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))
      ).length > 0
     
    if (verified) {
      console.log("Verification successful")
    } else {
      console.log("Verification failed")
    }
     
    Explanation
    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]
     

    To create the executing message, we need several fields that appear in the log entry we are verifying. This is how we obtain the relevant log entry.

    The first topic in a log entry emitted by Solidity is the event type. This event type (opens in a new tab) is emitted when an attestation is created. Of course, we only care about attestations created in the official attestations contract.

    // attestLogEntry.topics[1] = "0x8bf46bf4cfd674fa735a3d63ec1c9ad4153f033c290341f3a588b75685141b34"
     

    Part of testing a system is ensuring that it does not accept invalid input. You can uncomment this line to see that the verification fails with an incorrect log entry.

    const relayMessageParams = await wallet0.interop.buildExecutingMessage({
      log: attestLogEntry,
    })
     

    Build the executing message (opens in a new tab).

    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)
    }
     

    Call CrossL2Inbox.validateMessage (opens in a new tab). For some invalid inputs this function reverts, which means that the log entry requested is not validated.

    try {
      executingTransactionReceipt = await wallet1.waitForTransactionReceipt({
        hash: executingTransaction,
        timeout: 10_000 
      })    
    } catch (err) {
      console.log("Verification failed (timeout)")
      process.exit(0)
    }
     

    Wait for the transaction receipt for the executing message. Note the timeout, it is necessary because if the executing message is valid, but the log entry has not been seen yet the sequencer does not put the transaction with the message into a block.

    const verified = 
      executingTransactionReceipt.logs.filter(
        x => x.address=="0x4200000000000000000000000000000000000022" &&
             x.topics[0] == "0x5c37832d2e8d10e346e55ad62071a6a2f9fa5130614ef2ec6617555c6f467ba7" &&
             x.topics[1] == keccak256(toBytes(relayMessageParams.payload))
      ).length > 0
     

    Look for the log entry from CrossL2Inbox. To ensure this is the correct log entry, we look at the message type (opens in a new tab) and check that the ID is the correct one.

  2. Execute this program.

    node verify-attestation.mjs

Onchain verification

The code in the above example is not very useful, because it is running offchain. Offchain code can talk to any chain it wants, so it can verify the attestation directly on the chain where it originated. The real value of interop is when you have onchain code from one chain verify information from another chain. This is what we'll build now.

The access list of the transaction has to be calculated offchain, so we might as well just use the same interop.buildExecutingMessage function and just use relayParams to see what exactly we are validating.

ValueLocationSample value
Event signature (opens in a new tab)relayMessageParams.payload.slice(2,66)8bf46bf4cfd674fa735a3d63ec1c9ad4153f033c290341f3a588b75685141b35
Recipient addressrelayMessageParams.payload.slice(90,130)0123456789012345678901234567890123456789
Attester addressrelayMessageParams.payload.slice(154,194)f39fd6e51aad88f6f4ce6ab8827279cfffb92266
SchemarelayMessageParams.payload.slice(194,258)234dee4d3e6a625b4121e2042d6267058755e53a2ecc55555da51a1e6f06cc58
Attestation IDrelayMessageParams.payload.slice(194,258)c88cbbc15b9fb4aa12f70c9c97e6c1dd733a29db2816142809504718d615ff9f
Log entry originrelayMessageParams.id.origin0x4200000000000000000000000000000000000021
Log entry locationrelayMessageParams.id.logIndex0n
Block numberrelayMessageParams.id.blockNumber6746n
TimestamprelayMessageParams.id.timestamp1747518156n
Chain IdrelayMessageParams.id.chainId901n

The attestation ID itself is also a hash (opens in a new tab), but luckily most of the fields are either unused (such as revocationTime) or are already known to us (such as the schema). The sole exception is the data, which in our case includes the name being attested to. We can provide this data to the Solidity code as a parameter.

  1. Create a new Foundry project.

    mkdir ../onchain
    cd ../onchain
    forge init
  2. Add the EAS contracts (opens in a new tab) and Optimism contract to the project.

    cd lib
    npm install @ethereum-attestation-service/eas-contracts
    cd ..
    echo @ethereum-attestation-service/=lib/node_modules/@ethereum-attestation-service/ >> remappings.txt
  3. Create src/Verifier.sol.

    // 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);
        
            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        
        }
        
    }
    Explanation
    // 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;
    }

    At writing these definitions from ICrossL2Inbox (opens in a new tab) are not yet part of the npm package (opens in a new tab), so we include them here for now.

        /// @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
                    )
                );
        }

    This function is part of the EAS contract code (opens in a new tab) that calculates the attestation ID.

        function makePayloadHash(
            address recipient,
            address attester,
            bytes32 attestationID
        ) pure private returns (bytes32) {
            return keccak256(
                abi.encode(
                    EVENT_SIG,
                    recipient,
                    attester,
                    SCHEMA_UID,
                    attestationID
                )
            );
        }

    Calculate the payload hash.

        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);
        
            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);

    This is the function that gets the attestation information, builds the call, and calls CrossL2Inbox.

            // Code that uses the attestation goes here        

    Here you would do anything that requires the attesattion to have been verified. There is nothing here because it is sample code, but in a real world application this would be the most important part.

  4. Deploy the new contract and preserve the address.

    export VERIFIER_ADDRESS=`forge create Verifier --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY --broadcast | awk '/Deployed to:/ {print $3}'`
  5. You can try to just call verifyAttestation directly, but without the correct access list it is guaranteed to fail. Instead, get back to the offchain part of the project.

    cd ../offchain
  6. Create a file, onchain-verification.mjs.

    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]
     
    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"
      ],
      accessList: relayMessageParams.accessList
    })
     
    console.log(`VERIFICATION_TRANSACTION_HASH=${verificationTransaction}`)
    Explanation

    This is a modified version of the verify-attestation.mjs we used earlier, so we only go over the new parts.

    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

    Read the ABI for Verifier we created onchain.

    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"
      ],
      accessList: relayMessageParams.accessList
    })

    Call Verifier with the information needed to verify the attestation.

    console.log(`VERIFICATION_TRANSACTION_HASH=${verificationTransaction}`)

    Report the transaction hash.

  7. Run the verification program.

    node onchain-verification.mjs
  8. The program output includes a VERIFICATION_TRANSACTION_HASH= line. Run it to keep track of the verification transaction.

  9. See the receipt for the verification transaction.

    cast receipt $VERIFICATION_TRANSACTION_HASH --rpc-url $URL_CHAIN_B
  10. Modify the name in line 73 of onchain-verification.mjs and see that false attestations don't get verified.

Next steps