feat: el contrato Soroban del jefe mundial, que no le cree a nadie - #26
feat: el contrato Soroban del jefe mundial, que no le cree a nadie#26leocagli wants to merge 2 commits into
Conversation
| pub fn hit(env: Env, player: Address, seed: u32, ticks: u32) -> Result<BossState, Error> { | ||
| player.require_auth(); | ||
|
|
||
| if ticks == 0 || ticks > FIGHT_CAP { | ||
| return Err(Error::TicksFueraDeRango); | ||
| } | ||
|
|
||
| let mut boss: BossState = env | ||
| .storage() | ||
| .instance() | ||
| .get(&Key::Boss) | ||
| .ok_or(Error::NoIniciado)?; | ||
| if !boss.alive { | ||
| return Err(Error::YaMurio); | ||
| } |
There was a problem hiding this comment.
⚠️ Security: hit() lets an attacker front-run and steal damage credit
hit binds a fight only to a player-chosen seed/ticks pair and dedups globally per spawn (Key::Used(spawn_id)), but nothing binds the seed to the caller. Because the seed and ticks are visible in a pending transaction, an attacker can copy them into their own hit call, get the damage credited to their address (Key::Damage), and cause the honest player's transaction to fail with PeleaRepetida, ultimately stealing the reward via claim. For a contract whose whole purpose is to be a trustless authority, mix the caller's Address into the fight identity/seed (e.g. derive the RNG seed or the Used key from player + seed) so a replayed fight cannot be attributed to a different address.
Was this helpful? React with 👍 / 👎
| pub fn init(env: Env, admin: Address) -> Result<(), Error> { | ||
| if env.storage().instance().has(&Key::Admin) { | ||
| return Err(Error::YaIniciado); | ||
| } | ||
| env.storage().instance().set(&Key::Admin, &admin); | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
💡 Bug: Persistent/instance storage TTL is never extended
hit, claim, spawn and init write to instance and persistent storage but never call extend_ttl. On a live network the Boss, Damage, Used and Claimed entries can be archived once their TTL lapses, which would make an in-progress boss disappear or make contributions un-claimable. Add TTL bumps (e.g. env.storage().instance().extend_ttl(...) and extend_ttl on the persistent keys after each write). This is design-demo code today, but should be resolved before mainnet as the PR notes.
Was this helpful? React with 👍 / 👎
docs/world-boss.md pide una sola autoridad sobre la vida del jefe, replicada
como {spawnId, hp, phase, revision}. lib/net.js no puede serlo y lo dice de si
mismo en su primer comentario: "no hay estado compartido, no hay autoridad, no
hay historia". Esto es esa autoridad, y nada mas que eso.
Lo dificil de un jefe compartido no es guardar un numero: es que el jugador que
dice "le hice 900 de dano" no pueda mentir. Las salidas habituales son creerle,
pedir una prueba de conocimiento cero, o abrir una ventana de disputa con
fianzas. runa permite la buena, y por un motivo concreto: el lenguaje de guiones
no tiene bucles ni recursion, asi que una pelea tiene un techo de ticks conocido
de antemano.
Entonces hit() no recibe un numero de dano. Recibe semilla y ticks, y el
contrato rehace la pelea. Si el jugador miente sobre el resultado no gana nada,
porque el resultado no se lo pregunta nadie.
Que eso entre en el presupuesto esta medido sobre wasm de verdad:
cada tick 3.708 de cpu
una pelea de 900 ticks 3.583.664 3% del presupuesto
un duelo, dos peleas 6.917.601 6% del presupuesto
Queda margen de catorce veces.
DEUDA DECLARADA, y esto es importante: jefe/src/fight.rs es la FORMA del combate
de runa, no su motor. El motor vive en lib/world.js y lib/script.js. Mientras no
se porten, el dano que calcula el contrato y el que calcula el juego NO
coinciden, y este commit no declara ninguna equivalencia. Eso se arregla con un
test cruzado que corra la misma semilla y los mismos ticks en los dos lenguajes,
no leyendo los archivos y convenciendose. Hasta que ese test exista y este
verde, esto sirve para probar el diseno y no para repartir premios de verdad.
La simulacion JavaScript no se toca ni se reemplaza.
Dos cosas que casi salen mal y ahora tienen test:
- env.register(Contrato, ()) corre el contrato como Rust nativo y el medidor de
presupuesto no ve el bucle: daba 17.907 para un tick y para novecientos. Hay
que registrar el wasm con contractimport!. El test el_costo_crece_con_los_ticks
vigila eso, y el del duelo comprueba que las dos peleas corrieron de verdad.
- Las fases se calculaban con division entera y 238 de 360 daba 66 redondeado,
asi que el jefe cambiaba de fase un golpe antes que en el juego. Ahora se
compara en cruz. Los seis limites tienen test.
Un numero que conviene mirar: con 32,6 de dano promedio por pelea, 360 de vida
caen en once peleas. Es decision de diseno, no del contrato, pero queda medido.
No se agrega contracts/target, ni snapshots, ni dependencias compiladas.
.prettierignore hace falta porque prettier recorre el arbol entero y no lee
.gitignore.
contratos 16 tests (11 jefe + 5 medicion), todos verdes
wasm jefe 14.245 bytes, medicion 2.737
javascript 65/65 y 515/515, sin cambios: el contrato no toca JS
…rkspace `contracts/duel-arena` entro a main con el PR #45, despues de que esta rama se abriera. Sin tocar nada, cargo lo excluia del workspace en silencio: `cargo build` en `contracts/` compilaba jefe y medicion, y duel-arena no aparecia por ningun lado ni daba error. Quien arme el CI manana habria construido dos de tres contratos sin enterarse. Ahora la exclusion es explicita y dice por que existe. Las dos razones son reales: - duel-arena ancla `soroban-sdk = "=26.1.1"` con igual, mientras jefe y medicion usan 27.0.6, que es la version del protocolo vivo en mainnet y testnet. En un mismo workspace conviven pero se compilan dos copias del SDK. - duel-arena trae su propio `[profile.release]`, y un workspace solo honra el de la raiz, asi que meterlo adentro le cambiaria las opciones de compilacion por la espalda. Unificar las versiones es trabajo aparte y merece su propia issue. Lo que arregla este commit es que la exclusion sea una decision visible en vez de un olvido. cargo metadata sin error workspace ve medicion, jefe cargo test -p jefe 11/11
8869e0c to
bd8e840
Compare
Code Review
|
| Auto-apply | Compact |
|
|
Important
Your trial ends in 7 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.
Was this helpful? React with 👍 / 👎 | Gitar
|
Cierro este por la misma razón que el #25: su contenido ya está integrado en el trabajo Lo traen dos commits: Un dato que quedó comprobado recién y vale para el repositorio: los tests de Rust de este Sin el build previo, La rama |
La autoridad sobre la vida del jefe mundial. No reemplaza la simulacion
JavaScript ni declara equivalencia de dano (ver la deuda mas abajo).
Que resuelve
docs/world-boss.mdlo pide con todas las letras:lib/net.jsno puede serlo, y lo dice de si mismo en su primer comentario: "nohay estado compartido, no hay autoridad, no hay historia". Sirve para verse
caminar. Un jefe con vida compartida necesita que alguien diga cuanta vida le
queda y que todos le crean.
La decision que ordena todo
El contrato no le cree a nadie: calcula.
Lo dificil no es guardar un numero, es que el jugador que dice "le hice 900 de
dano" no pueda mentir. Las salidas habituales son creerle (malo), pedir una
prueba de conocimiento cero (caro), o abrir una ventana de disputa con fianzas
(economico, no matematico).
runa permite la buena, y por un motivo concreto: el lenguaje de guiones no
tiene bucles ni recursion, asi que una pelea tiene un techo de ticks conocido
de antemano. Entonces
hit()no recibe un numero de dano. Recibe semilla yticks, y el contrato rehace la pelea.
Hay un test que se llama
el_jugador_no_dice_cuanto_dano_hizoy compruebajustamente esa ausencia: si alguien agrega ese parametro alguna vez, deja de
compilar.
Que entre en el presupuesto esta medido, no supuesto
Sobre wasm de verdad, que es lo que corre la red:
3708 de cpu por tick. Queda margen de catorce veces.
La trampa que casi me hace publicar un numero falso
La primera medicion daba 17.907 para un tick y 17.907 para novecientos.
Identico. La causa:
env.register(Contrato, ())corre el contrato como Rustnativo, y el medidor de presupuesto no ve el bucle; ese numero era solo el
costo de invocar.
Para medir de verdad hay que registrar el wasm con
contractimport!. Quedarondos tests de guardia:
el_costo_crece_con_los_ticksfalla si el medidor deja de ver el bucle.corrio las dos de verdad. Antes media dos invocaciones sueltas y el arnes
reinicia el presupuesto entre ellas, asi que un duelo parecia salir gratis.
Un bug de precision que habria desincronizado las fases
Las fases se calculaban con division entera:
Pero el JavaScript trabaja con decimales y
0.6611no es<= 0.66, o sea fase0. El jefe habria cambiado de fase un golpe antes en la cadena que en el
juego. Ahora se compara en cruz sin dividir, y los seis limites tienen test.
La deuda, dicha de frente
jefe/src/fight.rses la forma del combate de runa, no su motor. El motorvive en
lib/world.jsylib/script.jsy son unas quinientas lineas.Mientras no se porten, el dano que calcula el contrato y el que calcula el
juego no coinciden, y este PR no declara ninguna equivalencia. Eso no se
arregla leyendo los dos archivos y convenciendose: se arregla con un test
cruzado que corra la misma semilla y los mismos ticks en los dos lenguajes.
Hasta que ese test exista y este verde, esto sirve para probar el diseno y no
para repartir premios de verdad.
Queda escrito tambien en
contracts/README.md.Un numero de balance, por si sirve
Con 32,6 de dano promedio por pelea, 360 de vida caen en once peleas. Es
decision de diseno y no del contrato, pero queda medido.
Verificacion
Los numeros de JavaScript no cambian porque el contrato no toca JavaScript.
Lo que NO entra al repositorio
Ni
contracts/target, ni snapshots generados, ni caches, ni dependenciascompiladas.
.prettierignorehace falta porque prettier recorre el arbol enteroy no lee
.gitignore.