Skip to main content
Superchain interop is in active development. Some features may be experimental.
Messages are relayed automatically in the interop devnet.

Overview

Learn to relay transactions directly by sending the correct transaction.

What you’ll build

Setup

These steps are necessary to run the tutorial, regardless of whether you are using cast or the JavaScript API.
1

Run Supersim

This exercise needs to be done on Supersim.
You cannot use the devnets because you cannot disable autorelay on them.
  1. Follow the installation guide.
  2. Run Supersim without --interop.relay.
./supersim
2

Create the state for relaying messages

The results of this step are similar to what the message passing tutorial would produce if you did not have autorelay on.Execute this script:
#! /bin/sh
# full shell script preserved here...
# (Greeter.sol, GreetingSender.sol, sendAndRelay.sh setup)
# ...
This script installs Greeter.sol on chain B and GreetingSender.sol on chain A.
These smart contracts let us send a message from chain A that needs to be relayed to chain B.
Then, the script creates ./manual-relay/sendAndRelay.sh to manually relay a message from chain A to chain B.
That script is explained below.
Finally, this script writes out some parameter setting lines that should be executed on the main shell before you continue.
With a fresh Supersim running, these should be:
GREETER_A_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
GREETER_B_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Manual relay using the API

1

Setup

Use a Node project.
  1. Initialize a new Node project.
mkdir -p manual-relay/offchain
cd manual-relay/offchain
npm init -y
npm install --save-dev viem @eth-optimism/viem
mkdir src
  1. Export environment variables:
export GREETER_A_ADDRESS GREETER_B_ADDRESS PRIVATE_KEY
2

Manual relaying app

