feat(governance): RV15 — el peso de un voto sale del saldo, y la revision adversaria tumbo la primera version
Cierra L-10. cast_vote y override_council_veto sumaban el stake_weight que
DECLARABA el votante (hueco medido por Palace el 2026-09-06; test en rojo
publicado en d37a278). Ahora:
- El peso no puede superar el OGC LIBRE de la cuenta, y ese OGC queda
congelado (fungible freeze, FreezeReason::GovernanceVote) mientras el voto
pueda cambiar un resultado: hasta end_block en un voto normal y hasta el
cierre de la ventana en un override. El mismo OGC puede respaldar votos en
propuestas distintas (se solapan, como en Polkadot), pero no puede salir de
la cuenta: se cierra votar, transferir y votar otra vez desde otra cuenta.
- Un respaldo POR VOTO (VoteBacking: propuesta, tipo, importe, hasta), no uno
por cuenta. El freeze es siempre el maximo de los votos VIVOS; los vencidos
se descartan solos al votar o desbloquear. MaxActiveVotes = 32 en el runtime.
- unlock_vote_funds(who) lo puede llamar CUALQUIERA, como el unlock de
conviction-voting: quien vota con todo su saldo no puede pagar la comision de
su propio desbloqueo. Libera solo lo que ya no respalda un voto vivo.
- El override tiene ventana: 14 dias desde el veto que ALCANZA el umbral
(antes estaba abierto para siempre). Sin ventana registrada, falla cerrado.
- voting_period_blocks tiene tope: 28 dias (MAX_VOTING_PERIOD_BLOCKS).
- VoteFundsUnlocked va al FINAL del enum de eventos: no desplaza indices SCALE.
- spec_version 14 -> 15; transaction_version sigue en 1.
La revision adversaria (4 lentes independientes sobre la primera version)
encontro tres fallos serios, y los tres eran mios:
1. El lock unico por cuenta (max importe, max bloque) mas un periodo de voto
sin tope congelaba OGC PARA SIEMPRE: un voto de 1 unidad en una propuesta de
periodo u64::MAX arrastraba el voto mayor de la cuenta hasta ese bloque, y ni
el veto lo liberaba. Lo demostraron tres lentes por separado. Cura: respaldo
por voto + tope de periodo.
2. Un lock vencido y no desbloqueado revivia con el siguiente voto pequeno.
3. Votar con todo el saldo dejaba la cuenta sin poder pagar la comision del
unlock, y solo ella podia desbloquearse. Cura: unlock por terceros.
Y seis mutaciones sobrevivian a mi suite, entre ellas congelar el peso del
voto actual en vez del maximo (el propio ataque que el test decia cubrir).
Certificacion: 17 contrafactuales, TODOS en rojo con su pieza quitada
(freeze = peso actual, sin exigir saldo, reducible_balance, total_balance,
unlock en until, sin podar vencidos, ventana desde start_block, ventana 0,
peso 0 con thaw, evento released=0, unlock todo o nada, sin tope de periodo,
unlock solo propio, override hasta end_block, veto sin ventana, sin freeze, y
MaxFreezes=0 en el runtime). Cada test lleva control positivo. Test nuevo con
la configuracion REAL del runtime (preset local_testnet): el voto congela en
pallet-balances, lo congelado no sale, y un tercero lo libera al vencer.
Benchmarks en el PEOR caso real (la revision mostro que median la rama
barata): lista de respaldo casi llena, override que alcanza el umbral, veto de
un council de 7 que devuelve el bond, finalize que devuelve el bond, unlock
con la mitad vencida. Pesos regenerados con frame-omni-bencher (Mac; los
canonicos siguen pendientes del Pi 5, L-6). veto_proposal estaba
subdeclarado ~3x: 36M ps 10r/7w -> 112M 21r/10w, proof 6132 -> 16416.
regen-weights.sh: el blob de bench (runtime-benchmarks) se construye en
target-bench/ DENTRO del repo, nunca en target/release/wbuild, que es de donde
sale un set_code. Lo que debe coincidir con lo desplegado es el commit, no el
blob. Dentro del repo porque substrate-wasm-builder busca el Cargo.lock
subiendo desde el target: fuera no lo encuentra y resuelve dependencias
nuevas (medido).
Fuera de RV15, a RV16 (L-11): el override no es un 4/5 real (el bond que el
veto devuelve y el OGC Aye cuentan en el denominador y en el numerador), el
veto sigue siendo posible tras end_block, y un periodo de voto minimo.
Existian antes de RV15; los demostro la misma revision.
cargo test --workspace: 334 passed, 0 failed, 0 ignored.
cargo test -p origin-chain-governance --features runtime-benchmarks: 79 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d37a278ba6
commit
a694cbd9f0
10 files changed
+1097
-106
No files matched your search
@@ -531,6 +531,11 @@ parameter_types! {
|
||||
impl origin_chain_governance::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = crate::Balances;
|
||||
// RV15: el OGC que respalda un voto queda congelado (freeze) mientras el voto
|
||||
// pueda cambiar un resultado. `MaxFreezes` de Balances cuenta sola la variante.
|
||||
type RuntimeFreezeReason = RuntimeFreezeReason;
|
||||
// RV15: votos VIVOS simultáneos por cuenta (los vencidos se descartan solos).
|
||||
type MaxActiveVotes = frame_support::traits::ConstU32<32>;
|
||||
type TreasuryPalletId = GovernanceTreasuryPalletId;
|
||||
// Weights INTERIM hand-derived DB-aware (ver weights.rs). Regenerable en HW.
|
||||
type WeightInfo = origin_chain_governance::weights::SubstrateWeight<Runtime>;
|
||||
|
||||
@@ -604,4 +604,76 @@ mod tests {
|
||||
"el pool og/ecosy debe tener exactamente los 400M que la emisión libera"
|
||||
);
|
||||
}
|
||||
|
||||
/// RV15 con la configuración REAL del runtime (no el mock): un voto congela su
|
||||
/// OGC en `pallet-balances`, lo congelado no sale, y vencido el voto un TERCERO
|
||||
/// lo libera. Sin este test, un cambio de configuración (por ejemplo
|
||||
/// `MaxFreezes = 0`) haría fallar en producción todo voto con peso sin que
|
||||
/// ningún test del pallet lo viera (revisión adversaria de RV15).
|
||||
#[test]
|
||||
fn un_voto_congela_ogc_con_la_configuracion_real_del_runtime() {
|
||||
use crate::{Balances, Governance, RuntimeFreezeReason, RuntimeOrigin, System};
|
||||
use frame_support::assert_ok;
|
||||
use frame_support::traits::fungible::{Inspect, InspectFreeze};
|
||||
use origin_chain_governance::{FreezeReason, MIN_PROPOSAL_STAKE};
|
||||
use origin_chain_types::VoteStance;
|
||||
|
||||
let json = get_preset(&PresetId::from(PRESET_LOCAL_TESTNET)).expect("preset");
|
||||
let mut ext = sp_io::TestExternalities::new_empty();
|
||||
ext.execute_with(|| {
|
||||
frame_support::genesis_builder_helper::build_state::<RuntimeGenesisConfig>(json)
|
||||
.expect("el preset construye estado");
|
||||
System::set_block_number(1);
|
||||
let alice = account_from_sr25519(ALICE_SR25519);
|
||||
let bob = account_from_sr25519(BOB_SR25519);
|
||||
let charlie = account_from_sr25519(CHARLIE_SR25519);
|
||||
|
||||
let (titulo, cuerpo, periodo) = ([1u8; 32], [2u8; 32], 100u64);
|
||||
assert_ok!(Governance::submit_proposal(
|
||||
RuntimeOrigin::signed(alice.clone()),
|
||||
titulo,
|
||||
cuerpo,
|
||||
periodo,
|
||||
MIN_PROPOSAL_STAKE,
|
||||
));
|
||||
let id = origin_chain_governance::derive_proposal_id(
|
||||
&origin_chain_types::AccountId::new(ALICE_SR25519),
|
||||
&titulo,
|
||||
&cuerpo,
|
||||
);
|
||||
|
||||
let saldo = Balances::balance(&bob);
|
||||
assert!(saldo > 0, "el preset local debe fondear a Bob");
|
||||
assert_ok!(Governance::cast_vote(
|
||||
RuntimeOrigin::signed(bob.clone()),
|
||||
id,
|
||||
VoteStance::Aye,
|
||||
saldo,
|
||||
));
|
||||
let razon: RuntimeFreezeReason = FreezeReason::GovernanceVote.into();
|
||||
assert_eq!(Balances::balance_frozen(&razon, &bob), saldo);
|
||||
assert!(
|
||||
Balances::transfer_allow_death(
|
||||
RuntimeOrigin::signed(bob.clone()),
|
||||
charlie.clone().into(),
|
||||
1,
|
||||
)
|
||||
.is_err(),
|
||||
"salió OGC congelado por un voto vivo"
|
||||
);
|
||||
|
||||
System::set_block_number(1 + periodo as u32 + 1);
|
||||
assert_ok!(Governance::unlock_vote_funds(
|
||||
RuntimeOrigin::signed(charlie.clone()),
|
||||
bob.clone(),
|
||||
));
|
||||
assert_eq!(Balances::balance_frozen(&razon, &bob), 0);
|
||||
// Control positivo: liberado, el OGC vuelve a moverse.
|
||||
assert_ok!(Balances::transfer_allow_death(
|
||||
RuntimeOrigin::signed(bob),
|
||||
charlie.into(),
|
||||
1,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1019,7 +1019,7 @@ pub const VERSION: sp_version::RuntimeVersion = sp_version::RuntimeVersion {
|
||||
spec_name: alloc::borrow::Cow::Borrowed("origin-chain"),
|
||||
impl_name: alloc::borrow::Cow::Borrowed("origin-chain-charon"),
|
||||
authoring_version: 1,
|
||||
spec_version: 14,
|
||||
spec_version: 15,
|
||||
impl_version: 1,
|
||||
apis: RUNTIME_API_VERSIONS,
|
||||
transaction_version: 1,
|
||||
@@ -1087,7 +1087,7 @@ mod tests {
|
||||
/// mismo commit en que la version subio a 12.
|
||||
#[test]
|
||||
fn spec_version_vigente_y_transaction_version_intacta() {
|
||||
assert_eq!(RUNTIME_VERSION, 14);
|
||||
assert_eq!(RUNTIME_VERSION, 15);
|
||||
assert_eq!(TRANSACTION_VERSION, 1);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user