chore(tools): оставить решающий тест + README, убрать дубли
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
# Инструменты проверки игрового контура
|
||||||
|
|
||||||
|
Скрипты для **живой** приёмки механик ставок (не заменяют `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` остаётся `open` — это ожидаемо (равные цены, отдельная задача),
|
||||||
|
а не сбой проверки.
|
||||||
|
|
||||||
|
Читает `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`). Запрос с верхним регистром
|
||||||
|
вернёт пусто, и это легко принять за «данных нет».
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Живая проверка того, что окно выбора времени ЗАКРЫТО.
|
|
||||||
#
|
|
||||||
# Схема проверки (повторяет поведение вора):
|
|
||||||
# 1. создаём кошелёк, пополняем
|
|
||||||
# 2. ставим ставку
|
|
||||||
# 3. ЖДЁМ существенно дольше экспирации (по умолчанию 60 с; экспирация 15 с)
|
|
||||||
# 4. закрываем и смотрим, какую цену вернул шлюз
|
|
||||||
#
|
|
||||||
# Ожидание ПОСЛЕ фикса: цена закрытия == цена на момент экспирации (из истории),
|
|
||||||
# а НЕ текущая цена на момент клика. Плюс шлюз должен отказать, если цена закрытия
|
|
||||||
# совпала с ценой входа (ставка останется open — это отдельная задача).
|
|
||||||
#
|
|
||||||
# Использование: bash check-exploit.sh [секунды_ожидания]
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
G="${GW_URL:-https://testfront.binom.pw/api}"
|
|
||||||
WAIT_SEC="${1:-60}"
|
|
||||||
|
|
||||||
jqget() { python3 -c "import sys,json;print(json.load(sys.stdin)$1)" 2>/dev/null; }
|
|
||||||
|
|
||||||
echo "=== 1. кошелёк ==="
|
|
||||||
W=$(curl -s --max-time 30 -X POST "$G/wallet" -H 'Content-Type: application/json' -d '{}')
|
|
||||||
A=$(echo "$W" | jqget "['address']")
|
|
||||||
echo "address=$A"
|
|
||||||
[ -z "$A" ] && { echo "ОШИБКА: адрес не получен ($W)"; exit 1; }
|
|
||||||
|
|
||||||
echo "=== 2. пополнение ==="
|
|
||||||
curl -s --max-time 60 "$G/faucet/$A" | head -c 200; echo
|
|
||||||
|
|
||||||
echo "=== 3. ставка ==="
|
|
||||||
BET=$(curl -s --max-time 45 -X POST "$G/bet" -H 'Content-Type: application/json' \
|
|
||||||
-d "{\"side\":\"UP\",\"amountUnits\":\"100000000\",\"address\":\"$A\"}")
|
|
||||||
echo "$BET"
|
|
||||||
ID=$(echo "$BET" | jqget "['id']")
|
|
||||||
ENTRY=$(echo "$BET" | jqget "['entryPrice']")
|
|
||||||
[ -z "$ID" ] && { echo "ОШИБКА: ставка не создана"; exit 1; }
|
|
||||||
echo "id=$ID entryPrice=$ENTRY"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== 4. цена шлюза СЕЙЧАС (до экспирации) ==="
|
|
||||||
curl -s --max-time 20 "$G/price"; echo
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== 5. ждём ${WAIT_SEC}с (экспирация 15с — окно прошло) ==="
|
|
||||||
sleep "$WAIT_SEC"
|
|
||||||
|
|
||||||
echo "=== 6. цена шлюза ПОСЛЕ ожидания (текущая, которую вор хотел бы получить) ==="
|
|
||||||
P_NOW=$(curl -s --max-time 20 "$G/price")
|
|
||||||
echo "$P_NOW"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== 7. ЗАКРЫТИЕ — какую цену возьмёт? ==="
|
|
||||||
BEFORE=$(date -u +%s)
|
|
||||||
CLOSE=$(curl -s --max-time 45 -X POST "$G/close" -H 'Content-Type: application/json' -d "{\"id\":\"$ID\"}")
|
|
||||||
AFTER=$(date -u +%s)
|
|
||||||
echo "$CLOSE"
|
|
||||||
|
|
||||||
EXIT=$(echo "$CLOSE" | jqget "['exitPrice']")
|
|
||||||
echo
|
|
||||||
echo "================ ИТОГ ================"
|
|
||||||
echo "entryPrice (вход) : $ENTRY"
|
|
||||||
echo "exitPrice (шлюз вернул) : $EXIT"
|
|
||||||
echo "цена в момент клика : $P_NOW"
|
|
||||||
echo
|
|
||||||
if [ -z "$EXIT" ]; then
|
|
||||||
echo "ВЕРДИКТ: закрытие НЕ выполнилось (см. ответ выше)."
|
|
||||||
elif [ "$EXIT" = "$ENTRY" ]; then
|
|
||||||
echo "ВЕРДИКТ: exit == entry -> ставка осталась open (равные цены, отдельная задача)."
|
|
||||||
echo " Главное: цена НЕ взята произвольно с момента клика."
|
|
||||||
else
|
|
||||||
# Сравниваем exit с ценой, которая была бы при старом поведении
|
|
||||||
CUR=$(echo "$P_NOW" | jqget "['price']")
|
|
||||||
if [ -n "$CUR" ]; then
|
|
||||||
echo "Текущая цена в момент клика: $CUR (в целом виде $(python3 -c "print(int(float('$CUR')*10**8))" 2>/dev/null))"
|
|
||||||
fi
|
|
||||||
echo "Смотрим глазами: exitPrice должен соответствовать цене НА МОМЕНТ ЭКСПИРАЦИИ,"
|
|
||||||
echo "а не текущей. Если exitPrice == текущей цене клика — ОКНО ОТКРЫТО, фикс не работает."
|
|
||||||
fi
|
|
||||||
echo "закрытие заняло: $((AFTER-BEFORE))с"
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
/**
|
|
||||||
* ЖИВАЯ проверка: закрыто ли окно выбора времени закрытия.
|
|
||||||
* Запускается ВНУТРИ пода шлюза (обращается к 127.0.0.1:8895 и читает ставку из RPC).
|
|
||||||
*
|
|
||||||
* Логика доказательства:
|
|
||||||
* - ставим ставку, запоминаем betTime/expireTime (сырые байты аккаунта Bet)
|
|
||||||
* - ЖДЁМ существенно дольше экспирации
|
|
||||||
* - считаем: price_at_expiry (история, ts <= expireTime) и price_now (текущая)
|
|
||||||
* - закрываем ставку и смотрим, какая цена ушла в транзакцию
|
|
||||||
*
|
|
||||||
* ОКНО ЗАКРЫТО, если exitPrice == price_at_expiry и при этом price_now != price_at_expiry
|
|
||||||
* (т.е. цена взята из прошлого, а не с момента клика).
|
|
||||||
*/
|
|
||||||
const { Connection, PublicKey } = require("@solana/web3.js");
|
|
||||||
const { DatabaseSync } = require("node:sqlite");
|
|
||||||
|
|
||||||
const GW = "http://127.0.0.1:8895";
|
|
||||||
const RPC = "http://192.168.76.181:8899";
|
|
||||||
const PROGRAM_ID = new PublicKey("9ALsnxXNzDBv3mokngCHbruRTWUZiWR7pf7vswS1fCpf");
|
|
||||||
const DB = "/app/data/price-history.db";
|
|
||||||
|
|
||||||
const WAIT_MS = Number(process.argv[2] || 75) * 1000;
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
||||||
|
|
||||||
async function api(path, body) {
|
|
||||||
const opts = body === undefined
|
|
||||||
? { method: "GET" }
|
|
||||||
: { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) };
|
|
||||||
const r = await fetch(GW + path, opts);
|
|
||||||
let j = null;
|
|
||||||
try { j = await r.json(); } catch { /* ignore */ }
|
|
||||||
return { status: r.status, json: j };
|
|
||||||
}
|
|
||||||
|
|
||||||
function priceAtExpiry(expireMs) {
|
|
||||||
const db = new DatabaseSync(DB);
|
|
||||||
const row = db.prepare("SELECT price FROM price_history WHERE ts <= ? ORDER BY ts DESC LIMIT 1").get(expireMs);
|
|
||||||
const cnt = db.prepare("SELECT COUNT(*) c FROM price_history").get().c;
|
|
||||||
const first = db.prepare("SELECT ts FROM price_history ORDER BY ts ASC LIMIT 1").get();
|
|
||||||
db.close();
|
|
||||||
return { price: row ? row.price : null, count: cnt, firstTs: first ? first.ts : null };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readBet(id) {
|
|
||||||
const conn = new Connection(RPC, "confirmed");
|
|
||||||
const seed = Buffer.alloc(8); seed.writeBigUInt64LE(BigInt(id));
|
|
||||||
const [pda] = PublicKey.findProgramAddressSync([Buffer.from("bet"), seed], PROGRAM_ID);
|
|
||||||
const info = await conn.getAccountInfo(pda, "confirmed");
|
|
||||||
if (!info) return null;
|
|
||||||
const d = info.data;
|
|
||||||
return {
|
|
||||||
pda: pda.toBase58(),
|
|
||||||
betTime: Number(d.readBigInt64LE(16)),
|
|
||||||
expireTime: Number(d.readBigInt64LE(24)),
|
|
||||||
entryPrice: d.readBigUInt64LE(32).toString(),
|
|
||||||
exitPrice: d.readBigUInt64LE(40).toString(),
|
|
||||||
status: d.readBigUInt64LE(56).toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
console.log("=== 1. кошелёк ===");
|
|
||||||
const w = await api("/wallet", {});
|
|
||||||
if (!w.json || !w.json.address) throw new Error("wallet failed: " + JSON.stringify(w));
|
|
||||||
const addr = w.json.address;
|
|
||||||
console.log("address:", addr);
|
|
||||||
|
|
||||||
console.log("=== 2. пополнение ===");
|
|
||||||
const f = await api("/faucet/" + addr);
|
|
||||||
console.log("faucet:", f.status, JSON.stringify(f.json).slice(0, 140));
|
|
||||||
|
|
||||||
console.log("=== 3. ставка ===");
|
|
||||||
const bet = await api("/bet", { side: "UP", amountUnits: "100000000", address: addr });
|
|
||||||
console.log("bet:", bet.status, JSON.stringify(bet.json).slice(0, 200));
|
|
||||||
if (!bet.json || !bet.json.id) throw new Error("bet failed");
|
|
||||||
const id = bet.json.id;
|
|
||||||
const entryResp = String(bet.json.entryPrice);
|
|
||||||
|
|
||||||
await sleep(1500);
|
|
||||||
const b0 = await readBet(id);
|
|
||||||
console.log("\n--- ставка on-chain ---");
|
|
||||||
console.log("betTime :", b0.betTime, new Date(b0.betTime * 1000).toISOString());
|
|
||||||
console.log("expireTime:", b0.expireTime, new Date(b0.expireTime * 1000).toISOString(),
|
|
||||||
"(+" + (b0.expireTime - b0.betTime) + "с)");
|
|
||||||
console.log("entry(on-chain):", b0.entryPrice, "| entry(/bet ответ):", entryResp);
|
|
||||||
|
|
||||||
console.log("\n=== 4. ПРОБА: закрыть ДО экспирации (должен быть 400) ===");
|
|
||||||
const early = await api("/close", { id });
|
|
||||||
const stillOpen = await readBet(id);
|
|
||||||
if (stillOpen.status === "0") {
|
|
||||||
console.log("ответ:", early.status, JSON.stringify(early.json));
|
|
||||||
console.log(early.status === 400 ? " -> ок, отказ до экспирации" : " -> ВНИМАНИЕ: ожидался 400");
|
|
||||||
} else {
|
|
||||||
console.log(" (ставка уже закрылась сама/успела — пропускаем пробу)");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n=== 5. ЖДЁМ " + (WAIT_MS / 1000) + "с (экспирация давно прошла) ===");
|
|
||||||
await sleep(WAIT_MS);
|
|
||||||
|
|
||||||
const b1 = await readBet(id);
|
|
||||||
const expireMs = b1.expireTime * 1000;
|
|
||||||
const hist = priceAtExpiry(expireMs);
|
|
||||||
|
|
||||||
const nowResp = await api("/price");
|
|
||||||
const priceNowStr = nowResp.json ? String(nowResp.json.price) : null;
|
|
||||||
const priceNowInt = priceNowStr ? Number(priceNowStr.replace(".", "").padEnd(0) === "" ? "0" : priceNowStr.replace(".", "")) : null;
|
|
||||||
const toInt = (s) => { const [i, fr = ""] = String(s).split("."); return Number(i + fr.padEnd(8, "0").slice(0, 8)); };
|
|
||||||
const nowInt = priceNowStr ? toInt(priceNowStr) : null;
|
|
||||||
|
|
||||||
console.log("цена на момент экспирации (из истории):", hist.price, "(строк в истории:", hist.count + ")");
|
|
||||||
console.log("текущая цена (момент клика) :", nowInt, "(" + priceNowStr + ")");
|
|
||||||
console.log("entry :", b1.entryPrice);
|
|
||||||
|
|
||||||
console.log("\n=== 6. ЗАКРЫТИЕ ===");
|
|
||||||
const cl = await api("/close", { id });
|
|
||||||
console.log("ответ:", cl.status, JSON.stringify(cl.json));
|
|
||||||
const exitPrice = cl.json && cl.json.exitPrice != null ? String(cl.json.exitPrice) : null;
|
|
||||||
|
|
||||||
const b2 = await readBet(id);
|
|
||||||
console.log("после закрытия: status=" + b2.status, "exitPrice=" + b2.exitPrice);
|
|
||||||
|
|
||||||
console.log("\n================ ВЕРДИКТ ================");
|
|
||||||
console.log("price_at_expiry (история) :", hist.price);
|
|
||||||
console.log("exitPrice (ушло в tx) :", exitPrice);
|
|
||||||
console.log("price_now (момент клика) :", nowInt);
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
if (exitPrice === null) {
|
|
||||||
console.log("ЗАКРЫТИЕ НЕ ВЫПОЛНИЛОСЬ — см. ответ выше.");
|
|
||||||
} else if (hist.price === null) {
|
|
||||||
console.log("НЕТ ДАННЫХ В ИСТОРИИ — проверить запись.");
|
|
||||||
} else if (String(hist.price) === String(exitPrice)) {
|
|
||||||
if (String(nowInt) !== String(hist.price)) {
|
|
||||||
console.log("*** ОКНО ЗАКРЫТО ***");
|
|
||||||
console.log("exitPrice взят ИЗ ИСТОРИИ на момент экспирации и НЕ равен цене момента клика.");
|
|
||||||
console.log("Игрок физически не может выбрать выгодный момент: цена уже зафиксирована прошлым.");
|
|
||||||
} else {
|
|
||||||
console.log("СОВПАЛО (но цена не двигалась) — тест слабый, цена сейчас та же. Повторить позже.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("!!! ОКНО ОТКРЫТО !!! exitPrice (" + exitPrice + ") != цена на момент экспирации (" + hist.price + ")");
|
|
||||||
}
|
|
||||||
if (b2.status === "0") console.log("\n(ставка осталась open — exit==entry: равные цены, отдельная задача)");
|
|
||||||
})().catch((e) => { console.error("ERR:", e.message); process.exit(1); });
|
|
||||||
Reference in New Issue
Block a user