feat(gobierno): GSUN-3, dos tercios para el tesoro (spec 21, pesos por certificar)
David, 2-nov-2026: «Sí a la súper-mayoría, dos tercios». - Una propuesta de tesoro se aprueba sólo si su Aye es al menos dos tercios del voto emitido (Aye + Nay; la abstención no cuenta), además del quórum y de la mayoría simple. - Se fija al crear la propuesta (storage TreasurySupermajorityOf) y se consume al decidirla. Las de tesoro abiertas antes de spec 21 conservan la mayoría simple con la que se abrieron. - Revertir el veto de una propuesta de tesoro exige los dos tercios frente a TODA la oposición: el Nay original más lo que sostuvo el veto en la ventana. Es el bloqueante de la revisión adversaria: con Nay 4Q y 2Q sosteniendo, 8Q pagaban cuando el voto ordinario pedía 12Q. - Evento nuevo ProposalRejectedForSupermajority, al final del enum. Sin migración y con transaction_version igual. - reaches_supermajority: exacta (aye por den >= num por emitido), monótona, sin Aye nunca aprueba y falla cerrada si desborda. Un const assert exige que la fracción sea mayor que 1/2. - Pruebas: 18 contrafactuales en rojo (17 en su aserción, 1 en compilación), 114 tests del pallet y 373 del workspace. - E2E: pasos 2c (la propuesta nueva guarda 2/3), 2d (la sembrada en spec 18 no) y 2e (la frontera del upgrade sobre todas las propuestas de tesoro). Falta antes de main: el certificado de Laurha para los tres pesos que cambian de esqueleto (submit_treasury_proposal, finalize_proposal, finalize_override), el E2E vivo y el upgrade. Sigue abierto, desde RV17: el override de una propuesta ORDINARIA no cuenta lo que sostiene. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6910f52b1e
commit
4be7da78c3
5 files changed
+486
-19
No files matched your search
@@ -23,6 +23,15 @@ Comprueba, en este orden (se detiene en el primer fallo, código de salida 1):
|
||||
el mismo para toda llamada: se mide con un treasury_deposit y el residuo tiene que
|
||||
coincidir. Prueba el cableado; que el número sea el coste real lo prueba el bench;
|
||||
2. Proposals(id) existe y TreasuryActions(id) = (Bob, monto), leído del storage por RPC;
|
||||
2b. el quórum guardado es el 10 % de lo que puede votar (spec 19);
|
||||
2c. desde spec 21 (GSUN-3), TreasurySupermajorityOf(id) = (2, 3): la propuesta de tesoro
|
||||
nueva guarda sus dos tercios. Contra spec 20 este paso reprueba;
|
||||
2d. la propuesta sembrada en spec 18 (SEMBRADA), si existe en esta cadena, NO tiene esa
|
||||
entrada: se decide con la mayoría simple con la que se abrió;
|
||||
2e. la frontera del upgrade: B es el primer bloque cuyo estado ya dice spec 21 (el del
|
||||
set_code, que aún ejecutó el runtime anterior). Toda propuesta de tesoro con
|
||||
start_block <= B no tiene marca, y toda la posterior la tiene. Se enumeran TODAS las
|
||||
de TreasuryActions, no sólo la sembrada (revisión adversarial de GSUN-3);
|
||||
3. tesoro:0 falla con TreasuryAmountBelowMinimum;
|
||||
4. reenviar la misma propuesta falla con ProposalIdCollision y TreasuryActions no cambia;
|
||||
5. sudo(treasury_disbursement) de esa propuesta, aún en votación, devuelve ProposalNotApproved
|
||||
@@ -45,6 +54,10 @@ TITULO = hashlib.blake2b(b"e2e-rv17/titulo" + (f"/{ETIQUETA}".encode() if ETIQUE
|
||||
# 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
|
||||
# El pago sembrado en la testnet de la casa el 28-oct (spec 18), que se cierra hacia el 26-nov.
|
||||
SEMBRADA = "fe92182ecff4df083b42da838a509c82aab7a4d0213a902e5a5a89729bd06ad0"
|
||||
# GSUN-3: la mayoría cualificada del tesoro, (num, den) como dos u32 en little endian.
|
||||
DOS_TERCIOS = "0x" + (2).to_bytes(4, "little").hex() + (3).to_bytes(4, "little").hex()
|
||||
BOB = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
|
||||
BOB_HEX = "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"
|
||||
|
||||
@@ -247,6 +260,24 @@ def main(sembrar_pago=False):
|
||||
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})")
|
||||
|
||||
# ORIGIN_E2E_EXIGE_SPEC=21 hace el paso aunque la cadena diga una spec anterior: es el
|
||||
# contrafactual (contra spec 20 tiene que reprobar en el storage, no en la versión).
|
||||
if v["specVersion"] >= 21 or os.environ.get("ORIGIN_E2E_EXIGE_SPEC", "") == "21":
|
||||
cualificada = rpc("state_getStorage", [clave("TreasurySupermajorityOf", id_hex)])["result"]
|
||||
if cualificada != DOS_TERCIOS:
|
||||
falla(f"TreasurySupermajorityOf(id) = {cualificada}, no (2, 3)")
|
||||
print("2c ✅ TreasurySupermajorityOf(id) = (2, 3): la propuesta de tesoro guarda sus dos tercios")
|
||||
if rpc("state_getStorage", [clave("Proposals", SEMBRADA)])["result"]:
|
||||
vieja = rpc("state_getStorage", [clave("TreasurySupermajorityOf", SEMBRADA)])["result"]
|
||||
if vieja is not None:
|
||||
falla(f"la propuesta sembrada en spec 18 tiene TreasurySupermajorityOf = {vieja}")
|
||||
if not rpc("state_getStorage", [clave("TreasuryActions", SEMBRADA)])["result"]:
|
||||
falla("la propuesta sembrada perdió su TreasuryActions")
|
||||
print(f"2d ✅ la sembrada 0x{SEMBRADA[:12]}… (spec 18) no tiene dos tercios: se decide con mayoría simple")
|
||||
else:
|
||||
print("2d — esta cadena no tiene la propuesta sembrada; no aplica")
|
||||
frontera_del_upgrade()
|
||||
|
||||
def falla_con(llamada, error, que):
|
||||
n, evs = enviar(llamada, genesis, v)
|
||||
idx = indice_de_error(error)
|
||||
@@ -271,6 +302,49 @@ def main(sembrar_pago=False):
|
||||
sembrar(id_hex, genesis, v)
|
||||
|
||||
|
||||
def frontera_del_upgrade():
|
||||
"""2e · B = primer bloque cuyo estado dice spec >= 21 (búsqueda binaria). Una propuesta de
|
||||
tesoro con start_block <= B la creó el runtime anterior y no lleva marca; una posterior,
|
||||
sí. Cualquier otra combinación reprueba."""
|
||||
def spec(n):
|
||||
return rpc("state_getRuntimeVersion", [rpc("chain_getBlockHash", [n])["result"]])["result"]["specVersion"]
|
||||
lo, hi = 0, best()
|
||||
if spec(hi) < 21:
|
||||
falla("2e: la cadena no llegó a spec 21")
|
||||
while lo < hi:
|
||||
mid = (lo + hi) // 2
|
||||
if spec(mid) >= 21:
|
||||
hi = mid
|
||||
else:
|
||||
lo = mid + 1
|
||||
b = lo
|
||||
pref = "0x" + twox128("Governance") + twox128("TreasuryActions")
|
||||
claves, desde = [], None
|
||||
while True:
|
||||
lote = rpc("state_getKeysPaged", [pref, 500, desde] if desde else [pref, 500])["result"]
|
||||
claves += lote
|
||||
if len(lote) < 500:
|
||||
break
|
||||
desde = lote[-1]
|
||||
antes, despues = [], []
|
||||
for k in claves:
|
||||
pid = k[-64:]
|
||||
p = rpc("state_getStorage", [clave("Proposals", pid)])["result"]
|
||||
if not p:
|
||||
falla(f"2e: TreasuryActions sin Proposals para 0x{pid[:12]}…")
|
||||
inicio = int.from_bytes(bytes.fromhex(p[2:])[144:152], "little")
|
||||
marca = rpc("state_getStorage", [clave("TreasurySupermajorityOf", pid)])["result"]
|
||||
if inicio <= b and marca is not None:
|
||||
falla(f"2e: 0x{pid[:12]}… es de antes del upgrade (#{inicio} <= #{b}) y tiene marca {marca}")
|
||||
if inicio > b and marca != DOS_TERCIOS:
|
||||
# Una ya decidida consumió su marca: sólo se exige en las que siguen en votación.
|
||||
if bytes.fromhex(p[2:])[160] == 0:
|
||||
falla(f"2e: 0x{pid[:12]}… es de después del upgrade (#{inicio} > #{b}), sigue en votación y no tiene (2, 3)")
|
||||
(antes if inicio <= b else despues).append(f"0x{pid[:8]}…@#{inicio}")
|
||||
print(f"2e ✅ frontera del upgrade en #{b}: {len(antes)} de antes sin marca ({', '.join(antes)}) · "
|
||||
f"{len(despues)} de después con (2, 3)")
|
||||
|
||||
|
||||
def sembrar(id_hex, genesis, v):
|
||||
"""Deja la propuesta lista para cerrar el camino feliz en vivo: fondea el vault y vota Aye
|
||||
con EXACTAMENTE el quórum guardado (la frontera `yes >= quórum`)."""
|
||||
|
||||
Reference in new issue
Block a user