Skip to content

Commit

Permalink
Merge pull request #149 from ethereum-optimism/09-17-example_clean_up…
Browse files Browse the repository at this point in the history
…_code_for_crosschainpingpong_and_add_walkthrough

example: clean up code for CrossChainPingPong and add walkthrough
  • Loading branch information
jakim929 authored Sep 17, 2024
2 parents a9c8c63 + a2ad02a commit daa397a
Show file tree
Hide file tree
Showing 11 changed files with 3,047 additions and 59 deletions.
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

0 comments on commit daa397a

Please sign in to comment.