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.
This commit is contained in:
1 parent
41990af9af
commit
eb4f941b37
4 files changed
+17
-3
No files matched your search
@@ -20,7 +20,7 @@ Two tools scan the Vexanium blockchain voters table for accounts whose only vote
|
||||
- The node sometimes returns `staked` as a string instead of a number — normalize coerces to float (spec §V8).
|
||||
- The voters table has a sentinel first row (`owner="...........q"`, uint64 0) that must be skipped.
|
||||
- Authoritative vote weight is `last_vote_weight` (high-precision decimal string) — stored verbatim as TEXT, never float-converted (spec §V6).
|
||||
- Freshness filter (spec §V13): `last_vote` (estimated last re-vote date) is derived at scan time from the Vexanium weight formula `last_vote_weight = staked_raw × 2^(years since 2000)` → `log2(weight / (staked×10000))`. Voters whose `last_vote` is older than `VEX_STALE_DAYS` (default 28) — or can't be derived (zero/empty weight) — are dropped in `normalize()` and never stored; the dashboard therefore shows only fresh voters (no stale UI).
|
||||
- Freshness filter (spec §V13): `last_vote` (estimated last re-vote date) is derived at scan time from the Vexanium weight formula `last_vote_weight = staked_raw × 2^(years since 2000)` → `log2(weight / (staked×10000))`. Voters whose `last_vote` is older than `VEX_STALE_DAYS` (default 28) — or can't be derived (zero/empty weight) — are dropped in `normalize()` and never stored; the dashboard therefore shows only fresh voters (no stale UI). The heuristic over-reads when a voter unstaked without re-voting (weight ÷ smaller stake ⇒ future date), so `normalize()` clamps any `last_vote` past scan time down to `now`.
|
||||
- The web list shows RANK / AKUN / STAKE (VEX) / VOTE TERAKHIR columns (3 stats cells). Vote weight stays in the DB and `/api/search` JSON but is not rendered as a column.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -44,7 +44,7 @@ V9: normalize drops voters where `staked` < 1000 VEX (setelah ÷10000)
|
||||
V10: dashboard baca `voters.db` → rows sort `staked` DESC (terbesar dulu); ⊥ mutasi DB
|
||||
V11: search `q` → filter owner substring (LIKE, wildcard-escape, case-insensitive); urut+paging terjaga; `rank` global (⊥ rank-dalam-filter); `?q=` & `/api/search` read-only
|
||||
V12: produksi web disajikan gunicorn (`dashboard:app`, `gunicorn.conf.py`); `DASH_WORKERS` default 2; `.env` ikut termuat via import `config`
|
||||
V13: `last_vote` = `2000-01-01 + log2(last_vote_weight / (staked×10000)) × 365.25 hari` (None bila bobot/stake nol); voter dgn `last_vote=None` atau `< now − VEX_STALE_DAYS` dibuang di `normalize` (⊥ disimpan) → DB hanya berisi pemilih segar
|
||||
V13: `last_vote` = `2000-01-01 + log2(last_vote_weight / (staked×10000)) × 365.25 hari` (None bila bobot/stake nol); estimasi yang melewati `now` (unstake tanpa revote) diklamp ke `now`; voter dgn `last_vote=None` atau `< now − VEX_STALE_DAYS` dibuang di `normalize` (⊥ disimpan) → DB hanya berisi pemilih segar
|
||||
V15: web list = kolom RANK/AKUN/STAKE (VEX)/VOTE TERAKHIR — ⊥ BOBOT SUARA, ⊥ penanda/stat/filter basi; stats = 3 sel (TOTAL PEMILIH/TOTAL VEX/STAKE TERTINGGI); `/api/search` hasil tetap `{owner,staked,weight,rank,last_vote}` (weight disimpan & di-API, ⊥ dirender)
|
||||
|
||||
## §T — Tasks
|
||||
|
||||
+4
-1
@@ -118,7 +118,8 @@ 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.
|
||||
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)
|
||||
@@ -131,6 +132,8 @@ def normalize(row, now=None):
|
||||
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 {
|
||||
|
||||
@@ -147,6 +147,17 @@ def main():
|
||||
'staked': 20000000,
|
||||
'last_vote_weight': _fresh_weight(20000000)})['owner'] == 'new6')
|
||||
|
||||
# V13: estimasi yang melewati now (unstake tanpa revote) diklamp ke now
|
||||
fixed_now = datetime(2026, 8, 5, 12, 0, 0)
|
||||
t_future = ((fixed_now - gv.WEIGHT_EPOCH).days + 5) / 365.25
|
||||
fut = gv.normalize({'owner': 'fut9', 'producers': [gv.TARGET_BP],
|
||||
'staked': 20000000,
|
||||
'last_vote_weight': f'{20000000 * 2 ** t_future:.17f}'},
|
||||
now=fixed_now)
|
||||
check('V13 last_vote masa depan diklamp ke now',
|
||||
fut is not None
|
||||
and fut['last_vote'] == fixed_now.isoformat(timespec='seconds'))
|
||||
|
||||
# V9: staked < 1000 VEX → dibuang; tepat 1000 → disimpan
|
||||
check('V9 di bawah 1000 dibuang',
|
||||
gv.normalize({'owner': 'low9', 'producers': [gv.TARGET_BP],
|
||||
|
||||
Reference in new issue
Block a user