En dice-duel, el resultado de la partida queda determinado por datos que quien
arma la transaccion conoce y controla antes de que nadie apueste.
La evidencia
contracts/dice-duel/src/lib.rs:274:
// Seed components (all deterministic and identical between sim/submit):
// 1. Session ID - unique per game
// 2. Player addresses - both players contribute
//
// Note: We do NOT include ledger sequence or timestamp because those differ
// between simulation and submission, which would cause different winners.
let mut seed_bytes = Bytes::new(&env);
seed_bytes.append(&Bytes::from_array(&env, &session_id.to_be_bytes()));
seed_bytes.append(&game.player1.to_string().to_bytes());
seed_bytes.append(&game.player2.to_string().to_bytes());
let base_seed = env.crypto().keccak256(&seed_bytes);
O sea: semilla = keccak256(session_id || player1 || player2).
Y en start_game (linea 135), el session_id es un parametro que elige quien
llama:
pub fn start_game(
env: Env,
session_id: u32,
player1: Address,
player2: Address,
player1_points: i128,
player2_points: i128,
) -> Result<(), Error> {
Por que eso importa
Los tres ingredientes de la semilla se conocen antes de firmar. Entonces quien arma
la partida puede, offline y gratis, calcular el resultado para cada session_id
posible y presentar a la firma solamente uno donde gana. Cada intento es un keccak;
encontrar uno favorable lleva unos pocos.
El rival firma con require_auth_for_args([session_id, points]). Ve el numero, pero
no tiene ningun motivo para sospechar que ese numero decide la partida, y para
saberlo tendria que correr el mismo keccak.
Y hay un detalle que lo hace peor: roll() (linea 208) solo marca banderas. Los
dados se generan recien en reveal_winner. O sea que tirar es teatro; el resultado
ya estaba resuelto cuando se creo la partida.
Para un contrato que sostiene apuestas, esto es la falla que importa: no es que
alguien pueda hacer trampa con esfuerzo, es que el resultado se elige en vez de
sortearse.
El comentario del codigo explica por que se llego aca
Y la razon es legitima: incluir el ledger rompia la coincidencia entre simulacion y
envio. Es un problema real de Soroban, no un descuido. Lo que hay que cambiar es la
salida, no el diagnostico.
Direccion del arreglo
El patron que resuelve esto es compromiso y revelacion, que separa el momento de
apostar del momento en que se conoce la aleatoriedad:
- Cada jugador publica
hash(su_numero_secreto + sal) junto con la apuesta.
- Nadie puede calcular el resultado, porque le falta el secreto del otro.
- Vencido el plazo, los dos revelan; el contrato comprueba los hashes.
- La semilla sale de los dos secretos combinados.
- Si uno no revela, el otro cobra por incomparecencia.
Eso mantiene la propiedad que el comentario buscaba (simulacion y envio dan lo mismo,
porque en el momento de liquidar todo es dato firme) y saca la que sobra (que el
resultado se pueda anticipar).
Antes de escribir codigo
Revisar si number-guess y twenty-one tienen la misma forma. En number-guess la
semilla incluye session_id y las dos apuestas (contracts/number-guess/src/lib.rs:280),
que es mejor pero puede compartir el problema si las dos apuestas viajan en la misma
llamada. Conviene arreglar los tres con el mismo patron en vez de tres parches
distintos.
Como se toma este trabajo: comentá la issue con un plan concreto (que archivo, que
funcion, y como lo vas a verificar). Se asigna por la calidad de ese plan, no por
orden de llegada. La PR va despues de la asignacion, con el cambio real adentro.
En
dice-duel, el resultado de la partida queda determinado por datos que quienarma la transaccion conoce y controla antes de que nadie apueste.
La evidencia
contracts/dice-duel/src/lib.rs:274:O sea:
semilla = keccak256(session_id || player1 || player2).Y en
start_game(linea 135), elsession_ides un parametro que elige quienllama:
Por que eso importa
Los tres ingredientes de la semilla se conocen antes de firmar. Entonces quien arma
la partida puede, offline y gratis, calcular el resultado para cada
session_idposible y presentar a la firma solamente uno donde gana. Cada intento es un keccak;
encontrar uno favorable lleva unos pocos.
El rival firma con
require_auth_for_args([session_id, points]). Ve el numero, perono tiene ningun motivo para sospechar que ese numero decide la partida, y para
saberlo tendria que correr el mismo keccak.
Y hay un detalle que lo hace peor:
roll()(linea 208) solo marca banderas. Losdados se generan recien en
reveal_winner. O sea que tirar es teatro; el resultadoya estaba resuelto cuando se creo la partida.
Para un contrato que sostiene apuestas, esto es la falla que importa: no es que
alguien pueda hacer trampa con esfuerzo, es que el resultado se elige en vez de
sortearse.
El comentario del codigo explica por que se llego aca
Y la razon es legitima: incluir el ledger rompia la coincidencia entre simulacion y
envio. Es un problema real de Soroban, no un descuido. Lo que hay que cambiar es la
salida, no el diagnostico.
Direccion del arreglo
El patron que resuelve esto es compromiso y revelacion, que separa el momento de
apostar del momento en que se conoce la aleatoriedad:
hash(su_numero_secreto + sal)junto con la apuesta.Eso mantiene la propiedad que el comentario buscaba (simulacion y envio dan lo mismo,
porque en el momento de liquidar todo es dato firme) y saca la que sobra (que el
resultado se pueda anticipar).
hash(secreto + sal)de cada jugador junto con la apuestasession_idreusar un compromiso viejo en otra partida
revelar dos veces, reusar el compromiso de otra partida
Antes de escribir codigo
Revisar si
number-guessytwenty-onetienen la misma forma. Ennumber-guesslasemilla incluye
session_idy las dos apuestas (contracts/number-guess/src/lib.rs:280),que es mejor pero puede compartir el problema si las dos apuestas viajan en la misma
llamada. Conviene arreglar los tres con el mismo patron en vez de tres parches
distintos.
Como se toma este trabajo: comentá la issue con un plan concreto (que archivo, que
funcion, y como lo vas a verificar). Se asigna por la calidad de ese plan, no por
orden de llegada. La PR va despues de la asignacion, con el cambio real adentro.