101a4b60a9
Найдено при приёмке боевого домена smart-updown.binom.pw (симптом «SOL не обновляется»). - SEC-16: при create_bet ренту PDA-аккаунта ставки (1 559 040 lamports = (96+32+128)*6960) платит БЕТТОР, а не шлюз; баланс шлюза-админа при этом не меняется (замер whose-fee.cjs). Аккаунт не закрывается никогда (в контракте 9ALs четыре инструкции, ни одной close = bettor) → рента теряется безвозвратно. В цепи 75 аккаунтов = 0.116928 SOL заперто, растёт линейно. Вердикт: на тестах терпимо (маскируется краном, 5 SOL = ~3207 раундов), перед mainnet — блокер. - TASKS.md: фича «если цена не изменилась — подождать ещё 15 с» (НЕ доделана), заметка про боевой домен на общем TS-шлюзе и лаг крана 8–13 с. - tools: доказательные скрипты (whose-fee, bet-pda-rent, faucet-latency, sol-balance-cycle, settle-math-test, backend-full-test) + README с питфоллами замера. - k8s/smart-updown-api-ingress.yaml: /api,/ws вынесены вне helm (релиз игры их сносил).
121 lines
8.1 KiB
Markdown
121 lines
8.1 KiB
Markdown
# Инструменты проверки игрового контура
|
||
|
||
Скрипты для **живой** приёмки механик ставок (не заменяют `npm test` в репозитории релейера,
|
||
а проверяют боевой стенд).
|
||
|
||
## `exploit-decisive.cjs` — решающая проверка окна закрытия
|
||
|
||
Проверяет главное: **цена закрытия берётся из истории на момент экспирации, а не в момент клика.**
|
||
|
||
Как запускать (внутри пода шлюза — там есть `node_modules` и доступ к RPC):
|
||
|
||
```bash
|
||
P=$(kubectl -n game get pod -l app.kubernetes.io/instance=testfront-gateway -o jsonpath='{.items[0].metadata.name}')
|
||
kubectl -n game cp tools/exploit-decisive.cjs "$P:/app/data/probe/exploit-decisive.cjs" -c gateway
|
||
kubectl -n game exec "$P" -c gateway -- sh -c "cd /app && node /app/data/probe/exploit-decisive.cjs 300"
|
||
```
|
||
|
||
### Питфоллы (каждый пойман вживую)
|
||
|
||
- **Нельзя просто сравнивать `exitPrice` с текущей ценой.** Цена часто стоит на месте, и сравнение
|
||
вырождается в «совпало» — ложный успех. Скрипт ждёт, пока цена на экспирации и текущая
|
||
**разойдутся**, и только тогда закрывает. Первый прогон без этого дал ложное «совпало» дважды.
|
||
- **Копировать только в `/app/data/`**, не в `/app` (`Permission denied`, под не root).
|
||
- **Запускать с `cd /app`**, иначе `Cannot find module '@solana/web3.js'` (из `/tmp`/`/app/data`
|
||
без cwd `node_modules` не виден).
|
||
- Ставка при `exit == entry`: с 2026-09-14 (`SEC-9`) **возвращает залог** статусом `3` и
|
||
финализируется — залипаний больше нет. *Открытый вопрос:* хочется вместо немедленного возврата
|
||
**подождать ещё 15 с** и взять цену заново (см. `TASKS.md`, «ждать, если цена не изменилась»).
|
||
|
||
Читает `bet_time` / `expire_time` прямо из аккаунта Bet (сырые байты) — off-chain данными не
|
||
верим. Раскладка аккаунта (Anchor, после 8-байтового дискриминатора): `amount u64 @8`,
|
||
`bet_time i64 @16`, `expire_time i64 @24`, `entry_price u64 @32`, `exit_price u64 @40`,
|
||
`side u64 @48`, `status u64 @56`, `bettor Pubkey @64`.
|
||
|
||
## `price-history-probe.cjs` — разбор истории цены
|
||
|
||
Смотрит, что реально накопилось в `binance-market` (Postgres `192.168.76.182:5434`, БД
|
||
`binance_market`) вокруг конкретной ставки: сделки в окне, цена на входе, цена на экспирации,
|
||
и что было бы при «ждать выгодной цены».
|
||
|
||
```bash
|
||
node tools/price-history-probe.cjs <programId> <betId>
|
||
```
|
||
|
||
⚠️ **Символ в БД — в нижнем регистре** (`solusdt`, не `SOLUSDT`). Запрос с верхним регистром
|
||
вернёт пусто, и это легко принять за «данных нет».
|
||
|
||
## `whose-fee.cjs` — с ЧЬЕГО кошелька уходит SOL за раунд (доказательство `SEC-16`)
|
||
|
||
Мерит SOL **игрока** в трёх точках каждого раунда (до ставки → после ставки → после расчёта)
|
||
и SOL **шлюза-админа**. Замер: игрок `5 000 000 000 → 4 998 440 960` (−**1 559 040** lamports),
|
||
шлюз не меняется — значит ренту PDA-аккаунта ставки платит игрок.
|
||
|
||
```bash
|
||
node tools/whose-fee.cjs
|
||
```
|
||
|
||
## `bet-pda-rent.cjs` — сколько ренты заперто в аккаунтах ставок
|
||
|
||
Обходит `getProgramAccounts` (фильтр `dataSize:96`) и показывает, что у каждой ставки PDA
|
||
держит ровно **1 559 040** lamports. Аккаунт не закрывается никогда (`close_bet` не делает
|
||
`close = bettor`) → рента теряется безвозвратно. Второе доказательство `SEC-16`.
|
||
|
||
```bash
|
||
node tools/bet-pda-rent.cjs
|
||
```
|
||
|
||
## `faucet-latency.cjs` / `sol-balance-cycle.cjs` — почему кажется, что «SOL не обновился»
|
||
|
||
`GET /faucet/{addr}` отвечает **мгновенно**, но `getBalance` **на цепи** показывает `0` ещё
|
||
**8–13 с** (замеры: 13309 / 13345 / 13345 мс). Игра читает баланс не через `/api/balance`, а
|
||
через свой nginx-прокси `/solana-rpc/getBalance` → прямо с цепи → попадает в окно лага.
|
||
Симптом «налил, а SOL не обновился» — это лаг зачисления, а не отставание шлюза.
|
||
|
||
```bash
|
||
node tools/faucet-latency.cjs # лаг крана: цепь против отчёта шлюза
|
||
node tools/sol-balance-cycle.cjs # баланс после крана: сразу 0, через ~20 с 5 SOL
|
||
```
|
||
|
||
## `settle-math-test.cjs` — математика расчёта (правильный способ замера)
|
||
|
||
⚠️ Шлюз **кастодиальный**, поэтому дельту надо считать **от момента после lock** (фантики ушли
|
||
в vault при ставке), а НЕ от старта раунда — иначе в дельту попадают и ставка, и выплата, и
|
||
«математика не сходится» ложно. Замерено: выигрыш `+130 000 000` (1.3×), проигрыш `0`,
|
||
ничья `+100 000 000` (возврат). Считать «от старта» = получить ложный провал (проверено живьём).
|
||
|
||
```bash
|
||
node tools/settle-math-test.cjs
|
||
```
|
||
|
||
## `backend-full-test.cjs` — контракт ручек и коды ошибок шлюза
|
||
|
||
Приёмка API живьём: `/state`, `/price`, `/wallet`, `/faucet`, `/balance`, `/bet`, идемпотентность
|
||
крана, `400` на кривые данные, `409` на вторую открытую ставку, `404` на снятые ручки
|
||
(`/close`, `/bet/{id}`), WS-события `bet_open`/`bet_closed`. Полный прогон с расчётом на цепи.
|
||
|
||
```bash
|
||
node tools/backend-full-test.cjs
|
||
```
|
||
|
||
⚠️ `/bet` требует **число** в `amountUnits` (строка → `400`) и `address` в теле.
|
||
⚠️ `getTokenAccountsByOwner` принимает **2–3** параметра: commitment кладётся **внутрь третьего**
|
||
(`cfg`), а не четвёртым — иначе `-32602 Expected from 2 to 3 parameters`.
|
||
|
||
## `k8s/smart-updown-api-ingress.yaml` — пути `/api`,`/ws` вне helm
|
||
|
||
⚠️ Чарт `fun-game-front2` рендерит **только** path `/`, поэтому любой `helm upgrade` игры
|
||
**сносит** `/api` и `/ws` с боевого домена (домен начинает отдавать HTML игры на `/api/*`).
|
||
Лечение — отдельный ingress `smart-updown-api` **вне helm**; front2-ingress остаётся `paths: [/]`.
|
||
|
||
```bash
|
||
ssh root@192.168.76.120 "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml; \
|
||
kubectl apply -f /root/smart-updown-api-ingress.yaml"
|
||
# проверка после каждого релиза игры:
|
||
kubectl -n game get ingress -o custom-columns='NAME:.metadata.name,HOST:.spec.rules[0].host,PATHS:.spec.rules[0].http.paths[*].path'
|
||
```
|
||
|
||
⚠️ НЕ править paths через `kubectl patch --type=json` с `remove` по индексам: при нескольких
|
||
путях индексы съезжают, остаётся висячий `/ws`, а последний path удалить нельзя
|
||
(`paths: Required value`). Надёжно — собрать нужный список целиком и `kubectl replace --force`.
|