Skip to content
This repository was archived by the owner on Apr 6, 2026. It is now read-only.
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",
Comment thread
qbzzt marked this conversation as resolved.
Outdated
}
Comment thread
krofax marked this conversation as resolved.
Outdated
220 changes: 220 additions & 0 deletions pages/interop/tutorials/message-passing/relay-with-cast.mdx
Comment thread
qbzzt marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
---
title: Manual relay transaction tutorial
Comment thread
qbzzt marked this conversation as resolved.
Outdated
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
Comment thread
krofax marked this conversation as resolved.
Outdated
Comment thread
qbzzt marked this conversation as resolved.
Outdated

<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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

**Development environment**

* Unix-like operating system (Linux, macOS, or WSL for Windows)
* Node.js version 16 or higher
* Git for version control
* Supersim
Comment thread
qbzzt marked this conversation as resolved.
Outdated
</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).
Comment thread
qbzzt marked this conversation as resolved.
Outdated

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
Comment thread
krofax marked this conversation as resolved.
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.
Comment thread
qbzzt marked this conversation as resolved.
Outdated

<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.
Comment thread
qbzzt marked this conversation as resolved.
Outdated
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.
Comment thread
qbzzt marked this conversation as resolved.
Outdated

```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".
Comment thread
qbzzt marked this conversation as resolved.
Outdated
* 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

Comment thread
qbzzt marked this conversation as resolved.
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
USER_ADDRESS=`cast wallet address --private-key $PRIVATE_KEY`
URL_CHAIN_A=http://localhost:9545
URL_CHAIN_B=http://localhost:9546

Comment thread
qbzzt marked this conversation as resolved.
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