Skip to content

How do use cast to relay messages #1589

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
3 changes: 3 additions & 0 deletions pages/interop/tutorials/message-passing/_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"relay-with-cast": "Manual relay transaction",
}
220 changes: 220 additions & 0 deletions pages/interop/tutorials/message-passing/relay-with-cast.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
---
title: Manual relay transaction tutorial
description: >-
Learn to relay transactions directly by sending the correct transaction.
lang: en-US
content_type: tutorial
topic: interop-cast-manual-relay-tutorial
personas:
- protocol-developer
- chain-operator
- app-developer
categories:
- protocol
- interoperability
- cross-chain-messaging
- message-relaying
- cross-domain-messenger
- smart-contracts
- testnet
- superchain
is_imported_content: 'false'
---

import { Callout } from 'nextra/components'
import { Steps } from 'nextra/components'
import { InteropCallout } from '@/components/WipCallout'
import { AutorelayCallout } from '@/components/AutorelayCallout'

# Manual relay transactions tutorial

<InteropCallout />

<AutorelayCallout />

## Overview

Learn to relay transactions directly by sending the correct transaction.

<details>
<summary>About this tutorial</summary>

**Prerequisite technical knowledge**

* Familiarity with blockchain concepts
* Familiarity with [Foundry](https://book.getfoundry.sh/getting-started/installation), especially `cast`

**What you'll learn**

* How to use `cast` to relay transactions with autorelay does not work

**Development environment**

* Unix-like operating system (Linux, macOS, or WSL for Windows)
* Node.js version 16 or higher
* Git for version control
* Supersim
</details>

### What You'll Build

* A script to relay messages without using [the JavaScript library](https://www.npmjs.com/package/@eth-optimism/viem)

## Setup

<Steps>
### 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](/app-developers/tutorials/supersim/getting-started/installation).

2. Run Supersim *without* `--interop.relay`.

```sh
./supersim
```

### Create the state for relaying messages

The results of this step are similar to what the [message passing tutorial](/interop/tutorials/message-passing) would produce if you did not have autorelay on.

Execute this script.

```sh file=<rootDir>/public/tutorials/setup-for-manual-relay.sh#L1-L147 hash=a63d72f58a06ca7ca78fd1592efcf4a3
```
</Steps>

## Manually relay a message using `cast`

Run this script:

```sh
./manual-relay/sendAndRelay.sh
```

### Explanation

```sh
#! /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 the same because the nonce for the user address' nonce is the same.

```sh
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.

```sh
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](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L83-L91).
Here we look for those messages, but get only the last one.

<details>
<summary>Example `log-entry`</summary>

```yaml
- address: 0x4200000000000000000000000000000000000023
blockHash: 0xcd0be97ffb41694faf3a172ac612a23f224afc1bfecd7cb737a7a464cf5d133e
blockNumber: 426
data: 0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064a41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001948656c6c6f2066726f6d20636861696e2041203131333030370000000000000000000000000000000000000000000000000000000000000000000000
logIndex: 0
removed: false
topics: [
0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320
0x0000000000000000000000000000000000000000000000000000000000000386
0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3
0x0000000000000000000000000000000000000000000000000000000000000000
]
transactionHash: 0x1d6f2e5e2c8f3eb055e95741380ca36492f784b9782848b66b66c65c5937ff3a
transactionIndex: 0
```
</details>

```sh
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.

```sh
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.

```sh
LOG_ENTRY=0x`echo $TOPICS$SENT_MESSAGE | sed 's/0x//'`
```

Consolidate the entire log entry.

```sh
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 solve some [denial of service issues](https://github.com/ethereum-optimism/design-docs/blob/main/protocol/interop-access-list.md), relay transactions have to have an access list with a checksum of the message.
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](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol#L87-L115) is a bit complicated, but you don't need to worry about it.
Supersim exposes [RPC calls](https://supersim.pages.dev/guides/interop/cast?highlight=manuall#7-construct-the-access-list-for-the-message) that calculates it for you on port 8420.
Even if you use a different interop cluster that lost autorelay for some reason, the code above should calculate the correct access list because this is a [pure function](https://en.wikipedia.org/wiki/Pure_function). The other RPC call, `admin_getAccessListByMsgHash` is not and therefore is less flexible.

```sh
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.

```sh
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`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L197-L256) to relay the message.

```sh
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

* Review the [Superchain Interop Explainer](/interop/explainer) for answers to common questions about interoperability.
* Read the [Message Passing Explainer](/interop/message-passing) to understand what happens "under the hood".
* Write a revolutionary app that uses multiple blockchains within the Superchain.
152 changes: 152 additions & 0 deletions public/tutorials/setup-for-manual-relay.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#! /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

echo Set these environment variables
echo GREETER_A_ADDRESS=$GREETER_A_ADDRESS
echo GREETER_B_ADDRESS=$GREETER_B_ADDRESS
echo PRIVATE_KEY=$PRIVATE_KEY