Skip to content
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

example: clean up code for CrossChainPingPong and add walkthrough #149

Merged
Show file tree
Hide file tree
Changes from all 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 .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
uses: peaceiris/actions-mdbook@v1
with:
mdbook-version: "latest"
- name: Install mdbook plugins
run: |
cargo install mdbook-mermaid
- name: Build book
run: mdbook build ./docs
- name: Deploy
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# 🛠️ Supersim

Supersim is a lightweight tool to simulate the Superchain locally (with a single L1 and multiple OP-Stack L2s). Run multiple local nodes with one command, and coordinate message passing between them.
**Supersim is a lightweight tool to simulate the Superchain locally** (with a single L1 and multiple OP-Stack L2s).

### ✨ Features

- spin up multiple anvil nodes
- predeployed OP Stack contracts and useful mock contracts (ERC20)
- fork multiple remote chains (fork the entire Superchain)
- simulate L1 <> L2 message passing (deposits) and L2 <> L2 message passing (interoperability)


For **detailed instructions** and **usage guides**, refer to the [**📚 Supersim docs**](https://supersim.pages.dev).

## ❓ Why Supersim?

Expand All @@ -15,14 +23,6 @@ Multichain development offers unique challenges:

Supersim enables fast, local iteration on cross-chain features, with a simple developer interface.

## ✨ Features

- spin up multiple anvil nodes
- predeployed OP Stack contracts and useful mock contracts (ERC20)
- fork multiple remote chains (fork the entire Superchain)
- simulate L1 <> L2 message passing (deposits)
- simulate L2 <> L2 message passing (interoperability) and auto-relayer

## 🚀 Getting Started

### 1. Install prerequisites: `foundry`
Expand Down
153 changes: 117 additions & 36 deletions contracts/src/CrossChainPingPong.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,143 @@ pragma solidity 0.8.25;
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";

/// @notice Thrown when a function is called by an address other than the L2ToL2CrossDomainMessenger.
error CallerNotL2ToL2CrossDomainMessenger();

/// @notice Thrown when the cross-domain sender is not this contract's address on another chain.
error InvalidCrossDomainSender();

/// @notice Thrown when attempting to serve the ball from a non-server chain.
error UnauthorizedServer(uint256 callerChainId, uint256 serverChainId);

/// @notice Thrown when attempting to serve the ball more than once.
error BallAlreadyServed();

/// @notice Thrown when attempting to hit the ball to an invalid destination chain.
error InvalidDestinationChain(uint256 toChainId);

/// @notice Thrown when attempting to hit the ball when it's not on the current chain.
error BallNotPresent();

/// @notice Represents the state of the ping-pong ball.
/// @param rallyCount The number of times the ball has been hit.
/// @param lastHitterChainId The chain ID of the last chain to hit the ball.
/// @param lastHitterAddress The address of the last player to hit the ball.
struct PingPongBall {
uint256 rallyCount;
uint256 lastHitterChainId;
address lastHitterAddress;
}

/**
* @title CrossChainPingPong
* @notice This contract implements a cross-chain ping-pong game using the L2ToL2CrossDomainMessenger.
* Players hit a virtual *ball* back and forth between allowed L2 chains. The game starts with a serve
* from a designated server chain, and each hit increases the rally count. The contract tracks the
* last hitter's address, chain ID, and the current rally count.
* @dev This contract relies on the L2ToL2CrossDomainMessenger for cross-chain communication.
*/
contract CrossChainPingPong {
event BallSent(uint256 indexed toChainId, PingPongBall ball);
/// @notice Emitted when a ball is sent from one chain to another.
/// @param fromChainId The chain ID from which the ball is sent.
/// @param toChainId The chain ID to which the ball is sent.
/// @param ball The PingPongBall data structure containing rally count and hitter information.
event BallSent(uint256 indexed fromChainId, uint256 indexed toChainId, PingPongBall ball);

/// @notice Emitted when a ball is received on a chain.
/// @param fromChainId The chain ID from which the ball was sent.
/// @param toChainId The chain ID on which the ball was received.
/// @param ball The PingPongBall data structure containing rally count and hitter information.
event BallReceived(uint256 indexed fromChainId, uint256 indexed toChainId, PingPongBall ball);

/// @dev Address of the L2 to L2 cross-domain messenger predeploy.
address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;

event BallReceived(uint256 indexed fromChainId, PingPongBall ball);
/// @notice Chain ID of the server (initial ball sender).
uint256 internal immutable SERVER_CHAIN_ID;

address internal constant MESSENGER = Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER;
/// @dev Flag indicating if the server has already served the ball.
bool internal _hasServerAlreadyServed;

mapping(uint256 => bool) internal allowedChainIds;
/// @dev Mapping to track which chain IDs are allowed in the game.
mapping(uint256 => bool) internal _isChainIdAllowed;

uint256 internal serverChainId;
bool internal serverAlreadyServed;
/// @dev The current received ball on this chain.
PingPongBall internal _receivedBall;

PingPongBall internal receivedBall;
/// @dev Tracks whether the ball is currently on this chain.
bool private _isBallPresent;

/**
* @notice Constructor initializes the contract with allowed chain IDs and the server chain ID.
* @param _allowedChainIds The list of chain IDs that are allowed to participate in the game.
* @param _serverChainId The chain ID that will act as the server for the first ball serve.
* @dev Ensures that the server chain ID is included in the list of allowed chain IDs.
*/
constructor(uint256[] memory _allowedChainIds, uint256 _serverChainId) {
for (uint256 i = 0; i < _allowedChainIds.length; i++) {
allowedChainIds[_allowedChainIds[i]] = true;
_isChainIdAllowed[_allowedChainIds[i]] = true;
}

require(allowedChainIds[_serverChainId], "Invalid first server chain ID");
if (!_isChainIdAllowed[_serverChainId]) {
revert InvalidDestinationChain(_serverChainId);
}

serverChainId = _serverChainId;
SERVER_CHAIN_ID = _serverChainId;
}

function serveBall(uint256 _toChainId) external {
require(serverChainId == block.chainid, "Cannot serve ball from this chain");
require(serverAlreadyServed == false, "Ball already served");
require(_isValidDestinationChain(_toChainId), "Invalid destination chain ID");

serverAlreadyServed = true;
/**
* @notice Serve the ping-pong ball to a specified destination chain.
* @dev Can only be called once from the server chain, and this starts off the game.
* @param _toChainId The chain ID to which the ball is served.
*/
function serveBallTo(uint256 _toChainId) external {
if (SERVER_CHAIN_ID != block.chainid) {
revert UnauthorizedServer(block.chainid, SERVER_CHAIN_ID);
}
if (_hasServerAlreadyServed) {
revert BallAlreadyServed();
}
if (!_isValidDestinationChain(_toChainId)) {
revert InvalidDestinationChain(_toChainId);
}
_hasServerAlreadyServed = true;

PingPongBall memory _newBall = PingPongBall(1, block.chainid, msg.sender);

_sendBallMessage(_newBall, _toChainId);
_sendCrossDomainMessage(_newBall, _toChainId);

emit BallSent(_toChainId, _newBall);
emit BallSent(block.chainid, _toChainId, _newBall);
}

function sendBall(uint256 _toChainId) public {
require(_isBallOnThisChain(), "Ball is not on this chain");
require(_isValidDestinationChain(_toChainId), "Invalid destination chain ID");
/**
* @notice Hit the received ping-pong ball to a specified destination chain.
* @dev Can only be called when the ball is on the current chain.
* @param _toChainId The chain ID to which the ball is hit.
*/
function hitBallTo(uint256 _toChainId) public {
if (!_isBallPresent) {
revert BallNotPresent();
}
if (!_isValidDestinationChain(_toChainId)) {
revert InvalidDestinationChain(_toChainId);
}

PingPongBall memory _newBall = PingPongBall(receivedBall.rallyCount + 1, block.chainid, msg.sender);
PingPongBall memory _newBall = PingPongBall(_receivedBall.rallyCount + 1, block.chainid, msg.sender);

delete receivedBall;
delete _receivedBall;
_isBallPresent = false;

_sendBallMessage(_newBall, _toChainId);
_sendCrossDomainMessage(_newBall, _toChainId);

emit BallSent(_toChainId, _newBall);
emit BallSent(block.chainid, _toChainId, _newBall);
}

/**
* @notice Receives the ping-pong ball from another chain.
* @dev Only callable by the L2 to L2 cross-domain messenger.
* @param _ball The PingPongBall data received from another chain.
*/
function receiveBall(PingPongBall memory _ball) external {
if (msg.sender != MESSENGER) {
revert CallerNotL2ToL2CrossDomainMessenger();
Expand All @@ -74,24 +150,29 @@ contract CrossChainPingPong {
revert InvalidCrossDomainSender();
}

receivedBall.lastHitterAddress = _ball.lastHitterAddress;
receivedBall.lastHitterChainId = _ball.lastHitterChainId;
receivedBall.rallyCount = _ball.rallyCount;
_receivedBall = _ball;
_isBallPresent = true;

emit BallReceived(IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(), _ball);
emit BallReceived(IL2ToL2CrossDomainMessenger(MESSENGER).crossDomainMessageSource(), block.chainid, _ball);
}

function _sendBallMessage(PingPongBall memory _ball, uint256 _toChainId) internal {
/**
* @notice Internal function to send the ping-pong ball to another chain.
* @dev Uses the L2ToL2CrossDomainMessenger to send the message.
* @param _ball The PingPongBall data to send.
* @param _toChainId The chain ID to which the ball is sent.
*/
function _sendCrossDomainMessage(PingPongBall memory _ball, uint256 _toChainId) internal {
bytes memory _message = abi.encodeCall(this.receiveBall, (_ball));
IL2ToL2CrossDomainMessenger(MESSENGER).sendMessage(_toChainId, address(this), _message);
}

function _isBallOnThisChain() internal view returns (bool) {
// Hack to check if receivedBall has been set
return receivedBall.lastHitterAddress != address(0);
}

/**
* @notice Checks if a destination chain ID is valid.
* @param _toChainId The destination chain ID to validate.
* @return True if the destination chain ID is allowed and different from the current chain.
*/
function _isValidDestinationChain(uint256 _toChainId) internal view returns (bool) {
return allowedChainIds[_toChainId] && _toChainId != block.chainid;
return _isChainIdAllowed[_toChainId] && _toChainId != block.chainid;
}
}
31 changes: 19 additions & 12 deletions contracts/test/CrossChainPingPong.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
CrossChainPingPong,
PingPongBall,
CallerNotL2ToL2CrossDomainMessenger,
InvalidCrossDomainSender
InvalidCrossDomainSender,
BallAlreadyServed
} from "../src/CrossChainPingPong.sol";
import {IL2ToL2CrossDomainMessenger} from "@contracts-bedrock/L2/interfaces/IL2ToL2CrossDomainMessenger.sol";
import {Predeploys} from "@contracts-bedrock/libraries/Predeploys.sol";
Expand Down Expand Up @@ -38,15 +39,16 @@ contract CrossChainPingPongTest is Test {
}

// Test serving the ball
function testServeBall() public {
function testServeBallTo() public {
uint256 fromChainId = 901;
uint256 toChainId = 902;

vm.chainId(901);
vm.chainId(fromChainId);

// Expect the BallSent event
PingPongBall memory _expectedBall = PingPongBall(1, block.chainid, bob);
vm.expectEmit(true, true, true, true, address(crossChainPingPong));
emit CrossChainPingPong.BallSent(toChainId, _expectedBall);
emit CrossChainPingPong.BallSent(fromChainId, toChainId, _expectedBall);

// Mock cross-chain message send call
bytes memory _message = abi.encodeCall(crossChainPingPong.receiveBall, (_expectedBall));
Expand All @@ -60,17 +62,19 @@ contract CrossChainPingPongTest is Test {

// Serve the ball
vm.prank(bob);
crossChainPingPong.serveBall(toChainId);
crossChainPingPong.serveBallTo(toChainId);

// Ensure serve can only happen once
vm.expectRevert("Ball already served");
crossChainPingPong.serveBall(toChainId);
vm.expectRevert(BallAlreadyServed.selector);
crossChainPingPong.serveBallTo(toChainId);
}

// Test receiving the ball from a valid cross-chain message
function testReceiveBall() public {
uint256 fromChainId = 901;
uint256 toChainId = 902;

vm.chainId(toChainId);
// Set up the mock for cross-domain message sender validation
PingPongBall memory _ball = PingPongBall(1, fromChainId, address(this));
_mockAndExpect(
Expand All @@ -88,17 +92,20 @@ contract CrossChainPingPongTest is Test {

// Expect the BallReceived event
vm.expectEmit(true, true, true, true, address(crossChainPingPong));
emit CrossChainPingPong.BallReceived(fromChainId, _ball);
emit CrossChainPingPong.BallReceived(fromChainId, toChainId, _ball);

// Call receiveBall as if from the messenger
vm.prank(MESSENGER);
crossChainPingPong.receiveBall(_ball);
}

// Test receiving then sending the ball
function testSendBall() public {
function testHitBall() public {
// 1. receive a ball from 901 to 902
uint256 receiveFromChainId = 901;
uint256 receiveToChainId = 902;

vm.chainId(receiveToChainId);

// Set up the mock for cross-domain message sender validation
PingPongBall memory _ball = PingPongBall(1, receiveFromChainId, sally);
Expand All @@ -117,7 +124,7 @@ contract CrossChainPingPongTest is Test {

// Expect the BallReceived event
vm.expectEmit(true, true, true, true, address(crossChainPingPong));
emit CrossChainPingPong.BallReceived(receiveFromChainId, _ball);
emit CrossChainPingPong.BallReceived(receiveFromChainId, receiveToChainId, _ball);

vm.prank(MESSENGER);
crossChainPingPong.receiveBall(_ball);
Expand All @@ -141,11 +148,11 @@ contract CrossChainPingPongTest is Test {
);

vm.expectEmit(true, true, true, true, address(crossChainPingPong));
emit CrossChainPingPong.BallSent(sendToChainId, _newBall);
emit CrossChainPingPong.BallSent(sendFromChainId, sendToChainId, _newBall);

// Send the ball
vm.prank(bob);
crossChainPingPong.sendBall(sendToChainId);
crossChainPingPong.hitBallTo(sendToChainId);
}

// Test receiving ball with an invalid cross-chain message sender
Expand Down
28 changes: 28 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Supersim docs developer guide

Deployed docs can be found at https://supersim.pages.dev


Supersim docs are built using [mdbook](https://rust-lang.github.io/mdBook/). `mdbook` uses the `SUMMARY.md` file for the high level hierarchy ([docs](https://rust-lang.github.io/mdBook/format/summary.html)).

## Development

### 1. Install `mdbook` CLI tool
Installation options can be found [here](https://rust-lang.github.io/mdBook/guide/installation.html).

### 2. Go to the `docs` folder

```sh
cd docs
```

### 3. Run the `mdbook` CLI tool
By default the built book is available at http://localhost:3000

```sh
mdbook serve --open
```

## Deployment
1. On every merge to `main`, [`deploy-docs`](../.github/workflows/deploy-docs.yml) workflow creates a deployable branch for the docs called [`gh-pages`](https://github.com/ethereum-optimism/supersim/tree/gh-pages).
2. Generated docs branch is then deployed using [Cloudflare Pages](https://pages.cloudflare.com/)
8 changes: 7 additions & 1 deletion docs/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ src = "src"
title = "Supersim"

[output.html]
git-repository-url = "https://github.com/ethereum-optimism/supersim"
git-repository-url = "https://github.com/ethereum-optimism/supersim"
additional-js = ["src/static/mermaid.min.js", "src/static/mermaid-init.js", "src/static/solidity.min.js"]

[preprocessor]

[preprocessor.mermaid]
command = "mdbook-mermaid"
Loading
Loading