Interoperability
Relay transactions manually

Relay transactions manually

Interop is currently in active development and not yet ready for production use. The information provided here may change frequently. We recommend checking back regularly for the most up-to-date information.
💡
Normally we expect Superchain blockchains to run an autorelayer and relay your messages automatically. However, for performance reasons or reliability, you might decide to submit the executing message manually. See below to learn how to do that.

Overview

Learn to relay transactions directly by sending the correct transaction.

About this tutorial

Prerequisite technical knowledge

What you'll learn

  • How to use cast to relay transactions when autorelay does not work

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

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

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
 
rm -rf manual-relay
mkdir manual-relay
cd manual-relay
 
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
USER_ADDRESS=`cast wallet address --private-key $PRIVATE_KEY`
URL_CHAIN_A=http://localhost:9545
URL_CHAIN_B=http://localhost:9546
 
forge init
cat > src/Greeter.sol <<EOF
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
import { Predeploys } from "@eth-optimism/contracts-bedrock/src/libraries/Predeploys.sol";
import { IL2ToL2CrossDomainMessenger } from "@eth-optimism/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol";    
 
contract Greeter {
 
    IL2ToL2CrossDomainMessenger public immutable messenger =
        IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
 
    string greeting;
 
    event SetGreeting(
        address indexed sender,     // msg.sender
        string greeting
    );
 
    event CrossDomainSetGreeting(
        address indexed sender,   // Sender on the other side
        uint256 indexed chainId,  // ChainID of the other side
        string greeting
    );
 
    function greet() public view returns (string memory) {
        return greeting;
    }
 
    function setGreeting(string memory _greeting) public {
        greeting = _greeting;
        emit SetGreeting(msg.sender, _greeting);
 
        if (msg.sender == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) {
            (address sender, uint256 chainId) =
                messenger.crossDomainMessageContext();
            emit CrossDomainSetGreeting(sender, chainId, _greeting);
        }
    }
}
EOF
 
cat > src/GreetingSender.sol <<EOF
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
 
import { Predeploys } from "@eth-optimism/contracts-bedrock/src/libraries/Predeploys.sol";
import { IL2ToL2CrossDomainMessenger } from "@eth-optimism/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol";
 
import { Greeter } from "src/Greeter.sol";
 
contract GreetingSender {
    IL2ToL2CrossDomainMessenger public immutable messenger =
        IL2ToL2CrossDomainMessenger(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
 
    address immutable greeterAddress;
    uint256 immutable greeterChainId;
 
    constructor(address _greeterAddress, uint256 _greeterChainId) {
        greeterAddress = _greeterAddress;
        greeterChainId = _greeterChainId;
    }
 
    function setGreeting(string calldata greeting) public {
        bytes memory message = abi.encodeCall(
            Greeter.setGreeting,
            (greeting)
        );
        messenger.sendMessage(greeterChainId, greeterAddress, message);
    }
}
EOF
 
cd lib
npm install @eth-optimism/contracts-bedrock
cd ..
echo @eth-optimism/=lib/node_modules/@eth-optimism/ >> remappings.txt
mkdir -p lib/node_modules/@eth-optimism/contracts-bedrock/interfaces/L2
wget https://raw.githubusercontent.com/ethereum-optimism/optimism/refs/heads/develop/packages/contracts-bedrock/interfaces/L2/IL2ToL2CrossDomainMessenger.sol
mv IL2ToL2CrossDomainMessenger.sol lib/node_modules/@eth-optimism/contracts-bedrock/interfaces/L2
CHAIN_ID_B=`cast chain-id --rpc-url $URL_CHAIN_B`
GREETER_B_ADDRESS=`forge create --rpc-url $URL_CHAIN_B --private-key $PRIVATE_KEY Greeter --broadcast | awk '/Deployed to:/ {print $3}'`
GREETER_A_ADDRESS=`forge create --rpc-url $URL_CHAIN_A --private-key $PRIVATE_KEY --broadcast GreetingSender --constructor-args $GREETER_B_ADDRESS $CHAIN_ID_B | awk '/Deployed to:/ {print $3}'`
 
echo Setup done
 
cat > sendAndRelay.sh <<EOF
#! /bin/sh
PRIVATE_KEY=$PRIVATE_KEY
USER_ADDRESS=$USER_ADDRESS
URL_CHAIN_A=$URL_CHAIN_A
URL_CHAIN_B=$URL_CHAIN_B
GREETER_A_ADDRESS=$GREETER_A_ADDRESS
GREETER_B_ADDRESS=$GREETER_B_ADDRESS
CHAIN_ID_B=$CHAIN_ID_B
 
cast send -q --private-key \$PRIVATE_KEY --rpc-url \$URL_CHAIN_A \$GREETER_A_ADDRESS "setGreeting(string)" "Hello from chain A \$$"
 
cast logs "SentMessage(uint256,address,uint256,address,bytes)" --rpc-url \$URL_CHAIN_A | tail -14 > log-entry
TOPICS=\`cat log-entry | grep -A4 topics | awk '{print \$1}' | tail -4 | sed 's/0x//'\`
TOPICS=\`echo \$TOPICS | sed 's/ //g'\`
 
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}'\`
LOG_ENTRY=0x\`echo \$TOPICS\$SENT_MESSAGE | sed 's/0x//'\`
 
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\`
 
echo Old greeting
cast call \$GREETER_B_ADDRESS "greet()(string)" --rpc-url \$URL_CHAIN_B
 
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
 
echo New greeting
cast call \$GREETER_B_ADDRESS "greet()(string)" --rpc-url \$URL_CHAIN_B
 
EOF
 
chmod +x sendAndRelay.sh

Manually relay a message using cast

Run this script:

./manual-relay/sendAndRelay.sh

Explanation

#! /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 the same nonce on both chains.

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.

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 (opens in a new tab) event. Extract only the latest SendMessage event from the logs.

Example log-entry
- address: 0x4200000000000000000000000000000000000023
  blockHash: 0xcd0be97ffb41694faf3a172ac612a23f224afc1bfecd7cb737a7a464cf5d133e
  blockNumber: 426
  data: 0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064a41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001948656c6c6f2066726f6d20636861696e2041203131333030370000000000000000000000000000000000000000000000000000000000000000000000
  logIndex: 0
  removed: false
  topics: [
          0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320
          0x0000000000000000000000000000000000000000000000000000000000000386
          0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3
          0x0000000000000000000000000000000000000000000000000000000000000000
  ]
  transactionHash: 0x1d6f2e5e2c8f3eb055e95741380ca36492f784b9782848b66b66c65c5937ff3a
  transactionIndex: 0
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.

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 (opens in a new tab), 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 (opens in a new tab) is a bit complicated, but you don't need to worry about it. Supersim exposes RPC calls (opens in a new tab) that calculates it for you on port 8420. The code above will calculate the correct access list even if you're using a different interop cluster where autorelay is not functioning. This is because the code implements a pure function (opens in a new tab), 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.

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.

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 (opens in a new tab) to relay the message.

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