Files
databisnisid/get_voters.py
T
proitlab eb4f941b37 T13: klamp last_vote heuristik ke waktu scan (⊥ di masa depan)
Unstake tanpa revote membuat rasio bobot/stake membesar → estimasi
last_vote melewati now. normalize() kini mengklamp ke now; voters.db
dikembalikan (27 baris, MAX(last_vote) ≤ MAX(scanned_at)). Oracle lolos.
2026-08-05 10:21:45 +07:00

208 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Scan tabel voters Vexanium, simpan pemilih BP databisnisid + bobot suara ke SQLite.
Hanya pemilih segar (revote ≤ VEX_STALE_DAYS lalu) yang disimpan — vote basi
dibuang. Berjalan imparsial (repeatable) dan siap dijadwalkan (cron).
"""
import math
import sqlite3
import sys
import time
from datetime import datetime, timedelta, timezone
import requests
from config import API_NODE, DB_PATH, MIN_STAKED_VEX, TARGET_BP, VEX_STALE_DAYS
# Kontrak & scope sistem Vexanium = vexcore (BUKAN vexio sesuai konvensi EOS)
CODE = 'vexcore'
SCOPE = 'vexcore'
TABLE = 'voters'
# Ukuran halaman saat paginasi
LIMIT = 500
# Ambang-batas nama akun "0" (kodifikasi uint64 0) pada tabel voters
ZERO_OWNER = '...........q'
# Skala stake (×10000) dan epoch bobot suara Vexanium (1 Jan 2000)
VEX_SCALE = 10000
WEIGHT_EPOCH = datetime(2000, 1, 1)
def derive_last_vote(weight, staked_vex):
"""V13: perkiraan waktu revote terakhir dari rasio bobot/stake.
Vexanium: last_vote_weight = staked_raw × 2^(tahun sejak 2000).
Maka t = log2(last_vote_weight / staked_raw) → tanggal revote.
Kembalikan None bila tak bisa dihitung (bobot/stake nol atau tak valid).
"""
staked_raw = staked_vex * VEX_SCALE
if staked_raw <= 0:
return None
try:
ratio = float(weight) / staked_raw
except (TypeError, ValueError):
return None
if ratio <= 0:
return None
t_years = math.log(ratio) / math.log(2)
return (WEIGHT_EPOCH + timedelta(days=t_years * 365.25)).isoformat(timespec='seconds')
def fetch_with_retry(payload, max_retries=3, timeout=30):
"""POST ke RPC dengan retry (node publik sering timeout).
V1: ulang ≥3 kali pada timeout/error HTTP, jeda 2 detik antar percobaan.
"""
url = f'{API_NODE}/v1/chain/get_table_rows'
for attempt in range(max_retries):
try:
resp = requests.post(url, json=payload, timeout=timeout)
resp.raise_for_status()
return resp.json()
except requests.RequestException:
if attempt == max_retries - 1:
raise
print(f'[Peringatan] Gagal menghubungi node, mencoba lagi... '
f'({attempt + 1}/{max_retries})')
time.sleep(2)
def scan_voters():
"""Iterasi seluruh tabel voters (paginasi), kembalikan daftar mentah.
V2: majukan lower_bound ke owner terakhir; berhenti saat more=false | baris kosong.
V3: pada halaman kelanjutan, buang baris pertama (induk pamungkas duplikat).
"""
rows_all = []
lower_bound = ''
has_more = True
while has_more:
payload = {
'json': True,
'code': CODE,
'scope': SCOPE,
'table': TABLE,
'lower_bound': lower_bound,
'limit': LIMIT,
}
data = fetch_with_retry(payload)
rows = data.get('rows') or []
if not rows:
break
if lower_bound != '':
rows = rows[1:]
rows_all.extend(rows)
has_more = data.get('more', False)
if has_more:
lower_bound = rows_all[-1]['owner']
return rows_all
def _is_stale(last_vote, now):
"""V13: apakah revote terakhir sudah lewat ambang basi (VEX_STALE_DAYS)."""
try:
vote_at = datetime.fromisoformat(last_vote)
except (TypeError, ValueError):
return True
return (now - vote_at) > timedelta(days=VEX_STALE_DAYS)
def normalize(row, now=None):
"""V4/V5/V6/V13: saring ke baris yang relevan, bidang dinormalisasi.
V13: pemilih dengan last_vote tak diketahui atau basi dibuang di sini —
hanya pemilih segar yang berakhir di DB. Estimasi last_vote yang melewati
`now` (unstake tanpa revote → rasio bobot/stake membesar) diklamp ke `now`.
"""
if now is None:
now = datetime.now(timezone.utc).replace(tzinfo=None)
owner = row['owner']
if owner == ZERO_OWNER: # V4: lewati sentinel nama akun 0
return None
if not (row.get('producers') == [TARGET_BP]): # V5
return None
staked_vex = float(row.get('staked', 0)) / 10000 # V8/V9: bisa string, ÷10000
if staked_vex < MIN_STAKED_VEX:
return None
last_vote = derive_last_vote(row.get('last_vote_weight', ''), staked_vex)
if last_vote is not None and last_vote > now.isoformat(timespec='seconds'):
last_vote = now.isoformat(timespec='seconds') # V13: heuristik ⊥ boleh di masa depan
if last_vote is None or _is_stale(last_vote, now): # V13: basi ⊥ disimpan
return None
return {
'owner': owner,
'weight': row.get('last_vote_weight', ''), # V6: bobot verbatim (string)
'staked': staked_vex,
'last_vote': last_vote,
}
def persist(rows):
"""V7: ganti snapshot — DROP+INSERT tiap skan, ⊥ history/append.
WAL: journal_mode=WAL → pembaca (dashboard multi-worker) tak terblokir
saat DROP+INSERT harian. Mode ini persisten setelah diset.
"""
scanned_at = datetime.now().isoformat(timespec='seconds')
conn = sqlite3.connect(DB_PATH)
try:
conn.execute('PRAGMA journal_mode=WAL')
conn.execute('DROP TABLE IF EXISTS voters') # snapshot bersih
conn.execute(
'CREATE TABLE voters (' # I.db
'owner TEXT PRIMARY KEY, '
'weight TEXT NOT NULL, '
'staked REAL NOT NULL, '
'scanned_at TEXT NOT NULL, '
'last_vote TEXT)'
)
conn.executemany(
'INSERT INTO voters (owner, weight, staked, scanned_at, last_vote) '
'VALUES (?, ?, ?, ?, ?)',
[(r['owner'], r['weight'], r['staked'], scanned_at, r.get('last_vote'))
for r in rows],
)
conn.commit()
finally:
conn.close()
return scanned_at
def summarize(rows, scanned_at):
"""I.cmd: ringkasan stdout dalam bahasa Indonesia, format id-ID."""
print('\n=== HASIL AKHIR ===')
print(f'Total pemilih untuk BP [{TARGET_BP}]: {len(rows)}')
print(f'Waktu pemindaian: {scanned_at}')
print('\nDaftar Pemilih:')
if not rows:
print('(Tidak ada pemilih ditemukan untuk BP ini)')
return
for index, row in enumerate(rows, start=1):
print(f"{index}. {row['owner']} - Stake: "
f"{row['staked']:,.4f} VEX - Bobot: {row['weight']}")
def main():
"""Titik masuk CLI."""
print(f'Mulai memindai voters untuk BP: "{TARGET_BP}"...')
raw = scan_voters()
now = datetime.now(timezone.utc).replace(tzinfo=None)
filtered = []
for row in raw:
item = normalize(row, now=now) # satu stempel waktu untuk seluruh skan
if item is not None:
filtered.append(item)
scanned_at = persist(filtered)
summarize(filtered, scanned_at)
return 0
if __name__ == '__main__':
sys.exit(main())