feat(gobierno): spec 19, el quorum es el 10 % de lo que puede votar
Decision de David, 2026-10-29: «Si, del 10 % de lo que puede votar». Que puede votar: - Es la emision vigente menos el saldo que nadie puede votar ni recuperar: el pool de emision, el de subsidios y el vault de gobierno (cuentas_sin_llave en configs.rs). - Los bonds (og/props) y los depositos de pallet-revive NO se restan, porque vuelven enteros a una cuenta con llave. La revision adversaria de spec 19 lo demostro con sonda: restarlos dejaba bajar el quorum un 14 % en el genesis de mainnet, aparcando un bond en una propuesta de relleno y recuperandolo despues. Efecto: - En mainnet el quorum pasa de ~100M a ~35,1M OGC (10 % de 351M). - En la testnet de la casa apenas cambia. - Las propuestas abiertas conservan el quorum que guardaron al crearse. Pruebas: - quorums_del_runtime clasifica cada cuenta de pallet por su nombre. - aparcar_un_bond_no_baja_el_quorum reproduce la sonda con la configuracion real. - toda_cuenta_de_pallet_con_saldo_esta_clasificada vigila las cuentas modl del genesis. - 10 contrafactuales en rojo. - El E2E (paso 2b) descubre las cuentas de pallet recorriendo el storage, no copia la lista, y exige el quorum exacto. La comision se quema despues de despachar, y eso se mide en el BurnedDebt de la propia transaccion. Pesos: submit_proposal, submit_treasury_proposal y veto_proposal leen 3 cuentas mas. Medidos en la Pi: 5 procesos limpios, el nucleo calentado 3 s antes de cada uno, a 2,4 GHz. Operacion de la rp1, arrancar.sh: - en cada encendido desde el 26-oct el nodo moria al segundo porque la red no estaba; ahora espera a la ruta por defecto; - un cerrojo flock impide dos instancias; - el pid vivo se reconoce por su linea de comandos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b6d05eaf45
commit
158060bfe4
7 files changed
+355
-101
No files matched your search
@@ -34,6 +34,16 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from upgrade_en_vivo import RAIZ, TOOL, ALICE, rpc, best, eventos, falla # noqa: E402
|
||||
|
||||
GOV = os.path.join(RAIZ, "crates/origin-chain-governance/src")
|
||||
# ORIGIN_E2E_ETIQUETA cambia el título de la propuesta, y con él su id: hace falta para repetir el
|
||||
# E2E en una cadena que ya tiene la propuesta de una corrida anterior.
|
||||
ETIQUETA = os.environ.get("ORIGIN_E2E_ETIQUETA", "")
|
||||
TESORO = "tesoro:{}" + (f":{ETIQUETA}" if ETIQUETA else "")
|
||||
TITULO = hashlib.blake2b(b"e2e-rv17/titulo" + (f"/{ETIQUETA}".encode() if ETIQUETA else b""), digest_size=32).hexdigest()
|
||||
# Cuentas de pallet («modl» ++ PalletId ++ ceros) cuyo OGC vuelve a una cuenta con llave y por eso
|
||||
# SÍ cuenta como votable: los bonds y los depósitos de revive (configs.rs). Las demás cuentas de
|
||||
# pallet no votan. El E2E las DESCUBRE recorriendo las cuentas de la cadena, no copia la lista del
|
||||
# runtime: una omisión compartida pasaría en los dos.
|
||||
VUELVEN = {(b"modl" + pid).ljust(32, b"\0").hex() for pid in (b"og/props", b"py/reviv")}
|
||||
MONTO = 5 * 10**18 # 5 OGC
|
||||
BOB = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
|
||||
BOB_HEX = "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"
|
||||
@@ -136,6 +146,26 @@ def enviar(llamada, genesis, v):
|
||||
falla(f"{llamada}: no se incluyó en 60 s")
|
||||
|
||||
|
||||
def cuentas_de_pallet(bloque):
|
||||
"""Todas las cuentas «modl…» con entrada en System::Account en `bloque`, leídas del storage."""
|
||||
pref = "0x" + twox128("System") + twox128("Account")
|
||||
cuentas, desde = [], pref
|
||||
while True:
|
||||
ks = rpc("state_getKeysPaged", [pref, 1000, desde, bloque])["result"]
|
||||
cuentas += [k[-64:] for k in ks if k[-64:].startswith(b"modl".hex())]
|
||||
if len(ks) < 1000:
|
||||
return cuentas
|
||||
desde = ks[-1]
|
||||
|
||||
|
||||
def saldo_total(cuenta_hex, bloque=None):
|
||||
"""free + reserved de una cuenta, como `total_balance` del runtime."""
|
||||
k = "0x" + twox128("System") + twox128("Account") + hashlib.blake2b(bytes.fromhex(cuenta_hex), digest_size=16).hexdigest() + cuenta_hex
|
||||
v = rpc("state_getStorage", [k] + ([bloque] if bloque else []))["result"]
|
||||
d = bytes.fromhex(v[2:]) if v else b""
|
||||
return int.from_bytes(d[16:32], "little") + int.from_bytes(d[32:48], "little") if d else 0
|
||||
|
||||
|
||||
def saldo_libre(cuenta_hex):
|
||||
k = "0x" + twox128("System") + twox128("Account") + hashlib.blake2b(bytes.fromhex(cuenta_hex), digest_size=16).hexdigest() + cuenta_hex
|
||||
v = rpc("state_getStorage", [k])["result"]
|
||||
@@ -150,8 +180,8 @@ def main(sembrar_pago=False):
|
||||
falla("RV17 llega con la spec 18")
|
||||
|
||||
nonce = rpc("system_accountNextIndex", [ALICE])["result"]
|
||||
xt = firmar(f"tesoro:{MONTO}", nonce, genesis, v)
|
||||
titulo = hashlib.blake2b(b"e2e-rv17/titulo", digest_size=32).hexdigest()
|
||||
xt = firmar(TESORO.format(MONTO), nonce, genesis, v)
|
||||
titulo = TITULO
|
||||
if xt.count("0a10" + titulo) != 1:
|
||||
falla("no encuentro la llamada Governance 16 en el extrinsic")
|
||||
r = rpc("author_submitExtrinsic", [xt.replace("0a10" + titulo, "0a63" + titulo)])
|
||||
@@ -175,7 +205,7 @@ def main(sembrar_pago=False):
|
||||
|
||||
_, evs = enviar(f"depositar:{2 * MONTO}", genesis, v)
|
||||
extensiones = residuo(evs, "treasury_deposit")
|
||||
n, evs = enviar(f"tesoro:{MONTO}", genesis, v)
|
||||
n, evs = enviar(TESORO.format(MONTO), genesis, v)
|
||||
sub = [e for e in evs if "TreasuryProposalSubmitted" in e]
|
||||
if not sub:
|
||||
falla("tesoro: no hay TreasuryProposalSubmitted\n" + "\n".join(evs))
|
||||
@@ -195,6 +225,28 @@ def main(sembrar_pago=False):
|
||||
falla(f"TreasuryActions(id) = {acc}, no (Bob, {MONTO})")
|
||||
print(f"2 ✅ storage por RPC: Proposals(id) existe · TreasuryActions(id) = (Bob, {MONTO})")
|
||||
|
||||
# 2b · el quórum guardado es el 10 % de lo que PUEDE votar (David, 29-oct): la emisión menos
|
||||
# las cuentas sin llave, al despachar. Se reconstruye desde el estado final del bloque más lo
|
||||
# que quemó la propia transacción DESPUÉS de despacharla: la comisión se retira antes
|
||||
# (`Withdraw`) pero se quema al final (`BurnedDebt`), así que al leer el quórum la emisión
|
||||
# todavía la incluye (medido en el ensayo de spec 19: la diferencia era BurnedDebt / 10).
|
||||
h = rpc("chain_getBlockHash", [n])["result"]
|
||||
leer = lambda k: rpc("state_getStorage", [k, h])["result"] # noqa: E731
|
||||
emision = int.from_bytes(bytes.fromhex(leer("0x" + twox128("Balances") + twox128("TotalIssuance"))[2:]), "little")
|
||||
sin_llave = sum(saldo_total(c, h) for c in cuentas_de_pallet(h) if c not in VUELVEN)
|
||||
guardado = int.from_bytes(bytes.fromhex(rpc("state_getStorage", [clave("VoteQuorumOf", id_hex)])["result"][2:]), "little")
|
||||
quemado = sum(int(m) for e in evs for m in re.findall(r"BurnedDebt \{ amount: (\d+)", e))
|
||||
if not quemado:
|
||||
falla("no encuentro el BurnedDebt de la comisión\n" + "\n".join(evs))
|
||||
emision += quemado
|
||||
esperado = -(-(emision - sin_llave) // 10)
|
||||
if guardado != esperado:
|
||||
falla(f"quórum guardado {guardado} ≠ 10 % de lo votable {esperado}")
|
||||
if v["specVersion"] >= 19 and guardado == -(-emision // 10):
|
||||
falla("el quórum es el 10 % de la emisión TOTAL, no de lo que puede votar")
|
||||
print(f"2b ✅ quórum guardado {guardado / 10**18:,.6f} OGC = 10 % de lo que puede votar "
|
||||
f"({(emision - sin_llave) / 10**18:,.4f} OGC; las cuentas sin llave guardan {sin_llave / 10**18:,.4f})")
|
||||
|
||||
def falla_con(llamada, error, que):
|
||||
n, evs = enviar(llamada, genesis, v)
|
||||
idx = indice_de_error(error)
|
||||
@@ -204,8 +256,8 @@ def main(sembrar_pago=False):
|
||||
falla(f"{llamada}: se esperaba {error} (índice {idx})\n{texto}")
|
||||
print(f"{que} ✅ #{n}: {llamada.split(':')[0]} → {error} (Module 10, error {idx})")
|
||||
|
||||
falla_con("tesoro:0", "TreasuryAmountBelowMinimum", "3")
|
||||
falla_con(f"tesoro:{MONTO * 2}", "ProposalIdCollision", "4")
|
||||
falla_con(TESORO.format(0), "TreasuryAmountBelowMinimum", "3")
|
||||
falla_con(TESORO.format(MONTO * 2), "ProposalIdCollision", "4")
|
||||
if rpc("state_getStorage", [clave("TreasuryActions", id_hex)])["result"] != acc:
|
||||
falla("el reenvío cambió TreasuryActions(id)")
|
||||
print(" ✅ TreasuryActions(id) sigue siendo (Bob, monto original)")
|
||||
|
||||
Reference in new issue
Block a user