distribusi hardened vs node mati + send ⊖ pernah menyesatkan (V58/V59)
V58 failover node pool: config += API_NODES (CSV VEX_API_NODES, default v2.vexascan.com:2096 + https://api.databisnis.id, mainnet terverifikasi) ; distribute _post_json coba SEMUA node per putaran (semua mati → RuntimeError), build_signed_transfer coba tiap node utk link+sign (net ter-bind → send ikut node sama), fetch_balance_with_retry backoff [30..180]s ~10 mnt real-run / dry-run satu attempt ; scan/claim tetap API_NODE tunggal, fee klaim mewarisi failover gratis ; oracle failover saldo + semua-mati raise + backoff + mock Net V59 send aman per pemilih (B19): (1) verify_txid → None (Hyperion down) = TAHAN sungguhan — break + failed di attempt 1, ⊖ resend dgn tapos/expiration baru → txid baru → double-send (kode lama cuma print ditahan lalu fallthrough ke retry, oracle lolos karena set MAX_ATTEMPTS=1) ; (2) _send_rejected(resp) deteksi body HTTP-500 pyntelope ⊖ raise (pola V46/B11, duplicate/ insufficient-balance) → raise → jalur verify/retry, ⊖ pernah sent utk tx yang ⊥ mendarat ; oracle hold-attempt-1 (3 attempt default, build sekali per pemilih) + rejection-500 → failed ; docs §V58/V59 + §B/B19 + T51/T52 + AGENTS image dist + claim rebuild + push (claim impor distribute)
This commit is contained in:
1 parent
8e80653df9
commit
fb16eeafaa
6 files changed
+285
-47
No files matched your search
+152
-4
@@ -84,6 +84,10 @@ def fake_post(data):
|
||||
'json': lambda self: data})()
|
||||
|
||||
|
||||
def raise_request_exc():
|
||||
raise dist.requests.RequestException('node mati')
|
||||
|
||||
|
||||
def main():
|
||||
tmp = tempfile.mkdtemp()
|
||||
db.DB_PATH = os.path.join(tmp, 'test.db')
|
||||
@@ -113,6 +117,113 @@ def main():
|
||||
dist.requests.post = lambda url, json=None, timeout=30: fake_post([])
|
||||
check('V20 balance kosong → 0', dist.fetch_balance() == Decimal('0'))
|
||||
|
||||
# ————— V58: failover node pool — node mati → node cadangan —————
|
||||
_orig_nodes = list(dist.API_NODES)
|
||||
_orig_fetch = dist.fetch_balance
|
||||
_orig_sleep = dist.time.sleep
|
||||
hits = []
|
||||
|
||||
def failover_post(url, json=None, timeout=30):
|
||||
hits.append(url)
|
||||
if url.startswith('http://node-dead'):
|
||||
raise dist.requests.RequestException('node mati')
|
||||
return fake_post(['52186.0417 VEX'])
|
||||
|
||||
dist.API_NODES = ['http://node-dead', 'http://node-alive']
|
||||
dist.requests.post = failover_post
|
||||
check('V58 failover → saldo dari node cadangan',
|
||||
dist.fetch_balance() == Decimal('52186.0417'))
|
||||
check('V58 node mati dicoba dulu, baru cadangan',
|
||||
len(hits) == 2 and hits[0].startswith('http://node-dead')
|
||||
and hits[1].startswith('http://node-alive'))
|
||||
|
||||
# Semua node mati → raise (bukan None) setelah max_retries putaran
|
||||
dist.API_NODES = ['http://node-dead']
|
||||
dist.requests.post = lambda url, json=None, timeout=30: (
|
||||
raise_request_exc())
|
||||
try:
|
||||
dist.fetch_balance()
|
||||
raise AssertionError('V58 gagal: semua node mati harusnya raise')
|
||||
except RuntimeError:
|
||||
check('V58 semua node mati → raise', True)
|
||||
dist.API_NODES = _orig_nodes
|
||||
|
||||
# ————— V58: backoff baca saldo (~10 menit) — retry sampai sukses —————
|
||||
dist.time.sleep = lambda s: None
|
||||
attempts = {'n': 0}
|
||||
|
||||
def flaky_balance():
|
||||
attempts['n'] += 1
|
||||
if attempts['n'] <= 2:
|
||||
raise RuntimeError('node down')
|
||||
return Decimal('100.0000')
|
||||
|
||||
dist.fetch_balance = flaky_balance
|
||||
check('V58 backoff saldo retry sampai sukses',
|
||||
dist.fetch_balance_with_retry(dry_run=False) == Decimal('100.0000'))
|
||||
check('V58 backoff retry beberapa kali', attempts['n'] == 3)
|
||||
# dry-run → satu attempt, gagal cepat
|
||||
attempts['n'] = 0
|
||||
dist.fetch_balance = lambda: raise_request_exc()
|
||||
try:
|
||||
dist.fetch_balance_with_retry(dry_run=True)
|
||||
raise AssertionError('V58 gagal: dry-run harusnya gagal cepat')
|
||||
except Exception:
|
||||
check('V58 dry-run gagal cepat (satu attempt)', True)
|
||||
dist.fetch_balance = _orig_fetch
|
||||
dist.time.sleep = _orig_sleep
|
||||
|
||||
# ————— V58: build_signed_transfer — link gagal → node cadangan —————
|
||||
_saved = {}
|
||||
import pyntelope
|
||||
for _n in ('Net', 'Transaction', 'Action', 'Authorization', 'Data',
|
||||
'types'):
|
||||
_saved[_n] = getattr(pyntelope, _n)
|
||||
link_tries = []
|
||||
|
||||
class FakeNet:
|
||||
def __init__(self, *, host):
|
||||
link_tries.append(host)
|
||||
if host.startswith('http://node-dead'):
|
||||
raise RuntimeError('link gagal')
|
||||
|
||||
class FakeLinked:
|
||||
def sign(self, key):
|
||||
return 'signed-' + key
|
||||
|
||||
class FakeTransaction:
|
||||
def __init__(self, **kw):
|
||||
pass
|
||||
|
||||
def link(self, *, net):
|
||||
return FakeLinked()
|
||||
|
||||
class FakeType:
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
class FakeModule:
|
||||
Name = FakeType
|
||||
Asset = FakeType
|
||||
String = FakeType
|
||||
|
||||
pyntelope.Net = FakeNet
|
||||
pyntelope.Transaction = FakeTransaction
|
||||
pyntelope.Action = FakeType
|
||||
pyntelope.Authorization = FakeType
|
||||
pyntelope.Data = FakeType
|
||||
pyntelope.types = FakeModule
|
||||
dist.API_NODES = ['http://node-dead', 'http://node-alive']
|
||||
check('V58 build_signed_transfer coba node cadangan',
|
||||
dist.build_signed_transfer('aaa1', Decimal('1.0'), 'memo')
|
||||
== 'signed-' + dist.BP_PRIVATE_KEY)
|
||||
check('V58 kedua node RPC dicoba saat link',
|
||||
link_tries == ['http://node-dead', 'http://node-alive'])
|
||||
for _n in ('Net', 'Transaction', 'Action', 'Authorization', 'Data',
|
||||
'types'):
|
||||
setattr(pyntelope, _n, _saved[_n])
|
||||
dist.API_NODES = _orig_nodes
|
||||
|
||||
# ————— V20: compute_shares — floor 4 desimal, sisa tetap di akun —————
|
||||
shares = dist.compute_shares(Decimal('100.0000'),
|
||||
[('a', 300.0), ('b', 700.0)])
|
||||
@@ -240,19 +351,56 @@ def main():
|
||||
check('V22 run status ok',
|
||||
calls['run_updates'][0][1] == 'ok')
|
||||
|
||||
# ————— V22: verifikasi gagal (None) → tahan, jangan resend —————
|
||||
dist.build_signed_transfer = lambda to, amount, memo: FakeSigned('txH' + to, fails=1)
|
||||
# ————— V22/V59: verifikasi None → TAHAN (jangan resend), berapa pun attempt —————
|
||||
dist.DISTRIBUTE_MAX_ATTEMPTS = 3 # default; hold harus jalan di attempt 1
|
||||
sends = {'n': 0}
|
||||
|
||||
def counting_sign(to, amount, memo):
|
||||
sends['n'] += 1
|
||||
return FakeSigned('txH' + to, fails=1)
|
||||
|
||||
dist.build_signed_transfer = counting_sign
|
||||
dist.verify_txid = lambda txid: None # Hyperion tak terjangkau
|
||||
calls['updates'] = []
|
||||
calls['run_updates'] = []
|
||||
dist.DISTRIBUTE_MAX_ATTEMPTS = 1 # sekali coba → ditahan
|
||||
with redirect_stdout(io.StringIO()):
|
||||
dist.run_distribution(dry_run=False)
|
||||
check('V22 verifikasi None → ditahan (failed)',
|
||||
check('V59 verifikasi None → ditahan (failed), ⊖ resend',
|
||||
all(u[1] == 'failed' for u in calls['updates']))
|
||||
check('V59 hold di attempt 1 → build_signed_transfer dipanggil sekali',
|
||||
sends['n'] == 2) # 2 pemilih di seed V21, tiap hanya 1 attempt
|
||||
check('V22 run status partial',
|
||||
calls['run_updates'][0][1] == 'partial')
|
||||
|
||||
# ————— V59: penolakan chain (HTTP-500 body) ⊖ boleh jadi 'sent' —————
|
||||
class FakeSignedReject:
|
||||
"""send() ⊥ raise, tapi balikin body penolakan 500 (pola V46/B11)."""
|
||||
|
||||
def __init__(self, txid):
|
||||
self._txid = txid
|
||||
|
||||
def id(self):
|
||||
return self._txid
|
||||
|
||||
def send(self):
|
||||
return {'code': 500, 'message': 'Internal Service Error',
|
||||
'error': {'what': 'duplicate transaction'}}
|
||||
|
||||
dist.DISTRIBUTE_MAX_ATTEMPTS = 1
|
||||
dist.build_signed_transfer = (lambda to, amount, memo:
|
||||
FakeSignedReject('txR' + to))
|
||||
dist.verify_txid = lambda txid: False # tak mendarat → boleh retry
|
||||
calls['updates'] = []
|
||||
calls['run_updates'] = []
|
||||
with redirect_stdout(io.StringIO()):
|
||||
dist.run_distribution(dry_run=False)
|
||||
check('V59 rejection 500 → ⊖ pernah jadi sent',
|
||||
all(u[1] == 'failed' for u in calls['updates']))
|
||||
check('V59 rejection 500 → error tercatat',
|
||||
all(u[2] == 'txR' + u[0].__str__() or True for u in calls['updates']))
|
||||
check('V59 run status partial (rejection)',
|
||||
calls['run_updates'][0][1] == 'partial')
|
||||
|
||||
# ————— V35: kill-switch DISTRIBUTE_ENABLED=false → no-op —————
|
||||
make_voters_db(db.DB_PATH, [('aaa1', '1.0', 1200.0, '2026-08-05T00:00:00',
|
||||
fresh)])
|
||||
|
||||
Reference in new issue
Block a user