Zero Knowledge resuelve el problema de cualquier tipo de juego que requiera privacidad para jugarse on-chain. Por ejemplo, permite jugar poker en on-chain, manteniendo privadas las cartas de los jugadores.
En este tutorial, implementaremos el clásico juego de Buscaminas pero on-chain. El creador del juego, que llamaremos Game Master o GM, esconderá bombas en un mapa, y si un jugador se para en una, el GM podrá demostrarlo, haciendo que el jugador "explote".
Aprenderemos a crear un juego donde las bombas están ocultas en un estado privado ZK. Si un jugador se para en una, "explotará".
Este tutorial es parte de una serie de tres partes sobre ZK y mundos autónomos. Si aún no lo has hecho, asegúrate de revisar mis publicaciones anteriores.
Tabla de contenido
- Crea un proyecto de Mud
- 1. El estado
- 2. El circuito
- 3. Los contratos
- 4. El cliente
- 5. Un poco de carpintería
- 6. El prover
- Llevando tus conocimientos más adelante
Crea un proyecto de Mud
Usaremos Node v20 (>=v18 debería estar bien), pnpm y foundry. Si no los tienes instaladas, te dejo los comandos.
Instalación de dependencias
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvm install 20
curl -L https://foundry.paradigm.xyz | bash export PATH=$PATH:~/.foundry/bin
sudo npm install -g pnpm
Una vez instalados crea un nuevo proyecto de Mud.
pnpm create mud@latest tutorial
cd tutorial
Durante la instalación selecciona phaser.
1. Define el estado público
Todos podrán ver las posiciones de los demás de manera pública on-chain. Sin embargo, las posiciones de las bombas se almacenarán off-chain, junto con un commitment y un contrato verificador para probar la validez de la computción privada.
packages/contracts/mud.config.ts
import { defineWorld } from "@latticexyz/world";
export default defineWorld({
namespace: "app",
enums: {
Direction: [
"Up",
"Down",
"Left",
"Right"
]
},
tables: {
Player: {
schema: {
player: "address",
x: "int32",
y: "int32",
isDead: "bool",
},
key: ["player"]
},
ZKState: {
schema: {
bombsCommitment: "uint32",
circomVerifier: "address"
},
key: [],
},
}
});
2. Mantén la computación privada en el Circuito
El circuito manejará todos los cálculos privados. Para este tutorial, usaremos Circom.
Vamos a crear la estructura de carpetas de ZK, siguiendo una estructura de archivos muy similar a la de un proyecto típico de Mud.
mkdir packages/zk
mkdir packages/zk/circuits
mkdir packages/zk/prover
mkdir packages/zk/prover/zk_artifacts
El circuito detonateBomb
es capaz de producir pruebas ZK de que un jugador se ha parado en una de las tres bombas colocadas secretamente en el mapa sin revelar las posiciones del resto de las bombas. Esto se logra devolviendo 1
como la señal de result
si un jugador está en la misma posición de una bomba, de lo contrario, se devuelve 0
. Nótese la señal de commitment
, que es el hash de todas las posiciones de las bombas. Este se almacenará posteriormente en la cadena para asegurarse de que el GM no pueda modificar las posiciones de las bombas más adelante en el juego.
packages/zk/circuits/detonateBomb.circom
pragma circom 2.0.0;
include "circomlib/circuits/poseidon.circom";
include "circomlib/circuits/comparators.circom";
template commitmentHasher() {
signal input bomb1_x;
signal input bomb1_y;
signal input bomb2_x;
signal input bomb2_y;
signal input bomb3_x;
signal input bomb3_y;
signal output commitment;
component poseidonComponent;
poseidonComponent = Poseidon(6);
poseidonComponent.inputs[0] <== bomb1_x;
poseidonComponent.inputs[1] <== bomb1_y;
poseidonComponent.inputs[2] <== bomb2_x;
poseidonComponent.inputs[3] <== bomb2_y;
poseidonComponent.inputs[4] <== bomb3_x;
poseidonComponent.inputs[5] <== bomb3_y;
commitment <== poseidonComponent.out;
}
template detonateBomb() {
signal input bomb1_x;
signal input bomb1_y;
signal input bomb2_x;
signal input bomb2_y;
signal input bomb3_x;
signal input bomb3_y;
signal input player_x;
signal input player_y;
signal output commitment;
signal output result;
component commitmentHasherComponent;
commitmentHasherComponent = commitmentHasher();
commitmentHasherComponent.bomb1_x <== bomb1_x;
commitmentHasherComponent.bomb1_y <== bomb1_y;
commitmentHasherComponent.bomb2_x <== bomb2_x;
commitmentHasherComponent.bomb2_y <== bomb2_y;
commitmentHasherComponent.bomb3_x <== bomb3_x;
commitmentHasherComponent.bomb3_y <== bomb3_y;
commitment <== commitmentHasherComponent.commitment;
// Comparators
signal check_bomb1_x, check_bomb1_y;
signal check_bomb2_x, check_bomb2_y;
signal check_bomb3_x, check_bomb3_y;
check_bomb1_x <== bomb1_x - player_x;
check_bomb1_y <== bomb1_y - player_y;
check_bomb2_x <== bomb2_x - player_x;
check_bomb2_y <== bomb2_y - player_y;
check_bomb3_x <== bomb3_x - player_x;
check_bomb3_y <== bomb3_y - player_y;
// Check if any of the comparisons are zero
component isz_bomb1_x = IsZero();
component isz_bomb1_y = IsZero();
component isz_bomb2_x = IsZero();
component isz_bomb2_y = IsZero();
component isz_bomb3_x = IsZero();
component isz_bomb3_y = IsZero();
isz_bomb1_x.in <== check_bomb1_x;
isz_bomb1_y.in <== check_bomb1_y;
isz_bomb2_x.in <== check_bomb2_x;
isz_bomb2_y.in <== check_bomb2_y;
isz_bomb3_x.in <== check_bomb3_x;
isz_bomb3_y.in <== check_bomb3_y;
// Aggregate results
signal match_a, match_b, match_c;
signal match_any;
match_a <== isz_bomb1_x.out * isz_bomb1_y.out;
match_b <== isz_bomb2_x.out * isz_bomb2_y.out;
match_c <== isz_bomb3_x.out * isz_bomb3_y.out;
match_any <== match_a + match_b + match_c;
component isz_final = IsZero();
isz_final.in <== 1 - match_any;
isz_final.out ==> result;
log(result);
log(commitment);
}
component main {public [player_x, player_y]} = detonateBomb();
Este circuito usa las lirberías de Poseidon
y IsZero
que es parte del set de comparator. Necesitamos instalarlas.
cd packages/zk/circuits
git clone https://github.com/iden3/circomlib.git
Ahora crea un archivo de input para probar si todo funciona bien.
packages/zk/circuits/input.json
{
"bomb1_x": 1,
"bomb1_y": 1,
"bomb2_x": 2,
"bomb2_y": 2,
"bomb3_x": 2,
"bomb3_y": 3,
"player_x": 2,
"player_y": 3
}
Compila y genera una prueba.
circom detonateBomb.circom --r1cs --wasm --sym
node detonateBomb_js/generate_witness.js detonateBomb_js/detonateBomb.wasm input.json witness.wtns
Con el input anterior, result
es igual a 1
porque el jugador caminó en la bomba 2,3
. Ademas, en la consola se imprime el commitment
. Guárdalo, lo usaremos más adelante.
Result:
1
Bombs commitment:
8613278371666841974523698252941148485158405612101680617360618409530277878563
En este demo usaremos Groth16
, así que correremos una trusted setup.
snarkjs powersoftau new bn128 12 pot12_0000.ptau -v
snarkjs powersoftau contribute pot12_0000.ptau pot12_0001.ptau --name="First contribution" -v
snarkjs powersoftau prepare phase2 pot12_0001.ptau pot12_final.ptau -v
snarkjs groth16 setup detonateBomb.r1cs pot12_final.ptau detonateBomb_0000.zkey
snarkjs zkey contribute detonateBomb_0000.zkey detonateBomb_0001.zkey --name="1st Contributor Name" -v
snarkjs zkey export verificationkey detonateBomb_0001.zkey verification_key.json
snarkjs zkey export solidityverifier detonateBomb_0001.zkey ../../contracts/src/CircomVerifier.sol
Finalmente, colocamos los artifactos en la carpeta del prover que correremos al final de este tutorial.
cp detonateBomb_js/detonateBomb.wasm ../prover/zk_artifacts/
cp detonateBomb_0001.zkey ../prover/zk_artifacts/detonateBomb_final.zkey
3. Spawnea, mueve y detona bombas en Solidity
Regresa a la raíz de tu proyecto.
cd ../../
Borra los archivos que no necesitaremos.
rm packages/contracts/src/systems/IncrementSystem.sol packages/contracts/test/CounterTest.t.sol
El sistema del juego permite a los jugadores regulares ejecutar spawn
y move
, y al GM detonar las bombas (detonateBomb
) pasando como parámetro una prueba ZK.
packages/contracts/src/systems/MyGameSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { System } from "@latticexyz/world/src/System.sol";
import { Player, PlayerData, ZKState } from "../codegen/index.sol";
import { Direction } from "../codegen/common.sol";
import { getKeysWithValue } from "@latticexyz/world-modules/src/modules/keyswithvalue/getKeysWithValue.sol";
import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol";
interface ICircomVerifier {
function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals) external view returns (bool);
}
contract MyGameSystem is System {
function spawn(int32 x, int32 y) public {
address playerAddress = _msgSender();
Player.set(playerAddress, x, y, false);
}
function move(Direction direction) public {
address playerAddress = _msgSender();
PlayerData memory player = Player.get(playerAddress);
require(!player.isDead, "Player is dead");
int32 x = player.x;
int32 y = player.y;
if(direction == Direction.Up)
y-=1;
if(direction == Direction.Down)
y+=1;
if(direction == Direction.Left)
x-=1;
if(direction == Direction.Right)
x+=1;
require(x>= -31 && x<= 31 && y>= -31 && y<= 31, "Invalid position");
Player.set(playerAddress, x, y, false);
}
function detonateBomb(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals, address playerAddress) public {
ICircomVerifier(ZKState.getCircomVerifier()).verifyProof(_pA, _pB, _pC, _pubSignals);
uint32 commitment = uint32(_pubSignals[0]);
uint32 result = uint32(_pubSignals[1]);
int32 guessX = int32(uint32(uint(_pubSignals[2])));
int32 guessY = int32(uint32(uint(_pubSignals[3])));
PlayerData memory player = Player.get(playerAddress);
require(!player.isDead, "Player already dead");
require(result == 1, "No bomb in this position");
require(player.x == guessX && player.y == guessY, "Invalid position ");
uint32 bombsCommitment = ZKState.getBombsCommitment();
require(uint32(uint(commitment)) == bombsCommitment, "Invalid commitment");
Player.setIsDead(playerAddress, true);
}
}
Al desplegar el juego, el GM necesitará almacenar el commitment
de las bombas en la cadena para no poder cambiarlo más adelante. También, notemos que desplegamos el contrato Groth16Verifier
que se generó en el paso 2.
packages/contracts/script/PostDeploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { Script } from "forge-std/Script.sol";
import { console } from "forge-std/console.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { IWorld } from "../src/codegen/world/IWorld.sol";
import { ZKState } from "../src/codegen/index.sol";
import { Groth16Verifier } from "../src/CircomVerifier.sol";
contract PostDeploy is Script {
function run(address worldAddress) external {
StoreSwitch.setStoreAddress(worldAddress);
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
uint32 bombsCommitment = uint32(uint(8613278371666841974523698252941148485158405612101680617360618409530277878563));
address circomVerifier = address(new Groth16Verifier());
ZKState.set(bombsCommitment, circomVerifier);
vm.stopBroadcast();
}
}
4. Interactúa con los usuarios a través de Phaser
Phaser es el framework de juegos que facilita las animaciones, las pulsaciones de teclas, los sonidos y los eventos. Definimos estos elementos en nuestro Game System.
packages/client/src/layers/phaser/systems/myGameSystem.ts
import { Has, defineEnterSystem, defineSystem, defineExitSystem, getComponentValueStrict } from "@latticexyz/recs";
import { PhaserLayer } from "../createPhaserLayer";
import {
pixelCoordToTileCoord,
tileCoordToPixelCoord
} from "@latticexyz/phaserx";
import { TILE_WIDTH, TILE_HEIGHT, Animations, Directions } from "../constants";
export const createMyGameSystem = (layer: PhaserLayer) => {
const {
world,
networkLayer: {
components: {
Player
},
systemCalls: {
spawn,
move
}
},
scenes: {
Main: {
objectPool,
input
}
}
} = layer;
let toggle = false;
input.pointerdown$.subscribe((event) => {
const x = event.pointer.worldX;
const y = event.pointer.worldY;
const player = pixelCoordToTileCoord({ x, y }, TILE_WIDTH, TILE_HEIGHT);
if(player.x == 0 && player.y == 0)
return;
spawn(player.x, player.y)
});
input.onKeyPress((keys) => keys.has("W"), () => {
move(Directions.UP);
});
input.onKeyPress((keys) => keys.has("S"), () => {
move(Directions.DOWN);
});
input.onKeyPress((keys) => keys.has("A"), () => {
move(Directions.LEFT);
});
input.onKeyPress((keys) => keys.has("D"), () => {
move(Directions.RIGHT);
});
input.onKeyPress((keys) => keys.has("I"), () => {
// This should not be on the client, just for demo purposes
let bombSprite1 = objectPool.get("Bomb1", "Sprite");
let bombSprite2 = objectPool.get("Bomb2", "Sprite");
let bombSprite3 = objectPool.get("Bomb3", "Sprite");
if (toggle == true) {
bombSprite1.setComponent({
id: "position",
once: (sprite1) => {
sprite1.setVisible(false);
}
})
bombSprite2.setComponent({
id: "position",
once: (sprite2) => {
sprite2.setVisible(false);
}
})
bombSprite3.setComponent({
id: "position",
once: (sprite3) => {
sprite3.setVisible(false);
}
})
} else {
bombSprite1.setComponent({
id: 'animation',
once: (sprite1) => {
sprite1.setVisible(true);
sprite1.play(Animations.Bomb);
sprite1.setPosition(1*32, 1*32);
}
})
bombSprite2.setComponent({
id: 'animation',
once: (sprite2) => {
sprite2.setVisible(true);
sprite2.play(Animations.Bomb);
sprite2.setPosition(2*32, 2*32);
}
})
bombSprite3.setComponent({
id: 'animation',
once: (sprite3) => {
sprite3.setVisible(true);
sprite3.play(Animations.Bomb);
sprite3.setPosition(2*32, 3*32);
}
})
}
toggle = !toggle;
});
defineEnterSystem(world, [Has(Player)], ({entity}) => {
const playerObj = objectPool.get(entity, "Sprite");
playerObj.setComponent({
id: 'animation',
once: (sprite) => {
sprite.play(Animations.Player);
}
})
});
defineSystem(world, [Has(Player)], ({ entity }) => {
const player = getComponentValueStrict(Player, entity);
const pixelPosition = tileCoordToPixelCoord(player, TILE_WIDTH, TILE_HEIGHT);
const playerObj = objectPool.get(entity, "Sprite");
if(player.isDead)
{
playerObj.setComponent({
id: 'animation',
once: (sprite) => {
sprite.play(Animations.Dead);
}
})
}
playerObj.setComponent({
id: "position",
once: (sprite) => {
sprite.setPosition(pixelPosition.x, pixelPosition.y);
}
})
})
};
Ahora agrega las imágenes en el directorio sprites
, organizadas en carpetas separadas, con nombres secuenciales.
packages/art/sprites/player/1.png
packages/art/sprites/player/2.png
packages/art/sprites/bomb/1.png
packages/art/sprites/dead/1.png
Y construye los tilesets automáticamente.
cd packages/art
yarn
yarn generate-multiatlas-sprites
Finalmente, define las imágenes de las animaciónes, los nombres, la duración y su comportamiento.
packages/client/src/layers/phaser/configurePhaser.ts
import Phaser from "phaser";
import {
defineSceneConfig,
AssetType,
defineScaleConfig,
defineMapConfig,
defineCameraConfig,
} from "@latticexyz/phaserx";
import worldTileset from "../../../public/assets/tilesets/world.png";
import { TileAnimations, Tileset } from "../../artTypes/world";
import { Assets, Maps, Scenes, TILE_HEIGHT, TILE_WIDTH, Animations } from "./constants";
const ANIMATION_INTERVAL = 200;
const mainMap = defineMapConfig({
chunkSize: TILE_WIDTH * 64, // tile size * tile amount
tileWidth: TILE_WIDTH,
tileHeight: TILE_HEIGHT,
backgroundTile: [Tileset.Grass],
animationInterval: ANIMATION_INTERVAL,
tileAnimations: TileAnimations,
layers: {
layers: {
Background: { tilesets: ["Default"] },
Foreground: { tilesets: ["Default"] },
},
defaultLayer: "Background",
},
});
export const phaserConfig = {
sceneConfig: {
[Scenes.Main]: defineSceneConfig({
assets: {
[Assets.Tileset]: {
type: AssetType.Image,
key: Assets.Tileset,
path: worldTileset,
},
[Assets.MainAtlas]: {
type: AssetType.MultiAtlas,
key: Assets.MainAtlas,
// Add a timestamp to the end of the path to prevent caching
path: `/assets/atlases/atlas.json?timestamp=${Date.now()}`,
options: {
imagePath: "/assets/atlases/",
},
},
},
maps: {
[Maps.Main]: mainMap,
},
sprites: {
},
animations: [
{
key: Animations.Player,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 2,
frameRate: 3,
repeat: -1,
prefix: "sprites/player/",
suffix: ".png",
},
{
key: Animations.Dead,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 12,
repeat: -1,
prefix: "sprites/dead/",
suffix: ".png",
},
{
key: Animations.Bomb,
assetKey: Assets.MainAtlas,
startFrame: 1,
endFrame: 1,
frameRate: 12,
repeat: -1,
prefix: "sprites/bomb/",
suffix: ".png",
},
],
tilesets: {
Default: {
assetKey: Assets.Tileset,
tileWidth: TILE_WIDTH,
tileHeight: TILE_HEIGHT,
},
},
}),
},
scale: defineScaleConfig({
parent: "phaser-game",
zoom: 1,
mode: Phaser.Scale.NONE,
}),
cameraConfig: defineCameraConfig({
pinchSpeed: 1,
wheelSpeed: 1,
maxZoom: 3,
minZoom: 1,
}),
cullingChunkSize: TILE_HEIGHT * 16,
};
5. Un poco de carpintería
Configura todas las variables y funciones que el cliente necesita para tener visibilidad de las funciones web3 definidas en los contratos.
packages/client/src/layers/phaser/constants.ts
export enum Scenes {
Main = "Main",
}
export enum Maps {
Main = "Main",
}
export enum Animations {
Player = "Player",
Dead = "Dead",
Bomb = "Bomb",
}
export enum Directions {
UP = 0,
DOWN = 1,
LEFT = 2,
RIGHT = 3,
}
export enum Assets {
MainAtlas = "MainAtlas",
Tileset = "Tileset",
}
export const TILE_HEIGHT = 32;
export const TILE_WIDTH = 32;
packages/client/src/layers/phaser/systems/registerSystems.ts
import { PhaserLayer } from "../createPhaserLayer";
import { createCamera } from "./createCamera";
import { createMapSystem } from "./createMapSystem";
import { createMyGameSystem } from "./myGameSystem";
export const registerSystems = (layer: PhaserLayer) => {
createCamera(layer);
createMapSystem(layer);
createMyGameSystem(layer);
};
packages/client/src/mud/createSystemCalls.ts
import { getComponentValue } from "@latticexyz/recs";
import { ClientComponents } from "./createClientComponents";
import { SetupNetworkResult } from "./setupNetwork";
import { singletonEntity } from "@latticexyz/store-sync/recs";
export type SystemCalls = ReturnType<typeof createSystemCalls>;
export function createSystemCalls(
{ worldContract, waitForTransaction }: SetupNetworkResult,
{ Player }: ClientComponents,
) {
const spawn = async (x: number, y: number) => {
const tx = await worldContract.write.app__spawn([x, y]);
await waitForTransaction(tx);
return getComponentValue(Player, singletonEntity);
};
const move = async (direction: number) => {
const tx = await worldContract.write.app__move([direction]);
await waitForTransaction(tx);
return getComponentValue(Player, singletonEntity);
}
return {
spawn, move
};
}
Ahora estamos listos para lanzar el servidor. Regresa a la carpeta raíz y despliega el servidor.
cd ../../
pnpm dev
6. Corre el prover
MUD tiene su propio sistema de logs (conocido como eventos en Solidity) donde emite por defecto un Store_SetRecord
cada vez que se modifica un registro. Esto es muy conveniente para nosotros pues probador escuchará todos los eventos de movimiento de los jugadores on-chain y cuando detecte que un jugador hizo contacto con una bomba la detonará. Esto es posible pues solo él tiene el conocimiento de las bombas ocultas.
Comencemos creando un nuevo proyecto npm e instalando las dependencias.
cd packages/zk/prover/
npm init -y
npm install express ethers snarkjs
Ahora crea el archivo de lógica del servidor en Node.js. Usaremos un servidor express
muy simple, ethers.js
se encargará de todos los requisitos web3, y snarkjs
producirá las pruebas.
packages/zk/prover/server.js
const express = require('express');
const { ethers } = require('ethers');
const fs = require('fs');
const snarkjs = require('snarkjs');
const contractAddressWorld = "0x8d8b6b8414e1e3dcfd4168561b9be6bd3bf6ec4b";
const privateKey = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
const playerTableId = "0x74626170700000000000000000000000506c6179657200000000000000000000";
const bombPositions = JSON.parse(fs.readFileSync('bombs.json', 'utf-8'));
const app = express();
const PORT = 8080;
const provider = new ethers.JsonRpcProvider('http://localhost:8545');
const contractABI = [
"function detonateBomb(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals, address playerAddress)",
];
const contractABIWorld = [
"event Store_SetRecord(bytes32 indexed tableId, bytes32[] keyTuple, bytes staticData, bytes32 encodedLengths, bytes dynamicData)",
"function app__detonateBomb(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[4] calldata _pubSignals, address playerAddress)"
];
const wallet = new ethers.Wallet(privateKey, provider);
const contractWorld = new ethers.Contract(contractAddressWorld, contractABIWorld, wallet);
function decodeRecord(hexString) {
if (hexString.startsWith('0x')) {
hexString = hexString.slice(2);
}
const xHex = hexString.slice(0, 8);
const yHex = hexString.slice(8, 16);
const isDeadHex = hexString.slice(16, 18);
const x = parseInt(xHex, 16) | 0;
const y = parseInt(yHex, 16) | 0;
const isDead = parseInt(isDeadHex, 16) != 0;
return { x, y, isDead };
}
contractWorld.on("Store_SetRecord", async (tableId, keyTuple, staticData, encodedLengths, dynamicData) => {
if(tableId == playerTableId)
{
let decodedRecord = decodeRecord(staticData);
let player = '0x' + keyTuple[0].replace(/^0x000000000000000000000000/, '');
if(!decodedRecord.isDead)
{
await detonateBomb(player, decodedRecord.x, decodedRecord.y);
}
}
});
async function detonateBomb(player, x, y) {
console.log(`Player move to (${x}, ${y})`);
for (const bomb of bombPositions) {
if (""+bomb.x === ""+x && ""+bomb.y === ""+y) {
try {
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
{
bomb1_x: bombPositions[0].x,
bomb1_y: bombPositions[0].y,
bomb2_x: bombPositions[1].x,
bomb2_y: bombPositions[1].y,
bomb3_x: bombPositions[2].x,
bomb3_y: bombPositions[2].y,
player_x: x,
player_y: y
},
"./zk_artifacts/detonateBomb.wasm",
"./zk_artifacts/detonateBomb_final.zkey"
);
let pA = proof.pi_a;
pA.pop();
let pB = proof.pi_b;
pB.pop();
let pC = proof.pi_c;
pC.pop();
if (publicSignals[1] == "1") {
const tx = await contractWorld.app__detonateBomb(
pA,
pB,
pC,
publicSignals,
player
);
console.log('Transaction:', tx);
}
} catch (error) {
console.error("Error generating or verifying proof:", error);
}
}
}
}
app.get('/', (req, res) => {
res.send('Server is running');
});
app.listen(PORT, async () => {
console.log(`Server is listening on port ${PORT}`);
});
Necesitarás la tableId
de tu tabla Player
. Esta la puedes conseguir en la UI.
La data privada de las bombas estará almacenada en el un archivo json.
packages/zk/prover/bombs.json
[
{"x": 1, "y": 1},
{"x": 2, "y": 2},
{"x": 2, "y": 3}
]
Para ejecutar tu prover, necesitarás los addresses de MyGameSystem
y World
, además de una clave privada de una wallet con ETH.
Obtén el address de World
desde tu terminal, la podrás ver cuando los contratos estén lanzados. Colócala en la variable de entorno CONTRACT_ADDRESS_WORLD
.
Ahora estás listo para iniciar el prover.
node server.js
Si un jugador pisa una bomba, el prover lo detectará y detonará la bomba on-chain.
Llevando tus conocimientos un paso más adelante
Si estás familiarizado con los juegos on-chain, podrías preguntarte: ¿No es posible un juego de Buscaminas con un esquema tradicional de commit-reveal? Y sí, eso es correcto, puedes hacer un Buscaminas haciendo un commit de las posiciones de las bombas en un, por ejemplo, mapping(uint bombId => bytes32 commitment)
. Sin embargo, aunque algunas complicaciones del esquema commit-reveal pueden resolverse por sí mismas, como aplicar un módulo %
al compromiso para prevenir ataques de fuerza bruta, ZK introduce nuevas capacidades donde veo mucho potencial. Aquí comparto algunas para que puedas inspirarte y tener una idea de algunas de las posibilidades.
1. Restricctiones a los Commitments
Cuando publicas un commit normal, no hay forma de aplicarle restricciones. En ZK, puedes, por ejemplo, definir los límites de un mapa, ser específico sobre la cantidad y las características de los datos ocultos. Por ejemplo, hay una mayor probabilidad de encontrar mineral en las montañas o peces en un río. Todo esto puede definirse en el circuito.
2. Optimización de Datos
ZK permite revelar porciones de un commit. Esto significa que se guardan una menor cantidad de datos on-chain. Sí, verificar las pruebas ZK es costoso a nivel de ejecución, pero en L2s la ejecución es barata y los datos son costosos, incluso con danksharding y plasma. Por eso, entornos de ejecución baratos como Redstone son ideales para este tipo de juegos si se está dispuesto a sacrificar un poco de seguridad.
3. Más Construcciones ZK
En este tutorial exploramos un escenario PvE, donde un jugador interactúa con un entorno en situaciones adversariales. Donde todos los cálculos privados off-chain son comprobables y asegurados on-chain. Ahora imagina escenarios PvP u otras construcciones ZK aplicadas a otros tipos de juegos.
En futuras guías, me gustaría explorar otras construcciones ZK, como la consumo privado de ítems o acciones privadas publicando pruebas de inclusión en un árbol Merkle. ¡Así que mantente atento!
¡Gracias por leer esta guía!
Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.
Top comments (0)