DEV Community

Ahmed Castro
Ahmed Castro

Posted on

Cómo crear una dApp en Tiempo Real en Ethereum sin código espagueti

Dediqué dos semanas construyendo una dApp en tiempo real, y este artículo resume cómo tuve que repensar la arquitectura, el diseño y la experiencia de usuario para una blockchain con bloques de 10ms.

La dApp que construimos con un amigo es un juego de carta alta simple: el jugador y la casa sacan una carta y gana la más alta. El reto es generar números aleatorios 100% on-chain con la menor latencia posible.

war game

¿Un Casino?

Sí!

Hay algo que me gusta de los casinos. Entiendo que los jugadores deben ser responsables y que pueden ser peligrosos, pero aún así, simplemente me gustan.

De hecho, la primera aplicación blockchain que lancé fue un casino.

dogecoin casino game

Eso fue en 2013, era un backend en Ruby on Rails conectado a un nodo local de Dogecoin que corría en mi máquina.

No existían los contratos inteligentes, así que decidí bajarlo porque no tenía forma de probar que operaba de manera justa.

Soy de un país del tercer mundo donde construir cosas es difícil, y en ese entonces era aún más complicado. Ese proyecto fue muy significativo para mí porque fue la primera vez que monetizaba mi trabajo, y también porque me permitió conectar con mucha gente a través de IRC. Bonitos recuerdos!

Después aprendí sobre los smart contract de Ethereum y decidí intentarlo de nuevo, esta vez usando Chainlink VRF.

breadacle game the chainlink vrf casino

Mi segundo intento fue Breadacle, un juego de lanzar una moneda donde apuestas si el pan sale tostado o quemado.

Ahora podía demostrar que mi juego es justo, que no estaba haciendo trampa pero era demasiado lento. También era caro, y tenía a los oráculos como dependencia, tenía que confiar en ellos.

Construyendo un juego justo y rápido

Hoy, con blockchain baja latencia, pude construir un juego rápido, verificable y sin oráculos.

Esta vez lo hice con un esquema de Commit-Reveal, un proceso de 3 transacciones que deben ejecutarse muy rápido.

commit reveal scheme behind war game

Actualmente nuestro demo tiene una latencia promedio de 1.3s por partida. Esto significa 3 transacciones ocurrieron en 1.3 segundos, lo cual nos parece que está bien, pero no es suficiente. Creemos que podemos bajarlo a menos de 500ms con mejoras en la infraestructura de blockchain rápidas combinadas con nuestras propias optimizaciones.

En esta arquitectura, el jugador ejecuta el cliente en su browser, y la casa (o sea yo) hostea un backend que responde a cada partida de los jugadores.

Cómo funciona el Backend

Para poder servir a muchos jugadores, el backend necesita escuchar nuevas partidas, indexarlas en memoria y procesarlas en batch con Multicall.

En caso de caídas, podemos tener una base de datos centralizada como respaldo. O mejor aún, usar el estado on-chain como recuperación. A lo que me refiero es usar la blockchain como backup, lo cual me parece bastante interesante.

real time ethereum war game backend architecture

Alternativamente, si los RPCs públicos son demasiado lentos para los jugadores, podemos abrir un endpoint para consultar el estado del juego desde nuestro servidor. Esto es debatible, por un lado puede hacer que el juego sea más rápido, pero por el otro significa que la casa (yo) podría engañar al jugador para que revele su número secreto antes de que yo publique el mío.

Por ahora no hemos podido probar esto completamente porque el juego lo construimos sobre MegaETH (una cadena con bloques cada 20ms, ¡muy rápida! que en el futuro debería ser sub 10ms), pero aún no hemos conseguido acceso al WebSocket que en este momento está restringido solo a equipos internos.

Estamos listos para probarlo una vez que obtengamos esa clave WSS.

El Frontend, en mi opinión la parte más interesante

Poco a poco descubrí que construir una dApp en tiempo real es muy diferente a una dApp común o una app móvil.

Cuando hacés una dApp en tiempo real tenés que dejar de pensar en flujos de listeners/callbacks y empezar a pensar como un desarrollador de videojuegos.

real time ethereum war game frontend architecture

