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.
Prerequisite technical knowledge
Familiarity with blockchain concepts
Familiarity with Foundry , especially cast
What you’ll learn
How to use cast
to relay transactions when autorelay does not work
How to relay transactions using JavaScript
Development environment requirements
Unix-like operating system (Linux, macOS, or WSL for Windows)
Node.js version 16 or higher
Git for version control
Supersim environment configured and running
Foundry tools installed (forge, cast, anvil)
What you’ll build
Setup
These steps are necessary to run the tutorial, regardless of whether you are using cast
or the JavaScript API.
Run Supersim
This exercise needs to be done on Supersim.
You cannot use the devnets because you cannot disable autorelay on them.
Follow the installation guide .
Run Supersim without --interop.relay
.
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
Setup
Use a Node project.
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
Export environment variables:
export GREETER_A_ADDRESS GREETER_B_ADDRESS PRIVATE_KEY
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 } ` )
import { supersimL2A , supersimL2B } from '@eth-optimism/viem/chains'
import { walletActionsL2 , publicActionsL2 } from '@eth-optimism/viem'
Run JavaScript program:
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:
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.
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.
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.
- address : 0x4200000000000000000000000000000000000023
blockHash : 0xcd0be97ffb41694faf3a172ac612a23f224afc1bfecd7cb737a7a464cf5d133e
blockNumber : 426
data : 0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064a41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001948656c6c6f2066726f6d20636861696e2041203131333030370000000000000000000000000000000000000000000000000000000000000000000000
logIndex : 0
removed : false
topics : [
0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320
0x0000000000000000000000000000000000000000000000000000000000000386
0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3
0x0000000000000000000000000000000000000000000000000000000000000000
]
transactionHash : 0x1d6f2e5e2c8f3eb055e95741380ca36492f784b9782848b66b66c65c5937ff3a
transactionIndex : 0
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.
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.
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.
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.
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