red-team send-path hardening: hold pada verify False/None, _send_rejected whitelist, fee guard, non-dict skip (V61/B20-B23)

This commit is contained in:
proitlab committed 2026-08-13 23:12:51 +07:00
1 parent 69704fa23d
commit d6b5927307
8 files changed
+182 -32

No files matched your search

+53 -21
View File
@@ -123,7 +123,12 @@ def _get_json(url, params=None, max_retries=3, timeout=10):
try:
resp = requests.get(f'{node}{url}', params=params, timeout=timeout)
resp.raise_for_status()
return resp.json()
data = resp.json()
if isinstance(data, dict):
return data
# V61: 200 tapi bentuk tak dikenal (list/string/proxy salah)
# ⊖ dianggap sukses — lewati host ini, coba cadangan.
continue
except requests.RequestException:
continue
if attempt < max_retries - 1:
@@ -142,20 +147,46 @@ def verify_txid(txid):
(bukan satu host hiccup) — hold V59 jadi jarang palsu.
"""
data = _get_json('/v2/history/get_transaction', params={'id': txid})
if data is None:
if not isinstance(data, dict): # V61: bentuk tak dikenal → ⊖ asumsi apapun
return None
return bool(data.get('executed'))
def _verify_settled(txid, retries=3, delay=2):
"""V61: verifikasi txid dgn settle-retry singkat — tx yang BARU mendarat
bisa belum terindeks Hyperion (`executed: false` sesaat, pola V50/B14 di
klaim). Baca `verify_txid` beberapa kali sebelum menyimpulkan tak-mendarat.
None (SEMUA host Hyperion mati) → langsung False (⊖ buang waktu sleep utk
pool yang mati — pemanggil TAHAN). → True bila pernah `executed`; False
bila tak bisa dikonfirmasi (pemanggil TAHAN, ⊖ resend). """
for _ in range(retries):
landed = verify_txid(txid)
if landed is True:
return True
if landed is None:
return False
time.sleep(delay)
return False
def _send_rejected(resp):
"""V59: deteksi penolakan chain dari respons `send()`.
"""V61: deteksi penolakan chain dari respons `send()` — WHITELIST.
pyntelope ⊥ raise pada HTTP 500 — chain rejection datang sbg body JSON
(pola V46/B11 di klaim): `{"code":500,"message":...,"error":{...}}` vs
sukses `{"transaction_id":..., "processed":...}`. → True bila bentuk
penolakan (ada `error`), False bila sukses/bentuk tak dikenal.
(pola V46/B11). Sukses HANYA bila respons berbentuk dict YANG MEMILIKI
`transaction_id` (nodeos selalu menyertakan pada push yang diterima);
bentuk penolakan (`{"code":500,"error":...}`), bentuk tak dikenal, maupun
`soft_fail`/`delayed` (200 tapi receipt ⊥ `executed`) → dianggap TOLAK.
→ True bila harus diperlakukan sbg gagal (⊥ tandai `sent` utk tx yang
belum tentu mendarat); False bila terbukti sukses sah.
"""
return isinstance(resp, dict) and resp.get('error') is not None
if not isinstance(resp, dict) or not resp.get('transaction_id'):
return True
receipt = ((resp.get('processed') or {}).get('receipt') or {}).get('status')
if receipt is not None and receipt != 'executed':
return True
return False
def build_signed_transfer(to, amount, memo):
@@ -276,27 +307,28 @@ def run_distribution(dry_run=False):
break
except Exception as exc:
# Timeout/penolakan setelah broadcast → tx mungkin mendarat.
# Verifikasi sebelum resend agar ⊥ ganda (V22). Bila Hyperion
# tak bisa dipastikan → TAHAN (tandai failed, jangan resend) —
# V59: hold sungguhan (⊥ break hanya di attempt terakhir).
# Verifikasi SEBELUM resend agar ⊥ ganda (V22). V61: HANYA
# `executed` yang boleh dilanjutkan; False (Hyperion bilang tak
# mendarat — bisa lag indeks V50/B14) MAUPUN None (Hyperion
# down) → TAHAN (tandai failed, ⊖ resend): resend membawa
# tapos/expiration baru → txid baru → ganda.
if txid is not None:
landed = verify_txid(txid)
landed = _verify_settled(txid)
if landed is True:
db.update_payment_status(pid, 'sent', txid=txid)
sent_amount += float(share)
print(f'[OK] {owner}: {share} VEX '
f'(terverifikasi, {txid[:16]}...)', flush=True)
break
if landed is None:
print(f'[Peringatan] Verifikasi txid tak tersedia '
f'untuk {owner} — ditahan (⊥ resend)',
flush=True)
db.update_payment_status(pid, 'failed', txid=txid,
error=str(exc))
failed.append((owner, exc))
print(f'[DITAHAN] {owner}: {share} VEX — {exc}',
flush=True)
break
print(f'[Peringatan] Verifikasi txid tak tersedia '
f'untuk {owner} — ditahan (⊥ resend)',
flush=True)
db.update_payment_status(pid, 'failed', txid=txid,
error=str(exc))
failed.append((owner, exc))
print(f'[DITAHAN] {owner}: {share} VEX — {exc}',
flush=True)
break
if attempt == DISTRIBUTE_MAX_ATTEMPTS:
db.update_payment_status(pid, 'failed', txid=txid,
error=str(exc))