-
Notifications
You must be signed in to change notification settings - Fork 271
Audit of message passing tutorial #1616
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
krofax
merged 10 commits into
ethereum-optimism:main
from
qbzzt:250519-replace-audit-of-message-passing
May 28, 2025
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
18b9ad7
Replacements written
qbzzt cee7233
Removed link to removed tutorials
qbzzt 4adcc06
Auto-fix: Update breadcrumbs, spelling dictionary and other automated…
qbzzt 895c2b0
Update pages/interop/tutorials/message-passing.mdx
krofax 92c7446
@krofax change
qbzzt f0847db
WIP
qbzzt d092c4e
@krofax edits
qbzzt c056b77
Remove links
qbzzt 107118a
Update pages/interop/tutorials/message-passing/manual-relay.mdx
krofax 75b1c8d
Merge branch 'main' into 250519-replace-audit-of-message-passing
qbzzt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
{ | ||
"relay-with-cast": "Relay transactions manually" | ||
"manual-relay": "Relay transactions manually" | ||
} |
353 changes: 353 additions & 0 deletions
353
pages/interop/tutorials/message-passing/manual-relay.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,353 @@ | ||
--- | ||
title: Relay transactions manually | ||
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, Steps } from 'nextra/components' | ||
import { InteropCallout } from '@/components/WipCallout' | ||
import { AutorelayCallout } from '@/components/AutorelayCallout' | ||
|
||
# Relay transactions manually | ||
|
||
<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 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) | ||
</details> | ||
|
||
### What you'll build | ||
|
||
* A program to relay messages using [the JavaScript library](https://www.npmjs.com/package/@eth-optimism/viem) | ||
* A shell script to relay messages using [`cast`](https://book.getfoundry.sh/cast/) | ||
|
||
## Setup | ||
|
||
These steps are necessary to run the tutorial, regardless of whether you are using `cast` or the JavaScript API. | ||
|
||
<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 hash=eea0a71ff6b4c27b91225a715eb2718e | ||
``` | ||
|
||
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](#manual-relay-using-cast). | ||
|
||
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: | ||
|
||
```sh | ||
GREETER_A_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 | ||
GREETER_B_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 | ||
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | ||
``` | ||
|
||
</Steps> | ||
|
||
## Manual relay using the API | ||
|
||
<Steps> | ||
### Setup | ||
|
||
Use a [Node](https://nodejs.org/en) project. | ||
|
||
1. Initialize a new Node project. | ||
|
||
```sh | ||
mkdir -p manual-relay/offchain | ||
cd manual-relay/offchain | ||
npm init -y | ||
npm install --save-dev viem @eth-optimism/viem | ||
mkdir src | ||
``` | ||
|
||
2. Export environment variables. | ||
This is necessary because those variables are currently limited to the shell process. | ||
We need them in the Node process that the shell creates. | ||
|
||
```sh | ||
export GREETER_A_ADDRESS GREETER_B_ADDRESS PRIVATE_KEY | ||
``` | ||
|
||
### Manual relaying app | ||
|
||
1. Create a file `manual-relay.mjs` with: | ||
|
||
```javascript file=<rootDir>/public/tutorials/manual-relay.mjs hash=b0c44b327af300437f3d71ff049842f9 | ||
``` | ||
|
||
<details> | ||
<summary>Explanation</summary> | ||
|
||
```javascript file=<rootDir>/public/tutorials/manual-relay.mjs#L8-L9 hash=2d062eb374989a8a40199a4d7dc8be6e | ||
``` | ||
|
||
Import from the [`@eth-optimism/viem`](https://www.npmjs.com/package/@eth-optimism/viem) package. | ||
|
||
```javascript file=<rootDir>/public/tutorials/manual-relay.mjs#L18-L32 hash=cf5ce47bcbcd80327230a6da689688f8 | ||
``` | ||
|
||
In addition to extending the wallets with [Viem public actions](https://viem.sh/docs/accounts/local#5-optional-extend-with-public-actions), extend with the OP-Stack actions. | ||
On wallet A we need the public actions, those that only read information. | ||
On wallet B we need the wallet actions, the ones that require an account. | ||
|
||
```javascript file=<rootDir>/public/tutorials/manual-relay.mjs#L55 hash=23aa6f24baeb5757130361f30c1b0e9c | ||
``` | ||
|
||
To relay a message we need the information in the receipt. | ||
Also, we need to wait until the transaction with the relayed message is actually part of a block. | ||
|
||
```javascript file=<rootDir>/public/tutorials/manual-relay.mjs#L57-L58 hash=c0d3d8a60143c30b93db256518e8b583 | ||
``` | ||
|
||
Show the user that until the relay transaction happens on chain B, the greeting is unchanged. | ||
|
||
```javascript file=<rootDir>/public/tutorials/manual-relay.mjs#L60-L63 hash=8cc99e67ee36474c81183108531cb295 | ||
``` | ||
|
||
A single transaction can send multiple messages. | ||
But here we know we sent just one, so we look for the first one in the list. | ||
|
||
```javascript file=<rootDir>/public/tutorials/manual-relay.mjs#L64-L71 hash=b7ed7d70ba5ec84322beee5369c5bee5 | ||
``` | ||
|
||
Here we first send the relay message on chain B, and then wait for the receipt for it. | ||
</details> | ||
|
||
2. Run JavaScript program, and see that the message is relayed. | ||
|
||
```sh | ||
node manual-relay.mjs | ||
``` | ||
|
||
### Debugging | ||
|
||
To see what messages were relayed by a specific transaction you can use this code: | ||
|
||
```javascript | ||
import { decodeRelayedL2ToL2Messages } from '@eth-optimism/viem' | ||
|
||
const decodedRelays = decodeRelayedL2ToL2Messages( | ||
{receipt: receiptRelay}) | ||
|
||
console.log(decodedRelays) | ||
console.log(decodedRelays.successfulMessages[0].log) | ||
``` | ||
</Steps> | ||
|
||
## 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. | ||
|
||
You can run the script using this command: | ||
|
||
```sh | ||
./manual-relay/sendAndRelay.sh | ||
``` | ||
|
||
Here is the detailed explanation: | ||
|
||
1. Configuration parameters | ||
|
||
```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 identical because the nonce for the user address has an identical nonce on both chains. | ||
|
||
2. Send a message that needs to be relayed | ||
|
||
```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. | ||
|
||
3. Find the log entry to relay | ||
|
||
```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`](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol#L83-L91) event. | ||
Extract only the latest `SendMessage` event from the logs. | ||
|
||
<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> | ||
|
||
4. Manipulate the log entry to obtain information | ||
|
||
```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. | ||
|
||
5. Create the access list for the executing message | ||
|
||
```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 secure cross-chain messaging and prevent potential [denial-of-service attacks](https://github.com/ethereum-optimism/design-docs/blob/main/protocol/interop-access-list.md), 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](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. | ||
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](https://en.wikipedia.org/wiki/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. | ||
|
||
6. Show that the manual relay is necessary | ||
|
||
```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. | ||
|
||
7. Actually relay the message | ||
|
||
```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. | ||
|
||
8. Show the relay results | ||
|
||
```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. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.