From 9a79dc2059407e150c146285bad6b37274bb14fe Mon Sep 17 00:00:00 2001 From: Hamdi Allam Date: Thu, 28 Nov 2024 05:53:07 +0700 Subject: [PATCH] docs: tictactoe contract and frontend (#284) * frontend readme * contract docs & bugfix * succint file names * nits --- contracts/src/tictactoe/TicTacToe.sol | 6 +- docs/src/SUMMARY.md | 12 +- ...aying-interop-messages-cast.md => cast.md} | 2 +- ...=> cross-chain-contract-calls-pingpong.md} | 2 +- .../cross-chain-event-reads-tictactoe.md | 191 ++++++++++++++++++ .../interop/{relay-using-viem.md => viem.md} | 2 +- .../interop/writing-contract-using-l2cdm.md | 1 - examples/tictactoe/README.md | 58 +++++- 8 files changed, 257 insertions(+), 17 deletions(-) rename docs/src/guides/interop/{manually-relaying-interop-messages-cast.md => cast.md} (99%) rename docs/src/guides/interop/{cross-chain-contract-via-l2cdm.md => cross-chain-contract-calls-pingpong.md} (99%) create mode 100644 docs/src/guides/interop/cross-chain-event-reads-tictactoe.md rename docs/src/guides/interop/{relay-using-viem.md => viem.md} (99%) delete mode 100644 docs/src/guides/interop/writing-contract-using-l2cdm.md diff --git a/contracts/src/tictactoe/TicTacToe.sol b/contracts/src/tictactoe/TicTacToe.sol index 51e6c4fe..f41a357e 100644 --- a/contracts/src/tictactoe/TicTacToe.sol +++ b/contracts/src/tictactoe/TicTacToe.sol @@ -59,6 +59,7 @@ contract TicTacToe { struct Game { address player; address opponent; + uint256 gameId; // `1` for the player's moves, `2` opposing. uint8[3][3] moves; uint8 movesLeft; @@ -107,6 +108,7 @@ contract TicTacToe { Game storage game = games[chainId][gameId][msg.sender]; game.player = msg.sender; game.opponent = opponent; + game.gameId = gameId; game.lastOpponentId = _newGameId; game.movesLeft = 9; @@ -142,6 +144,7 @@ contract TicTacToe { // Record Game Metadata game.player = msg.sender; game.opponent = opponent; + game.gameId = gameId; game.lastOpponentId = _acceptedGameId; game.movesLeft = 9; @@ -170,9 +173,10 @@ contract TicTacToe { (uint256 chainId, uint256 gameId,, uint8 oppX, uint8 oppY) = abi.decode(_movePlayedData[32:], (uint256, uint256, address, uint8, uint8)); - // Game was instantiated for this player + // Game was instantiated for this player & the move is for the same game Game storage game = games[chainId][gameId][msg.sender]; if (game.player != msg.sender) revert GameNotExists(); + if (game.gameId != gameId) revert GameNotExists(); // The move played is forward progressing from the same chain if (_movePlayedId.chainId != game.lastOpponentId.chainId) revert IdChainMismatch(); diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index b89a512e..1b1f33dc 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -23,12 +23,8 @@ - [Sending deposit transactions](./guides/deposit-transactions.md) - [Interoperability](./guides/interop/README.md) - - [Viem to send and relay interop messages](./guides/interop/relay-using-viem.md) - - [Manually relaying interop messages with cast](./guides/interop/manually-relaying-interop-messages-cast.md) + - [Cross Chain Contract Calls (PingPong)](./guides/interop/cross-chain-contract-calls-pingpong.md) + - [Cross Chain Event Reading (TicTacToe)](./guides/interop/cross-chain-event-reads-tictactoe.md) - [Bridging SuperchainWETH](./guides/interop/bridging-superchain-weth.md) - - [Cross Chain Contract via L2ToL2CDM](./guides/interop/cross-chain-contract-via-l2cdm.md) - - [Calling a contract on destination chain]() - -# Examples - -- [Cross-chain tic-tac-toe](./examples/cross-chain-tictactoe.md) + - [Viem bindings](./guides/interop/viem.md) + - [Relay with Cast](./guides/interop/cast.md) \ No newline at end of file diff --git a/docs/src/guides/interop/manually-relaying-interop-messages-cast.md b/docs/src/guides/interop/cast.md similarity index 99% rename from docs/src/guides/interop/manually-relaying-interop-messages-cast.md rename to docs/src/guides/interop/cast.md index bc1f4353..201df30f 100644 --- a/docs/src/guides/interop/manually-relaying-interop-messages-cast.md +++ b/docs/src/guides/interop/cast.md @@ -1,5 +1,5 @@ -# Manually relaying interop messages with `cast` and L2ToL2CrossDomainMessenger +# `cast` commands to relay interop messages This guide describes how to form a [message identifier](https://specs.optimism.io/interop/messaging.html#message-identifier) to relay a [L2ToL2CrossDomainMessenger](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/src/L2/L2ToL2CrossDomainMessenger.sol) cross chain call. diff --git a/docs/src/guides/interop/cross-chain-contract-via-l2cdm.md b/docs/src/guides/interop/cross-chain-contract-calls-pingpong.md similarity index 99% rename from docs/src/guides/interop/cross-chain-contract-via-l2cdm.md rename to docs/src/guides/interop/cross-chain-contract-calls-pingpong.md index ed974aba..48ca74c9 100644 --- a/docs/src/guides/interop/cross-chain-contract-via-l2cdm.md +++ b/docs/src/guides/interop/cross-chain-contract-calls-pingpong.md @@ -1,5 +1,5 @@ -# Cross Chain Contract via the `L2ToL2CrossDomainMessenger` +# Cross Chain Contract Calls (PingPong) This guide walks through the `CrossChainPingPong.sol` contract, focusing on high level design and steps on integrating the `L2ToL2CrossChainMessenger` contract. The source code can be found [here](https://github.com/ethereum-optimism/supersim/blob/main/contracts/src/pingpong/CrossChainPingPong.sol). diff --git a/docs/src/guides/interop/cross-chain-event-reads-tictactoe.md b/docs/src/guides/interop/cross-chain-event-reads-tictactoe.md new file mode 100644 index 00000000..6a4fed4f --- /dev/null +++ b/docs/src/guides/interop/cross-chain-event-reads-tictactoe.md @@ -0,0 +1,191 @@ +# Cross Chain Event Reading (TicTacToe) + +A horizontally scalable implementation of TicTacToe. This [implementation](https://github.com/ethereum-optimism/supersim/blob/main/contracts/src/tictactoe/TicTacToe.sol) allows players to play each other from any chain without cross-chain calls, instead relying on cross-chain event reading. Since superchain interop can allow for event reading with a 1-block latency, the experience is the **same as a single-chain implementation** + +See the documentation for the [frontend](https://github.com/ethereum-optimism/supersim/tree/main/examples/tictactoe) for how this game UI is presented to the player. + +- [How it works](#how-it-works) + - [1. Intent To Play](#1-intent-to-play) + - [2. Accepting A Game](#2-accepting-a-game) + - [3. Starting The Game](#3-starting-the-game) + - [4. Making Moves](#4-making-moves) +- [Takeaways](#takeaways) + +## How it works + +We use events to define the ordering of the a game with players only maintaining a local view. By default, a chain is also apart of its own interopble dependency set, *meaning players on the same chain can also play each other with no code changes*! + +The system predeploy that enables pulling in validated cross-chain events is the [CrossL2Inbox](https://specs.optimism.io/interop/predeploys.html#crossl2inbox). + +```solidity +contract ICrossL2Inbox { + function validateMessage(Identifier calldata _id, bytes32 _msgHash) external view; +} +``` + +This contract relies on a **CREATE2** deployment to ensure a consistent address across all chains, used to assert the origin of the pulled in game event. + +### 1. Intent To Play + +A game is uniquely identified by the chain it was started from with a unqiue nonce. This identifier is included in all event fields such that each player can uniquely reference it locally. + +To start a game, a player invokes `newGame` which broadcasts a `NewGame` event that any opponent **on any chain** can react to. + +```solidity +event NewGame(uint256 chainId, uint256 gameId, address player); + +function newGame() external { + emit NewGame(block.chainid, nextGameId, msg.sender); + nextGameId++; +} +``` + +### 2. Accepting A Game + +When a `NewGame` event is observed, any player can declare their intent to play via `acceptGame`, referencing the `NewGame` event. An `AcceptedGame` event is emitted to signal to the creator that a game is ready to begin. + +```solidity +event AcceptedGame(uint256 chainId, uint256 gameId, address opponent, address player); + +function acceptGame(ICrossL2Inbox.Identifier calldata _newGameId, bytes calldata _newGameData) external { + if (_newGameId.origin != address(this)) revert IdOriginNotTicTacToe(); + ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_newGameId, keccak256(_newGameData)); + + bytes32 selector = abi.decode(_newGameData[:32], (bytes32)); + if (selector != NewGame.selector) revert DataNotNewGame(); + + ... + + emit AcceptedGame(chainId, gameId, game.opponent, game.player); +} +``` + +To prepare for the game, the event data is decoded and a local view of this game is stored. + +```solidity +(uint256 chainId, uint256 gameId, address opponent) = abi.decode(_newGameData[32:], (uint256, uint256, address)); +if (opponent == msg.sender) revert SenderIsOpponent(); + +// Record Game Metadata (no moves) +Game storage game = games[chainId][gameId][msg.sender]; +game.player = msg.sender; +game.opponent = opponent; +game.gameId = gameId; +game.lastOpponentId = _newGameId; +game.movesLeft = 9; + +emit AcceptedGame(chainId, gameId, game.opponent, game.player); +``` + +### 3. Starting The Game + +As `AcceptedGame` events are emmited, the player must pick one opponent to play. The opponent's `AcceptedGame` event is used to instantiate the game and play the starting move via the `MovePlayed` event. + +```solidity +event MovePlayed(uint256 chainId, uint256 gameId, address player, uint8 _x, uint8 _y); + +function startGame(ICrossL2Inbox.Identifier calldata _acceptedGameId, bytes calldata _acceptedGameData, uint8 _x, uint8 _y) external { + if (_acceptedGameId.origin != address(this)) revert IdOriginNotTicTacToe(); + ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_acceptedGameId, keccak256(_acceptedGameData)); + + bytes32 selector = abi.decode(_acceptedGameData[:32], (bytes32)); + if (selector != AcceptedGame.selector) revert DataNotAcceptedGame(); + + ... + + emit MovePlayed(chainId, gameId, game.player, _x, _y); +``` + +The event fields contain the information required to perform the neccessary validation. +- The game identifier for lookup +- The caller is the appropriate player +- The player is accepting from the same starting chain + +```solidity +(uint256 chainId, uint256 gameId, address player, address opponent) = // player, opponent swapped in local view + abi.decode(_acceptedGameData[32:], (uint256, uint256, address, address)); + +// The accepted game was started from this chain, from the sender +if (chainId != block.chainid) revert GameChainMismatch(); +if (msg.sender != player) revert SenderNotPlayer(); + +// Game has not already been started with an opponent. +Game storage game = games[chainId][gameId][msg.sender]; +if (game.opponent != address(0)) revert GameStarted(); + +// Store local view of this game +... + +// Locally record the move by the player with 1 +game.moves[_x][_y] = 1; +game.lastOpponentId = _acceptedGameId; + +emit MovePlayed(chainId, gameId, game.player, _x, _y); +``` + +### 3. Making Moves + +Once a game is started, players can continually make moves by invoking `makeMove`, reacting to a `MovePlayed` event of their opponent. + +```solidity +function makeMove(ICrossL2Inbox.Identifier calldata _movePlayedId, bytes calldata _movePlayedData, uint8 _x, uint8 _y) external { + if (_movePlayedId.origin != address(this)) revert IdOriginNotTicTacToe(); + ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_movePlayedId, keccak256(_movePlayedData)); + + bytes32 selector = abi.decode(_movePlayedData[:32], (bytes32)); + if (selector != MovePlayed.selector) revert DataNotMovePlayed(); +} +``` + +Similar to `acceptGame`, validation is performed and the move of their opponent is first locally recorded. +- The game identifier for lookup +- The caller is the player for this game +- The opponent event corresponds to the same game +- Ordering is enforced by ensuring that the supplied event is always forward progressing. + +```solidity +(uint256 chainId, uint256 gameId,, uint8 oppX, uint8 oppY) = abi.decode(_movePlayedData[32:], (uint256, uint256, address, uint8, uint8)); + +// Game was instantiated for this player & the move is for the same game +Game storage game = games[chainId][gameId][msg.sender]; +if (game.player != msg.sender) revert GameNotExists(); +if (game.gameId != gameId) revert GameNotExists(); + +// The move played event is forward progressing from the last observed event +if (_movePlayedId.chainId != game.lastOpponentId.chainId) revert IdChainMismatch(); +if (_movePlayedId.blockNumber <= game.lastOpponentId.blockNumber) revert MoveNotForwardProgressing(); +game.lastOpponentId = _movePlayedId; + +// Mark the opponents move +game.moves[oppX][oppY] = 2; +game.movesLeft--; +``` + +When a move is played we check if the game has been drawn or won, determining the subsequent event to emit. + +The `makeMove` function is only callable when an opponent has a new `MovePlayed` event. Therefore, if the game is won or drawn, it cannot be progressed any further by the opponent. + +```solidity +// Make the players move +game.moves[_x][_y] = 1; +game.movesLeft--; + +// Determine the status of the game +if (_isGameWon(game)) { + emit GameWon(chainId, gameId, game.player, _x, _y); +} else if (game.movesLeft == 0) { + emit GameDraw(chainId, gameId, game.player, _x, _y); +} else { + emit MovePlayed(chainId, gameId, game.player, _x, _y); +} +``` + +## Takeaways + +Leveraging superchain interop, we can build a new type of horizontally scalable contracts that do not rely on hub/spoke messaging with relayers. + +1. As new chains are added to the superchain, this contract can be installed by anyone and immediately playable with no necessary code changes. The frontend simply needs to react the addition of a new chain + +2. The concept of a "chain" can be completely abstracted away from the user. When connecting their wallet, the frontend can simply pick the chain which the user has funds on with the lowest gas fees. + +3. Event reading enables a new level of composability for cross-chain interactions! Imagine a prediciton market contract that resolves based on the outcome of a TicTacToe game via the `GameWon` or `GameLost` event without the need for a trusted oracle, nor permission or native integration with the TicTacToe contract. \ No newline at end of file diff --git a/docs/src/guides/interop/relay-using-viem.md b/docs/src/guides/interop/viem.md similarity index 99% rename from docs/src/guides/interop/relay-using-viem.md rename to docs/src/guides/interop/viem.md index 3e2472bb..ad7648a3 100644 --- a/docs/src/guides/interop/relay-using-viem.md +++ b/docs/src/guides/interop/viem.md @@ -1,5 +1,5 @@ -# Using `viem` to relay interop messages (TypeScript) +# Viem to send and relay interop messages This guide describes how to use [`viem`](https://viem.sh/) to send and relay interop messages using the `L2ToL2CrossDomainMessenger` diff --git a/docs/src/guides/interop/writing-contract-using-l2cdm.md b/docs/src/guides/interop/writing-contract-using-l2cdm.md deleted file mode 100644 index da03cbd5..00000000 --- a/docs/src/guides/interop/writing-contract-using-l2cdm.md +++ /dev/null @@ -1 +0,0 @@ -# Writing cross-chain contract using L2ToL2CrossDomainMessenger diff --git a/examples/tictactoe/README.md b/examples/tictactoe/README.md index 8c745e5f..61b62c11 100644 --- a/examples/tictactoe/README.md +++ b/examples/tictactoe/README.md @@ -1,11 +1,61 @@ -# TicTacToe +# TicTacToe Frontend -A simple tic-tac-toe game built with React, Wagmi, and Supersim. +The frontend implementation for the horizontally scalable multichain TicTacToe game built on the superchain interop specification via Supersim. + +See the relevant section in the Supersim [docs](http://supersim.pages.dev/guides/interop/cross-chain-event-reads-tictactoe.html) for how the contract was designed. ## Overview -This project is a simple tic-tac-toe game that allows two players to play against each other on the Ethereum blockchain using Supersim. The game is built with React, Wagmi, and Supersim. +This frontend streams all game events across the superchain and presents a single view to the connected wallet. Since this frontend was built for local demonstrative purposes, it simply streams all game events from the genesis block on every refresh - *not a good idea for production*. + +When run in vanilla mode, Supersim instantiates two L2 chains, OP Chain A (901) and OP Chain B (902). The frontend represents OP Chain A as the OP Mainnet network and OP Chain B as the Mode network just for demonstrative purposes as to how this might look in production. ## Getting Started -TODO \ No newline at end of file +### 1. Deploy The TicTacToe Contract + +We'll use the private key of the last pre-funded account to deploy the contracts. **The deployer has no special privileges** + +```bash +cd contracts + +# Deploy to OP Chain A +forge script script/tictactoe/Deploy.s.sol --rpc-url http://localhost:9545 --broadcast --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 + +# Deploy to OP Chain B +forge script script/tictactoe/Deploy.s.sol --rpc-url http://localhost:9546 --broadcast --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 +``` + +The forge script will log the deployed deterministic contract address. + +```bash +... +Script ran successfully.. +== Logs == + Deployed at: 0x14eFE545C60FB3b65B9eeb23E22b8013908e48Bc + +... +``` + +### 2. Run The Frontend + +Ensure the contract address matches the constant within `examples/tictactoe/src/constants/tictactoe.ts` + +```bash +cd examples/tictactoe +pnpn i && pnpm run dev +``` + +The frontend will be available at `http://localhost:5173`. + +## Implementation Notes + +1. The "backend" is implemented as a React hook within [src/hooks/useGame.ts](./src/hooks/useGame.ts). Regardless of the connected wallet, this hook syncs all past events and listens for all new events emitted by the TicTacToe contract. All games can be looked up by the mapping provided from this hook. the [src/hooks/usePlayerGames.ts](./src/hooks/usePlayerGames.ts) hook filters this down to the games relevant to the connected wallet. + +2. Each Game is looked up per player via the GameKey, `--`. As a game progresses, the latest action, the emitted event, made by each player is locally stored. This allows provides a quick lookup for the player to make their move by simply querying the Game state by their opponents address. + +## Improvements + +1. **Install the contract directly from the frontend**. Since this contract can be permissionlessly deployed, we can envision users simply *installing* this game on new chains added into the superchain. From this, we can design our frontend to be flexible and react to new chains dynamically. + +2. **Chain abstraction**. The game allows players to *start* a game from any chain and must be finished in the same manner. However a player can do this simultaneously on multiple chains. How can we better design this in the frontend and provide a seamless experience initiating transactions between chains without requiring constant network switching. \ No newline at end of file