Como dato adicional, y un poco de autopromoción descarada: antes era desarrollador de videojuegos. Construí mi propio motor de juegos, lancé un juego en Steam con él, desarrollé un SDK en C que fue usado por decenas de miles de jugadores concurrentemente y fui entrevistado por Unreal Engine en un stream en vivo.

Así que lo más importante que noté fue que, en lugar de programar pensando en callbacks y eventos, hay que usar un gameLoop() como este:

gameLoop() {
  readState()
  processInputs()
  sendTransactions()
  processUI()
}
Enter fullscreen mode Exit fullscreen mode

Esta función procesa todo el juego y debe ser llamada continuamente a intervalos muy rápidos.

Esto es porque las apps en tiempo real, como los juegos, son muy caóticas y si intentas manejar todo con callbacks como en una dApp normal, vas a terminar con código espagueti y una UX con glitches.

Esto aplica no solo a juegos, también a DeFi, redes sociales, identidad, etc.


Cosas a tener en cuenta:

1. Leer el estado solo una vez por tick

Un tick es una vuelta del gameLoop() y deberías hacer solo una llamada call() a Ethereum. Cada llamada toma de 100ms a 300ms, son demasiado lentas.

Esto incluye verificar el nonce y el gasPrice. Así que no uses web3.js, ethers o viem — construye las transacciones manualmente. Lleva el nonce tú mismo y lee el gasPrice solo una vez cada 5 minutos.

Considera escribir una función que te devuelva toda la información que necesitas en cada tick. Me gusta llamarla “Llamada Singleton” y la mía se ve así:

function getGameState(address player) external view returns (
  uint player_balance,
  State gameState,
  bytes32 playerCommit,
  bytes32 houseHash,
  uint256 gameId,
  GameResult[] memory recentHistory
)
Enter fullscreen mode Exit fullscreen mode

Devuelvo el balance del jugador para no tener que llamarlo aparte, y los últimos 10 juegos para mostrarlos en el frontend. No es elegante, pero es funcional. Alternativamente, puedes usar Multicall.

2. Buffer de inputs

Cuando el usuario hace click, no reacciones de inmediato. Guarda la acción en una lista y procésala más tarde.

Este concepto es muy común en juegos de pelea, donde necesitas presionar combinaciones precisas.

fighting game input buffer

Mi juego no es tan complejo, pero aún así, en JS no tenemos control sobre los threads. Así que puedes:
a. implementar un buffer de inputs
b. entrar al infierno de los thread locks

Recomiendo empezar con una lista simple y procesar solo una acción por tick. Si tu app se siente lenta, puede ser que la chain que estás usando no sea lo suficientemente rápida.

3. Wallet en localStorage

Obviamente no quieres que el usuario tenga que confirmar cada transacción.

Así que genera una llave privada en el navegador:

function generateWallet() {
  const account = web3.eth.accounts.create();
  localStorage.setItem('localWallet', JSON.stringify({
    address: account.address,
    privateKey: account.privateKey
  }));
  return account;
}
Enter fullscreen mode Exit fullscreen mode

Esto crea una wallet y la guarda en el navegador de manera que nadie que no sea tu dApp pueda accederlas. Está de más decir que esto no es para almacenar fondos a largo plazo pues si se borran los datos del navegador también se pierden los fondos.

En mi casino de 3 transacciones, puedo incluso revelar el número aleatorio automáticamente, sin que el usuario haga clic.

Esto ya lo usan juegos como Dark Forest y otros juegos de mundos autónomos.

4. Paralelización

Piensa en cómo hacer cada acción lo antes posible y cómo dar feedback al jugador rápidamente.

En mi caso, puedo iniciar un nuevo juego incluso antes de que el anterior termine.

Normalmente sería:

  1. El jugador envía el commit
  2. El jugador detecta que la casa respondió
  3. El jugador envía el reveal

Podemos paralelizar iniciando un nuevo juego antes de que el reveal esté confirmado on-chain siempre y cuando supongamos que el secuenciador ordenará todo en FIFO.

Además, cuando el jugador ya tiene el número de la casa, puede calcular el resultado sin esperar a que esté on-chain.

Esto lo llamo “Renderizado Optimista”.

Estas optimizaciones son importantes, pero como no controlamos al secuenciador, debemos implementar formas de recuperación o reintento automático si algo no ocurrió como esperamos.

¡Gracias por ver este artículo!

Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.

Top comments (0)