Create a file manual-relay.mjs with:
    import {
    createWalletClient,
    http,
    publicActions,
    getContract,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { supersimL2A, supersimL2B } from '@eth-optimism/viem/chains' 
import { walletActionsL2, publicActionsL2 } from '@eth-optimism/viem'

import { readFileSync } from 'fs';

const greeterData = JSON.parse(readFileSync('../onchain/out/Greeter.sol/Greeter.json'))
const greetingSenderData = JSON.parse(readFileSync('../onchain/out/Greeter.sol/Greeter.json'))

const account = privateKeyToAccount(process.env.PRIVATE_KEY)

const walletA = createWalletClient({
    chain: supersimL2A,
    transport: http(),
    account
}).extend(publicActions)
    .extend(publicActionsL2())
//    .extend(walletActionsL2())

const walletB = createWalletClient({
    chain: supersimL2B,
    transport: http(),
    account
}).extend(publicActions)
//    .extend(publicActionsL2())
    .extend(walletActionsL2())

const greeter = getContract({
    address: process.env.GREETER_B_ADDRESS,
    abi: greeterData.abi,
    client: walletB
})

const greetingSender = getContract({
    address: process.env.GREETER_A_ADDRESS,
    abi: greetingSenderData.abi,
    client: walletA
})

const txnBHash = await greeter.write.setGreeting(
    ["Greeting directly to chain B"])
await walletB.waitForTransactionReceipt({hash: txnBHash})

const greeting1 = await greeter.read.greet()
console.log(`Chain B Greeting: ${greeting1}`)

const txnAHash = await greetingSender.write.setGreeting(
    ["Greeting through chain A"])
const receiptA = await walletA.waitForTransactionReceipt({hash: txnAHash})

const greeting2 = await greeter.read.greet()
console.log(`Greeting before the relay transaction: ${greeting2}`)

const sentMessages = await walletA.interop.getCrossDomainMessages({
  logs: receiptA.logs,
})
const sentMessage = sentMessages[0] // We only sent 1 message
const relayMessageParams = await walletA.interop.buildExecutingMessage({
  log: sentMessage.log,
})
const relayMsgTxnHash = await walletB.interop.relayCrossDomainMessage(relayMessageParams)

const receiptRelay = await walletB.waitForTransactionReceipt({
  hash: relayMsgTxnHash,
})

const greeting3 = await greeter.read.greet()
console.log(`Greeting after the relay transaction: ${greeting3}`)
Run JavaScript program:
node manual-relay.mjs
3

Debugging

To see what messages were relayed by a specific transaction:
import { decodeRelayedL2ToL2Messages } from '@eth-optimism/viem'

const decodedRelays = decodeRelayedL2ToL2Messages({ receipt: receiptRelay })

console.log(decodedRelays)
console.log(decodedRelays.successfulMessages[0].log)

Manual relay using cast

You can see an example of how to manually relay using cast in manual-relay/sendAndRelay.sh.
It is somewhat complicated, so the setup creates one that is tailored to your environment.
Run the script:
./manual-relay/sendAndRelay.sh
Here is the detailed explanation:
  1. Configuration parameters
    #! /bin/sh
    PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    USER_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
    URL_CHAIN_A=http://localhost:9545
    URL_CHAIN_B=http://localhost:9546
    GREETER_A_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
    GREETER_B_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3
    CHAIN_ID_B=902
    
    This is the configuration. The greeter addresses are identical because the nonce for the user address has an identical nonce on both chains.
  2. Send a message that needs to be relayed
    cast send -q --private-key $PRIVATE_KEY --rpc-url $URL_CHAIN_A $GREETER_A_ADDRESS "setGreeting(string)" "Hello from chain A $$"
    
    Send a message from chain A to chain B. The $$ is the process ID, so if you rerun the script you’ll see that the information changes.
  3. Find the log entry to relay
    cast logs "SentMessage(uint256,address,uint256,address,bytes)" --rpc-url $URL_CHAIN_A | tail -14 > log-entry
    
    Whenever L2ToL2CrossDomainMessenger sends a message to a different blockchain, it emits a SendMessage event. Extract only the latest SendMessage event from the logs.
  4. Manipulate the log entry to obtain information
    TOPICS=`cat log-entry | grep -A4 topics | awk '{print $1}' | tail -4 | sed 's/0x//'`
    TOPICS=`echo $TOPICS | sed 's/ //g'`
    
    Consolidate the log topics into a single hex string.
    ORIGIN=0x4200000000000000000000000000000000000023
    BLOCK_NUMBER=`cat log-entry | awk '/blockNumber/ {print $2}'`
    LOG_INDEX=`cat log-entry | awk '/logIndex/ {print $2}'`
    TIMESTAMP=`cast block $BLOCK_NUMBER --rpc-url $URL_CHAIN_A | awk '/timestamp/ {print $2}'`
    CHAIN_ID_A=`cast chain-id --rpc-url $URL_CHAIN_A`
    SENT_MESSAGE=`cat log-entry | awk '/data/ {print $2}'`
    
    Read additional fields from the log entry.
    LOG_ENTRY=0x`echo $TOPICS$SENT_MESSAGE | sed 's/0x//'`
    
    Consolidate the entire log entry.
  5. Create the access list for the executing message
RPC_PARAMS=$(cat <<INNER_END_OF_FILE
{
    "origin": "$ORIGIN",
    "blockNumber": "$BLOCK_NUMBER",
    "logIndex": "$LOG_INDEX",
    "timestamp": "$TIMESTAMP",
    "chainId": "$CHAIN_ID_A",
    "payload": "$LOG_ENTRY"
}
INNER_END_OF_FILE
)

ACCESS_LIST=`cast rpc admin_getAccessListForIdentifier --rpc-url http://localhost:8420 "$RPC_PARAMS" | jq .accessList`

To secure cross-chain messaging and prevent potential denial-of-service attacks, relay transactions require properly formatted access lists that include a checksum derived from the message data. This lets sequencers know what executing messages to expect in a transaction, which makes it easy not to include transactions that are invalid because they rely on messages that were never sent. The algorithm to calculate the access list is a bit complicated, but you dont need to worry about it. Supersim exposes RPC calls that calculates it for you on port 8420. The code above will calculate the correct access list even if youre using a different interop cluster where autorelay is not functioning. This is because the code implements a pure function, which produces consistent results regardless of external state. In contrast, the admin_getAccessListByMsgHash RPC call is not a pure function, it is dependent on system state and therefore less flexible in these situations.
  1. Show that the manual relay is necessary
    echo Old greeting
    cast call $GREETER_B_ADDRESS "greet()(string)" --rpc-url $URL_CHAIN_B
    
    Show the current greeting. The message has not been relayed yet, so it’s still the old greeting.
  2. Actually relay the message
    cast send -q $ORIGIN "relayMessage((address,uint256,uint256,uint256,uint256),bytes)" "($ORIGIN,$BLOCK_NUMBER,$LOG_INDEX,$TIMESTAMP,$CHAIN_ID_A)" $LOG_ENTRY --access-list "$ACCESS_LIST" --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY
    
    Call relayMessage to relay the message.
  3. Show the relay results
    echo New greeting
    cast call $GREETER_B_ADDRESS "greet()(string)" --rpc-url $URL_CHAIN_B
    
    Again, show the current greeting. Now it’s the new one.

Next steps

I