Tipos de transacción 24, 25, 26, 27, 28 y 39. Cuerpos, el registro de aplicaciones, los pagos y cómo verificar un resultado por tu cuenta. Transcrito de las fuentes del nodo.
Una app en ZooBC es un juego o una apuesta cuyas reglas se ejecutan en el consenso. Cada jugada es una transacción, el
tablero vive en el estado de la cadena y el pago lo realiza el protocolo. No hay ningún servidor en el que confiar, ningún
operador que pueda negarse a pagar y ningún resultado que haya que aceptar por fe: cualquiera puede reproducirlo
todo a partir del historial de la cadena, para siempre.
Este manual es la referencia para integradores: los seis tipos de transacción, los cuerpos exactos, el registro
de apps con la codificación de jugadas de cada una, cómo se mueve el dinero y cómo se obtiene y se verifica la aleatoriedad.
Todo lo que aparece aquí está transcrito de las fuentes del nodo, include/zoobc/common/types.h,
src/transaction/app_rules.cpp, y src/transaction/transaction_executor.cpp, no de las notas
de diseño, que son anteriores a la implementación y difieren de ella en algunos puntos.
1. Las tres categorías
| Categoría | Plazas | Contraparte | Aleatoriedad | Tipos |
|---|---|---|---|---|
| Individual contra la casa | 1 | el fondo de apps del protocolo | sí, semilla del bloque | 16–21 |
| Uno contra uno | 2 | otro jugador | solo si la app la usa | 1–8 |
| Grupo | 3–4 | otros jugadores | dados a partir de la semilla del bloque | 32–35 |
Las tres comparten un mismo motor: los mismos tipos de transacción, el mismo depósito en garantía del stake, el mismo plazo
por jugada y la misma regla de que una jugada es una transacción.
seats determina la categoría y se comprueba con el tipo de app. Una app individual debe tener seats == 1
y un tipo dentro de 16–31; una app multijugador debe tener seats de 2 a 4 y un tipo fuera de ese rango. Si esto
es incorrecto, se rechaza al entrar en la mempool, no en la ejecución.
2. Tipos de transacción
Seis tipos. Todos los cuerpos son little-endian; todas las cantidades son atómicas (divide entre 1e8 para obtener ZBC).
| Tipo | Nombre | Cuerpo |
|---|---|---|
| 24 | CreateApp | app_type(1) · stake_token_id(8) · stake_amount(8) · seats(1) · params_len(2) · params · [opponent(36)] · [channel(1)] |
| 25 | JoinApp | app_id(8) |
| 26 | AppMove | app_id(8) · move_len(2) · move_bytes |
| 27 | ResignApp | app_id(8) |
| 28 | ClaimAppTimeout | app_id(8) |
| 39 | SettleApp | app_id(8) · final_seq(4) · move_count(4) · [seat(1) · move_len(2) · move · signature(64)]* |
Los dos campos finales opcionales de CreateApp
opponent y channel son ambos opcionales, y cuáles están presentes se deduce de cuántos bytes
quedan después de params:
| Bytes restantes | Significado |
|---|---|
| 0 | app abierta, juego en la cadena |
| 1 | channel solamente |
| 36 | opponent solamente |
| 37 | opponent y después channel |
Un opponent convierte la app en un desafío directo: solo esa dirección puede unirse. Si lo omites, cualquiera
puede ocupar la plaza. channel = 1 marca una app de canal de estado y solo es válido cuando seats == 2.
Las apps multijugador almacenan a los jugadores en ranuras fijas de 36 bytes, por lo que toda dirección que ocupe una plaza debe ser canónica de 36 bytes (una dirección ZBC o una clave pública de 32 bytes sin prefijo). Las cuentas que no son de 36 bytes se rechazan al crear la app. Las apps individuales no tienen esa restricción: solo hay un jugador.
3. El registro de apps
app_type es un único byte. Solo se conocen estos valores; cualquier otro se rechaza.
Uno contra uno (2 plazas)
| Tipo | App | Tablero / estado | Bytes de la jugada |
|---|---|---|---|
| 1 | Tres en raya | 9 casillas | [cell 0..8] |
| 2 | Ajedrez | 64 casillas | [from, to], legalidad completa, jaque, jaque mate, ahogado, promoción automática a dama |
| 3 | Connect-4 | 42 casillas (7×6) | [column 0..6], gravedad, 4 en línea |
| 4 | Damas | 64 casillas + bloqueo de salto múltiple | [from, to], captura obligatoria, continuación de salto múltiple, damas coronadas |
| 5 | Reversi | 64 casillas | [cell 0..63], hay que voltear al menos una; el bando sin jugada pasa; gana quien tenga más fichas |
| 6 | Gomoku | 225 casillas (15×15) | [x, y], 5 en línea |
| 7 | Batalla naval | 6400 de compromiso + 200 de revelación | compromiso-revelación, ver abajo |
| 8 | Puntos y cajas | 24 lados + 9 cajas | [edge 0..23], quien completa una caja vuelve a jugar |
Individual contra la casa
| Tipo | App | params | Paga |
|---|---|---|---|
| 16 | Dos dados | [bet_type 0..3, total] | menos de 7 / más de 7 2.28×, 7 de la suerte 5.7×, total exacto 3420/ways % |
| 17 | Cara o cruz | [choice 0/1] | 1.98× |
| 18 | Ruleta | [bet_type, value] | número único 36×, color 2× |
| 19 | Tragamonedas | ninguna | tres iguales 15×, triple 7 50×, cualquier pareja 1.8× |
| 20 | Lotería | [pick 0..99] | 90× |
| 21 | Crash | [target ×100, 2 bytes LE] | objetivo ×, de 1.01× a 10.00× |
Los tipos de apuesta de los dados son 0 menos de 7, 1 7 de la suerte, 2 más de 7, 3 total exacto. Para un total exacto, el
multiplicador es 3420 / ways en centésimas, donde ways = 6 - |7 - total|, así que el 7 paga 5.70× y el 2 o el
12 pagan 34.20×. La ruleta tiene 37 casillas; el 0 es verde y pierde con ambos colores.
Grupo (3–4 plazas)
| Tipo | App | Estado | Bytes de la jugada |
|---|---|---|---|
| 32 | Ludo | seats×4 posiciones de las fichas + dado pendiente | en dos fases: tirar y luego elegir una ficha |
| 33 | Pig | seats puntuaciones + total del turno | [0 roll, 1 hold] |
| 34 | Carrera (serpientes y escaleras) | seats posiciones | [0], tirar |
| 35 | Monopoly simplificado | dinero, posiciones, propiedades, bancarrota, fase | en dos fases: tirar y luego, opcionalmente, comprar |
4. Dinero
Stakes. CreateApp retiene en garantía el stake del creador. Cada JoinApp retiene un stake igual. El bote
es la suma. Un stake_token_id de 0 significa ZBC; cualquier otro valor apuesta ese token coloreado, y toda
la app (bote, comisión de la casa, pago) se liquida en ese token.
Comisión de la casa. En un final decisivo, el 1% del bote va al fondo de apps y el ganador recibe el
resto. En un empate o una app cancelada, se reembolsan todos los stakes y no se cobra comisión.
El fondo de apps es una cuenta del protocolo con un saldo por token. Crece con esa comisión del 1% más
la ventaja de la casa en las apps individuales. Es la contraparte de toda apuesta individual, así que su saldo es lo que hace
posible el juego individual; puedes consultarlo en cualquier momento:
GET https://zoobc.network/api/v1/apps/pool
Límite de banca. El pago máximo posible de una sola apuesta individual no puede superar el 5% del fondo para
ese token. Se comprueba frente al peor caso de la propia apuesta para la casa, no frente a su media, así que ningún
acierto aislado puede vaciar el fondo. Una apuesta que lo superaría se rechaza; si se rechaza una apuesta grande,
este suele ser el motivo.
5. Aleatoriedad, y cómo verificarla
Las apps individuales se resuelven a partir de la semilla del bloque, que en ZooBC es
block_seed = blocksmith_signature(SHA3(previous_block.block_seed)), determinista, verificable
públicamente con la clave pública del blocksmith e imposible de conocer antes de que exista el bloque.
Una apuesta realizada en el bloque de altura H se resuelve en H + 2:
r = SHA3-256( block_seed[H+2] ‖ app_id ) → the low 8 bytes, little-endian, as a uint64
El jugador se compromete dos bloques antes de que exista la semilla con la que se resuelve, así que no puede
predecirla ni elegirla. Cualquiera puede recalcular r después a partir de la semilla del bloque y el id de la app y,
por tanto, recalcular el resultado. Nada del resultado depende de datos que la cadena no contenga.
Crash obtiene su punto de caída del mismo r:
u = r mod 1e6
C = 99_000_000 / (1_000_000 - u) clamped to [100, 1000], i.e. 1.00× .. 10.00×
La apuesta gana stake × target / 100 cuando C >= target. El 99 del numerador es la ventaja
de la casa, el 1%. El límite de 10.00× es el techo, así que la mayor ganancia posible en Crash es diez veces la apuesta.
Multijugada. Cualquier app individual puede jugarse hasta 100 veces con una sola apuesta. Añade un byte
final de recuento N a params, después de los bytes de elección de esa app. El stake se reparte a partes iguales entre las N jugadas
y los pagos se suman, así que una transacción y una revelación producen N resultados independientes sin
esperar un bloque para cada uno. Omitir N significa una sola jugada, y una sola jugada es idéntica bit a bit a lo que
habría sido antes de que existiera la multijugada. N debe estar entre 1 y 100, y el stake debe poder dividirse de modo que cada jugada
apueste al menos una unidad.
6. Turnos, plazos y finales
Una app se activa cuando se ocupa su última plaza. A partir de entonces, cada jugada aceptada establece
deadline_height = current_height + 240 (~1 hour at 15 s per block)
Una jugada solo se acepta del jugador al que le toca, solo mientras la app está activa y solo
antes de que venza el plazo. Una app puede terminar de cuatro maneras:
- Una victoria o un empate según las reglas. Se liquida de inmediato: se paga al ganador o, en caso de empate, se reembolsan todos los stakes.
ResignApp. Abandonas; tu parte del bote va al oponente.ClaimAppTimeout. Tu oponente dejó pasar el plazo de 240 bloques. Lo reclamas y ganas el bote.
Te corresponde a ti reclamarlo: no ocurre nada automáticamente solo porque el plazo haya vencido.
- Nadie se une. Una app abierta que nadie acepta se cancela y se reembolsa el stake.
7. Canales de estado y SettleApp
En una app uno contra uno, hacer cada jugada en la cadena cuesta una transacción y un bloque por jugada. Un
canal de estado cambia eso por una única liquidación: crea la app con channel = 1 y seats = 2, juega
fuera de la cadena con cada parte firmando cada jugada, y envía la partida completa una sola vez como SettleApp (tipo 39).
La cadena reproduce las jugadas firmadas y ordenadas desde el estado inicial de la app con el mismo motor
de reglas, verifica cada firma y cada turno, y paga al ganador. Una liquidación puede ser reemplazada por
otra con un final_seq mayor durante 240 bloques después de registrarse; cuando ese periodo de impugnación
termina, queda liquidada.
Se aplican tres límites al cuerpo, y un integrador tiene que respetar los tres:
final_seqdebe ser igual amove_count.move_countno puede superar 1024 entradas.move_countno puede superar lo que el resto del cuerpo puede contener físicamente. Cada entrada ocupa al menos 67
bytes, seat(1) + move_len(2) + signature(64), así que un recuento mayor que remaining / 67 se
rechaza antes de reservar un solo byte.
1024 entradas es un límite por medio movimiento, no por par de jugadas: una entrada es un medio movimiento (ply), así que una liquidación de 1024 entradas cubre una partida de 512 jugadas. La partida de ajedrez más larga registrada duró 269 jugadas, es decir, 538 medios movimientos, así que el límite es aproximadamente el doble de la partida más larga que se haya jugado nunca.
8. Batalla naval (tipo 7)
Batalla naval es la única app uno contra uno que necesita información oculta, por lo que usa compromiso-revelación.
El tablero es de 10×10. Para cada una de las 100 casillas, el jugador elige una sal aleatoria de 32 bytes y se compromete con
SHA3(is_ship ‖ salt); el compromiso son esos 100 hashes concatenados, 3200 bytes, que se pasan como
params al crear la app. seats debe ser 2 y el compromiso debe tener exactamente 3200 bytes; de lo contrario, la creación se
rechaza.
El juego alterna dos tipos de jugada:
- Disparar
[0, cell]: disparas a una casilla del tablero del oponente. El turno pasa a él para que revele. - Revelar
[1, cell, is_ship, salt(32)]: el oponente demuestra qué había abriendo el compromiso
de esa casilla. Como cada casilla se compromete por separado, un jugador no puede mentir sobre ninguna casilla.
Se registra el acierto o el fallo, y quien revela dispara a continuación.
Si aciertas las 17 casillas de barco, ganas. Un jugador que no revela está sujeto al plazo como en cualquier otra jugada,
así que se aplica ClaimAppTimeout.
Las comisiones crecen con el tamaño. Una creación de 3200 bytes es una transacción grande, y la comisión mínima crece con el tamaño de la transacción. En una cadena en vivo, una comisión de 0.1 ZBC se rechazó por insuficiente y se aceptó una de unos 5 ZBC. Tenlo en cuenta en el presupuesto al crear la app; las jugadas de disparo y revelación son pequeñas y baratas.
9. Lectura del estado de una app
Cinco endpoints, servidos por la API del nodo; ten en cuenta que son endpoints de nodo, no de archivo:
URL base https://zoobc.network/api/v1. Es la pasarela pública de la TestNet de ZooBC, no la MainNet.
| Endpoint | Devuelve |
|---|---|
GET /api/v1/apps | el lobby; filtra con status=, category=solo|pvp|party, limit= |
GET /api/v1/apps/open | desafíos uno contra uno abiertos a los que cualquiera puede unirse |
GET /api/v1/apps/:id | el estado completo de una app |
GET /api/v1/apps/pool | los saldos del fondo de apps, por token |
GET /api/v1/apps/stats | recuentos en vivo: total, abiertas, activas, terminadas, jugadores, bote en juego |
Una fila de app incluye id, app_type, status (0 abierta, 1 activa, 2 terminada, 3 cancelada), seats,
creator_address, players, opponent_address, stake_token_id, stake_amount, pot,
state_blob, turn, created_height, last_move_height, deadline_height, resolve_height,
winner_address, persist_height y channel.
El state_blob es el tablero derivado actual, que se conserva para que un nodo no tenga que reproducir el historial en cada bloque.
Es una comodidad, no el registro: el registro son las transacciones de jugada, y un cliente puede reproducirlas
para auditarlas o animarlas. La fila en vivo de una app terminada se elimina tras un periodo de gracia; sus jugadas permanecen
en el historial de la cadena de forma permanente, así que la app siempre puede reconstruirse.
10. Desde la línea de comandos
zoobc-cli cubre los seis tipos. El primer parámetro es siempre la clave privada del remitente.
| Comando | Tipo |
|---|---|
app-create | 24 |
app-join | 25 |
app-move | 26 |
app-resign | 27 |
app-claim | 28 |
app-settle | 39 |
# a coin-flip against the house: type 17, 1 ZBC, seats=1, choice 0
zoobc-cli app-create <privkey> 17 0 100000000 1 00 --api $API
# tic-tac-toe, open to anyone, 1 ZBC
zoobc-cli app-create <privkey> 1 0 100000000 2 --api $API
# take the middle square
zoobc-cli app-move <privkey> <app_id> 04 --api $API
zoobc-cli help app-create muestra los campos de cualquiera de ellos. Consulta el Manual de la CLI para ver el conjunto
completo de comandos y el Manual de transacciones para ver la disposición de bytes de los 50 tipos de transacción.
11. Constantes
| Constante | Valor | Qué regula |
|---|---|---|
| Comisión de la casa | 1% del bote | al fondo de apps, solo en un final decisivo |
| Plazo por jugada | 240 bloques | ~1 hora; se reinicia con cada jugada aceptada |
| Resolución individual | altura de la apuesta + 2 | ~30 segundos |
| Límite de banca | 5% del fondo | pago máximo de una sola apuesta individual |
| Límite de multijugada | 100 | jugadas por apuesta individual |
| Límite de jugadas por liquidación | 1024 | entradas por SettleApp, ≥67 bytes cada una |
| Periodo de impugnación de la liquidación | 240 bloques | ~1 hora durante la cual gana un final_seq mayor |
| Plazas | 1, o 2–4 | 1 es individual; 2–4 es multijugador |
| Compromiso de Batalla naval | 3200 bytes | exactos; si no, la creación se rechaza |