Cartesi Rollups version: 0.9.x
This example shows how to use the Commit-Reveal logic to implement an Odds and evens game. Such approach may be replicated for any other game where "simultaneous" or secret information is exchanged.
In the Commit-Reveal scheme, each player submits an "action commitment" either by hashing the action with a random number or encrypting the message. After all players have sent their commitments, they reveal their actions by providing their action accompanied by the random number or encryption key previously used.
In this scheme, players cannot deny the actions they have commited to, and they cannot reveal their actions before the other players have commited their own actions.
This scheme has two potential cases that players may misbehave:
- a player fails to present enough information to reveal their action; or
- a player don't send his reveal message.
The DApp back-end is written in Python.
Known limitations of this implementation:
- The DApp supports multiple concurrent games, but only one running game per player pair
- Commitments are done by SHA512/256 (SHA512 with 256 truncation) to avoid length extension attacks
DISCLAIMERS
This is not a final product and should not be used as one.
A regular game may be represented by the following message flow:
- First player commits:
opponent [second player pk] parity [odds or evens] commit [sha512/256 of chosen number-nonce]
- Second player commits:
opponent [first player pk] commit [sha512/256 of chosen number-nonce]
- First player reveals:
opponent [second player pk] action [chosen number] nonce [nonce]
- Second player reveals:
opponent [first player pk] action [chosen number] nonce [nonce]
An alternative game flow can skip the second player commitment, as it not necessary to hide the information:
- First player commits:
opponent [second player pk] parity [odds or evens] commit [sha512/256 of chosen number-nonce]
- Second player reveals:
opponent [first player pk] action [chosen number]
- First player reveals:
opponent [second player pk] action [chosen number] nonce [nonce]
Alternative messages include:
- First player cancels match:
opponent [second player pk] cancel
- First or second player aborts the match because the other player is not answering:
opponent [opponent player pk] timeout
All accepted fields may be found in the table below:
Field | Description | Usage constraints |
---|---|---|
opponent (or o) |
Opposing player address | Mandatory for all messages |
parity (or p) |
Parity chosen (odd or even) by th first player | Only for the first player commitment |
commit (or c) |
Commitment of the player action given by the sha512/256 hash of the chosen pair number-nonce | For the first and second player commitments |
action (or a) |
Chosen number by either player | For the first and second player reveals |
nonce (or n) |
Random number generated to avoid guessing the action | For the first and second player reveals |
cancel (or x) |
Abort match before the other player commits their action | Only for the first player before the second player commits. It has no effect otherwise |
timeout (or t) |
Claim timeout when one player had already sent their reveal but the other player hasn't after a specified period of time | For the first and second players after their own reveals. It has no effect otherwise |
Please refer to the rollups-examples requirements.
To build the application, run the following command:
docker buildx bake -f docker-bake.hcl -f docker-bake.override.hcl --load
To start the application, execute the following command:
docker compose -f docker-compose.yml -f docker-compose.override.yml up
The application can afterwards be shut down with the following command:
docker compose -f docker-compose.yml -f docker-compose.override.yml down -v
In this section we present a way to interact with the application using Linux command line tools such as shasum
to generate the commitment hash. Optionally, you could use a online tool such as SHA512_256.
You can use the Foundry's command-line tool for performing Ethereum RPC calls cast to interact with the DApp. Also, you can use curl to get outputs, and jq and xxd to process them.
First, go to a separate terminal window and define some variables:
# Dapp address
INPUT_BOX_ADDRESS=0x5a723220579C0DCb8C9253E6b4c62e572E379945
# Dapp address
DAPP_ADDRESS=0x142105FC8dA71191b3a13C738Ba0cF4BC33325e2
# Add wallets information (local network deployment):
PLAYER1=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
PLAYER1_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PLAYER2=0x70997970C51812dc3A010C7d01b50e0d17dc79C8
PLAYER2_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
# Create the commitment for the first player as follows:
PLAYER1_ACTION=5
PLAYER1_NONCE=$RANDOM
# Do the same For the second player:
PLAYER2_ACTION=0
PLAYER2_NONCE=$RANDOM
After that, send the first player commitment:
cast send $INPUT_BOX_ADDRESS \
"addInput(address,bytes)" $DAPP_ADDRESS \
$(xxd -c10000 -p <<< "opponent $PLAYER2 parity odd commit $(echo -n "$PLAYER1_ACTION-$PLAYER1_NONCE" | shasum -a 512256 | head -c 64)" ) \
--rpc-url http://localhost:8545 --from $PLAYER1 --private-key $PLAYER1_KEY
Then, send the second player commitment:
cast send $INPUT_BOX_ADDRESS \
"addInput(address,bytes)" $DAPP_ADDRESS \
$(xxd -c10000 -p <<< "o $PLAYER1 c $(echo -n "$PLAYER2_ACTION-$PLAYER2_NONCE" | shasum -a 512256 | head -c 64)" ) \
--rpc-url http://localhost:8545 --from $PLAYER2 --private-key $PLAYER2_KEY
After the commitment, send the reveal for the first player:
cast send $INPUT_BOX_ADDRESS \
"addInput(address,bytes)" $DAPP_ADDRESS \
$(xxd -c10000 -p <<< "opponent $PLAYER2 action $PLAYER1_ACTION nonce $PLAYER1_NONCE" ) \
--rpc-url http://localhost:8545 --from $PLAYER1 --private-key $PLAYER1_KEY
And the reveal for the second player:
cast send $INPUT_BOX_ADDRESS \
"addInput(address,bytes)" $DAPP_ADDRESS \
$(xxd -c10000 -p <<< "o $PLAYER1 a $PLAYER2_ACTION n $PLAYER2_NONCE" ) \
--rpc-url http://localhost:8545 --from $PLAYER2 --private-key $PLAYER2_KEY
In order to verify the reports generated by all inputs, run the command:
curl -s -H 'Content-Type: application/json' -X POST http://localhost:4000/graphql -d '{"query": "query { reports { edges { node { payload }}}}"}' | jq -r '.data.reports.edges[] | .node.payload,"0a0a"' | xxd -r -p
The response should look like this:
Game(e7fe733917) | Phase(COMMIT) | Last(CREATED - 2023-07-05T08:46:36) | Player(0xf39f...2266) Parity(odd) Ts(2023-07-05T08:46:36): Commit(7a7fa724fdce5a7f3408b99a73f48e565ceef6695da33f7a8f46fa61ef843ec1) | Player(0x7099...79c8) Parity(even): No Commit
Game(e7fe733917) | Phase(REVEAL) | Last(COMMIT ADDED - 2023-07-05T08:46:46) | Player(0xf39f...2266) Parity(odd) Ts(2023-07-05T08:46:36): Commit(7a7fa724fdce5a7f3408b99a73f48e565ceef6695da33f7a8f46fa61ef843ec1) | Player(0x7099...79c8) Parity(even) Ts(2023-07-05T08:46:46): Commit(ed01f128bb4157ff084ec7b5390bef2cb1b085c619d1b83a176a8f1d01ed2e03)
Game(e7fe733917) | Phase(REVEAL) | Last(REVEAL ADDED - 2023-07-05T08:46:51) | Player(0xf39f...2266) Parity(odd) Ts(2023-07-05T08:46:51): Commit(7a7fa724fdce5a7f3408b99a73f48e565ceef6695da33f7a8f46fa61ef843ec1), Action(5), Nonce(31791) | Player(0x7099...79c8) Parity(even) Ts(2023-07-05T08:46:46): Commit(ed01f128bb4157ff084ec7b5390bef2cb1b085c619d1b83a176a8f1d01ed2e03)
WINNER[normal victory](0xf39f...2266) | Game(e7fe733917) | Phase(FINISH) | Last(REVEAL ADDED - 2023-07-05T08:47:01) | Player(0xf39f...2266) Parity(odd) Ts(2023-07-05T08:46:51): Commit(7a7fa724fdce5a7f3408b99a73f48e565ceef6695da33f7a8f46fa61ef843ec1), Action(5), Nonce(31791) | Player(0x7099...79c8) Parity(even) Ts(2023-07-05T08:47:01): Commit(ed01f128bb4157ff084ec7b5390bef2cb1b085c619d1b83a176a8f1d01ed2e03), Action(0), Nonce(7633)
The report message has the following format:
WINNER[<type>](<winner>) | Game(<id>) | Phase(<phase>) | Last(<last action - ts>) | Player(<addressA>) Parity(<parityA>) Ts(<tsA>): Commit(<commitA>), Action(<actionA>), Nonce(<nonceA>) | Player(<addressB>) Parity(<parityB>) Ts(<tsB>): Commit(<commitB>), Action(<actionB>), Nonce(<nonceB>)
So the obtained messages are:
Winner | Game | Phase | Last Action | Player A | Player B |
---|---|---|---|---|---|
Game(e7fe733917) | Phase(COMMIT) | Last(CREATED - 2023-07-05T08:46:36) | Player(0xf39f...2266) Parity(odd) Ts(2023-07-05T08:46:36): Commit(7a7fa724fdce5a7f3408b99a73f48e565ceef6695da33f7a8f46fa61ef843ec1) | Player(0x7099...79c8) Parity(even): No Commit | |
Game(e7fe733917) | Phase(REVEAL) | Last(COMMIT ADDED - 2023-07-05T08:46:46) | Player(0xf39f...2266) Parity(odd) Ts(2023-07-05T08:46:36): Commit(7a7fa724fdce5a7f3408b99a73f48e565ceef6695da33f7a8f46fa61ef843ec1) | Player(0x7099...79c8) Parity(even) Ts(2023-07-05T08:46:46): Commit(ed01f128bb4157ff084ec7b5390bef2cb1b085c619d1b83a176a8f1d01ed2e03) | |
Game(e7fe733917) | Phase(REVEAL) | Last(REVEAL ADDED - 2023-07-05T08:46:51) | Player(0xf39f...2266) Parity(odd) Ts(2023-07-05T08:46:51): Commit(7a7fa724fdce5a7f3408b99a73f48e565ceef6695da33f7a8f46fa61ef843ec1), Action(5), Nonce(31791) | Player(0x7099...79c8) Parity(even) Ts(2023-07-05T08:46:46): Commit(ed01f128bb4157ff084ec7b5390bef2cb1b085c619d1b83a176a8f1d01ed2e03) | |
WINNER[normal victory](0xf39f...2266) | Game(e7fe733917) | Phase(FINISH) | Last(REVEAL ADDED - 2023-07-05T08:47:01) | Player(0xf39f...2266) Parity(odd) Ts(2023-07-05T08:46:51): Commit(7a7fa724fdce5a7f3408b99a73f48e565ceef6695da33f7a8f46fa61ef843ec1), Action(5), Nonce(31791) | Player(0x7099...79c8) Parity(even) Ts(2023-07-05T08:47:01): Commit(ed01f128bb4157ff084ec7b5390bef2cb1b085c619d1b83a176a8f1d01ed2e03), Action(0), Nonce(7633) |
Unlike reports, notices are messages that once the epoch finishes, Cartesi Rollups framework create a proof so the information may be used in other contracts. The Odds and Evens DApp generates a notice once a game finishes to inform the winner.
To generate the notice proof, you can advance time in the local network and force the end of the epoch with the following command:
curl -H "Content-Type: application/json" -X POST --data '{"id":1337,"jsonrpc":"2.0","method":"evm_increaseTime","params":[864000]}' http://localhost:8545
Then, to get the notice with the proof, you can run:
curl -s -H 'Content-Type: application/json' -X POST http://localhost:4000/graphql -d '{"query": "query { notices { edges { node { payload proof { context validity { inputIndex outputIndex machineStateHash outputHashesRootHash noticesEpochRootHash vouchersEpochRootHash keccakInHashesSiblings outputHashesInEpochSiblings }}}}}}"}' | jq -r -c '.data.notices.edges[] | .node | .payload,.proof.validity.inputIndex,.proof.validity.outputIndex,.proof.validity.outputHashesRootHash,.proof.validity.vouchersEpochRootHash,.proof.validity.noticesEpochRootHash,.proof.validity.machineStateHash,.proof.validity.keccakInHashesSiblings,.proof.validity.outputHashesInEpochSiblings,.proof.context' | xargs printf "%s ((%s,%s,%s,%s,%s,%s,%s,%s),%s)\n"
Save a single result to NOTICE_RESULT
, so you can reference it later.
As an example that uses the notice data, you can create a simple solidity contract that receives the notice payload and proof and adds to the players' score.
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;
import {Bitmask} from "@cartesi/util/contracts/Bitmask.sol";
import "@cartesi/rollups/contracts/dapp/ICartesiDApp.sol";
import "@cartesi/rollups/contracts/library/LibOutputValidation.sol";
contract Score {
using Bitmask for mapping(uint256 => uint256);
mapping(uint256 => uint256) noticeBitmask;
mapping (address => uint) public nGames;
mapping (address => uint) public nWins;
address dappAddress;
constructor(address _dappAddress) {
dappAddress = _dappAddress;
}
function addScore(bytes calldata _payload, Proof calldata _v) public {
ICartesiDApp dapp = ICartesiDApp(dappAddress);
// validate notice
dapp.validateNotice(_payload,_v);
// check if notice has been processed
uint256 noticePosition = LibOutputValidation.getBitMaskPosition(_v.validity.outputIndex,_v.validity.inputIndex);
require(!noticeBitmask.getBit(noticePosition),"notice re-processing not allowed");
// process notice
(address playerA, address playerB, address winner) = abi.decode(_payload,(address, address, address));
nGames[playerA] += 1;
nGames[playerB] += 1;
nWins[winner] += 1;
// mark it as processed
noticeBitmask.setBit(noticePosition, true);
}
function getScore(address player) public view returns (uint) {
if (nGames[player] == 0) return 0;
return 100 * nWins[player] / nGames[player];
}
}
After deploying the contract (and obtain the CONTRACT_ADDRESS
), you can interact with:
cast send $CONTRACT_ADDRESS \
"addScore(bytes,((uint64,uint64,bytes32,bytes32,bytes32,bytes32,bytes32[],bytes32[]),bytes))" $NOTICE_RESULT \
--rpc-url http://localhost:8545 --from $PLAYER1 --private-key $PLAYER1_KEY
Then you can check the scores with:
cast call $CONTRACT_ADDRESS "getScore(address)" $PLAYER1 \
--rpc-url http://localhost:8545 \
| xargs printf "Player ($PLAYER1) Score: %d\n"