From a694cbd9f06a2a87d7a5582ae8cf47eaa6aaec24 Mon Sep 17 00:00:00 2001 From: Charon Date: Thu, 24 Sep 2026 12:03:44 -0500 Subject: [PATCH] =?UTF-8?q?feat(governance):=20RV15=20=E2=80=94=20el=20pes?= =?UTF-8?q?o=20de=20un=20voto=20sale=20del=20saldo,=20y=20la=20revision=20?= =?UTF-8?q?adversaria=20tumbo=20la=20primera=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 3 + .../src/benchmarking.rs | 114 +++- crates/origin-chain-governance/src/lib.rs | 258 ++++++++- crates/origin-chain-governance/src/mock.rs | 6 + .../src/tests_integration.rs | 488 +++++++++++++++++- crates/origin-chain-governance/src/weights.rs | 229 +++++--- crates/origin-chain-runtime/src/configs.rs | 5 + .../src/genesis_config_presets.rs | 72 +++ crates/origin-chain-runtime/src/lib.rs | 4 +- scripts/bench/regen-weights.sh | 24 +- 10 files changed, 1097 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index 7f764e9..7117276 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ contracts/*/Cargo.lock # Local config .env .env.local + +# Blob de benchmarks (runtime-benchmarks): NUNCA se despliega. Ver scripts/bench/regen-weights.sh +/target-bench diff --git a/crates/origin-chain-governance/src/benchmarking.rs b/crates/origin-chain-governance/src/benchmarking.rs index 0f8f638..a60c0e5 100644 --- a/crates/origin-chain-governance/src/benchmarking.rs +++ b/crates/origin-chain-governance/src/benchmarking.rs @@ -15,6 +15,7 @@ use super::*; use frame_benchmarking::v2::*; use frame_support::traits::fungible::{Inspect, Mutate}; +use frame_support::traits::Get; use frame_system::RawOrigin; use origin_chain_types::AccountId; @@ -52,6 +53,34 @@ fn seed_council(who: &T::AccountId) { CouncilSize::::put(1u32); } +/// RV15: siembra `n` votos vivos respaldados por `who` (propuestas distintas del +/// id de los benchmarks), con `until` dado. Es el peor caso de PoV: la lista de +/// respaldo se lee y se reescribe entera. +fn seed_backing(who: &T::AccountId, n: u32, until: u64) { + for i in 0..n { + Pallet::::back_vote_with_funds( + who, + ProposalId([100u8.wrapping_add(i as u8); 32]), + BackingKind::Vote, + 1_000u128.saturating_add(i as u128), + until, + ) + .expect("respaldo sembrado"); + } +} + +/// Bond real de la propuesta `id`, con fondos en la cuenta-bond: la rama cara +/// del veto y del finalize es la que lo devuelve. +fn seed_bond(id: ProposalId) -> T::AccountId { + let proposer: T::AccountId = account("proposer", 0, 0); + let _ = T::Currency::mint_into( + &Pallet::::proposal_bond_account(), + MIN_PROPOSAL_STAKE.saturating_mul(2), + ); + ProposalBond::::insert(id, (proposer.clone(), MIN_PROPOSAL_STAKE)); + proposer +} + const EM_TITLE: [u8; 32] = [3u8; 32]; const EM_BODY: [u8; 32] = [4u8; 32]; @@ -73,6 +102,14 @@ mod benchmarks { #[benchmark] fn cast_vote() { let caller: T::AccountId = whitelisted_caller(); + // RV15: el peso sale del saldo. Peor caso: la cuenta ya respalda + // MaxActiveVotes - 1 votos vivos (lista y freeze que leer y reescribir). + fund::(&caller); + seed_backing::( + &caller, + T::MaxActiveVotes::get().saturating_sub(1), + 1_000_000u64, + ); let id = ProposalId([7u8; 32]); seed_proposal::(id, ProposalStatus::Voting, 1_000_000u64); @@ -87,6 +124,8 @@ mod benchmarks { let caller: T::AccountId = whitelisted_caller(); let id = ProposalId([7u8; 32]); seed_proposal::(id, ProposalStatus::Voting, 0u64); + // Rama cara: devuelve el bond (transfer real desde la cuenta-bond). + let proposer = seed_bond::(id); // current_block debe ser > end_block(0) → avanza a 2. frame_system::Pallet::::set_block_number(2u32.into()); @@ -97,6 +136,8 @@ mod benchmarks { Proposals::::get(id).unwrap().status, ProposalStatus::Voting )); + assert!(!ProposalBond::::contains_key(id)); + assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE); } #[benchmark] @@ -122,15 +163,43 @@ mod benchmarks { #[benchmark] fn veto_proposal() { + // Rama cara: council LLENO (7), 4 vetos vivos previos, y el del caller es el + // 5.º que alcanza el umbral 2/3 → recorre todos los vetos, flipa a Rejected, + // abre la ventana de override (RV15) y devuelve el bond. let caller: T::AccountId = whitelisted_caller(); - seed_council::(&caller); let id = ProposalId([7u8; 32]); seed_proposal::(id, ProposalStatus::Voting, 1_000_000u64); + let proposer = seed_bond::(id); + let mut miembros: alloc::vec::Vec = (0..MAX_COUNCIL_SIZE - 1) + .map(|i| account("council", i, 0)) + .collect(); + miembros.push(caller.clone()); + for m in &miembros { + Council::::insert( + m, + CouncilMember { + account: AccountId::new([0u8; 32]), + joined_at_block: 0, + role: CouncilRole::Member, + }, + ); + } + CouncilSize::::put(MAX_COUNCIL_SIZE); + let previos = council_veto_threshold(MAX_COUNCIL_SIZE) - 1; + for m in miembros.iter().take(previos as usize) { + VetoVotes::::insert(id, m, ()); + } #[extrinsic_call] veto_proposal(RawOrigin::Signed(caller.clone()), id); assert!(VetoVotes::::contains_key(id, &caller)); + assert!(matches!( + Proposals::::get(id).unwrap().status, + ProposalStatus::Rejected + )); + assert!(OverrideDeadline::::contains_key(id)); + assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE); } #[benchmark] @@ -160,11 +229,24 @@ mod benchmarks { let id = ProposalId([7u8; 32]); seed_proposal::(id, ProposalStatus::Rejected, 0u64); VetoRejected::::insert(id, ()); + // RV15: ventana abierta, peso respaldado por saldo y la lista de respaldo casi + // llena (peor caso). Y rama cara: el override ALCANZA el umbral y aprueba. + OverrideDeadline::::insert(id, 1_000_000u64); + fund::(&caller); + seed_backing::( + &caller, + T::MaxActiveVotes::get().saturating_sub(1), + 1_000_000u64, + ); #[extrinsic_call] - override_council_veto(RawOrigin::Signed(caller.clone()), id, 100u128); + override_council_veto(RawOrigin::Signed(caller.clone()), id, MIN_PROPOSAL_STAKE); assert!(OverrideVotes::::contains_key(id, &caller)); + assert!(matches!( + Proposals::::get(id).unwrap().status, + ProposalStatus::Approved + )); } #[benchmark] @@ -222,5 +304,33 @@ mod benchmarks { assert!(EmergencyEndorsements::::contains_key(content_id, &caller)); } + #[benchmark] + fn unlock_vote_funds() { + // Peor caso: lista de respaldo LLENA, la mitad vencida (until = 1) y la otra + // mitad viva → se poda, se recalcula el máximo y se reescribe la lista. + let caller: T::AccountId = whitelisted_caller(); + let votante: T::AccountId = account("votante", 0, 0); + fund::(&votante); + let max = T::MaxActiveVotes::get(); + let vencidos = max / 2; + seed_backing::(&votante, vencidos, 1u64); + for i in vencidos..max { + Pallet::::back_vote_with_funds( + &votante, + ProposalId([100u8.wrapping_add(i as u8); 32]), + BackingKind::Vote, + 500u128, + 1_000_000u64, + ) + .expect("respaldo vivo"); + } + frame_system::Pallet::::set_block_number(2u32.into()); + + #[extrinsic_call] + unlock_vote_funds(RawOrigin::Signed(caller), votante.clone()); + + assert_eq!(VoteLocks::::get(&votante).len() as u32, max - vencidos); + } + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/crates/origin-chain-governance/src/lib.rs b/crates/origin-chain-governance/src/lib.rs index 55f5bfb..48830c3 100644 --- a/crates/origin-chain-governance/src/lib.rs +++ b/crates/origin-chain-governance/src/lib.rs @@ -124,7 +124,7 @@ sp_api::decl_runtime_apis! { #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; - use frame_support::traits::fungible::{Inspect, Mutate}; + use frame_support::traits::fungible::{freeze, Inspect, Mutate}; use frame_support::traits::tokens::Preservation; use frame_support::PalletId; use frame_system::pallet_prelude::*; @@ -192,6 +192,22 @@ pub mod pallet { /// 14 days × 24 hours × 600 blocks/hour (@ 6s slot Aura) = 201,600 blocks. pub const DEFAULT_VOTING_PERIOD_BLOCKS: u64 = 14 * 24 * 600; + /// RV15: ventana para revertir un veto del council, contada desde el bloque en + /// que el veto alcanza su umbral. Mide lo mismo que un voto normal (14 días). + /// + /// Antes no había ventana: una propuesta vetada aceptaba votos de override para + /// siempre, así que podía resucitarse años después. Y con peso real (RV15) el + /// OGC que respalda un override tiene que liberarse ALGÚN día: sin ventana, o se + /// congelaba para siempre o el voto dejaba de estar respaldado. + pub const OVERRIDE_WINDOW_BLOCKS: u64 = DEFAULT_VOTING_PERIOD_BLOCKS; + + /// RV15: periodo de voto máximo que puede pedir un proponente (28 días). + /// + /// Antes era libre hasta `u64::MAX`. Con el voto respaldado por OGC eso era un + /// arma: una propuesta de periodo casi eterno congelaba casi para siempre el OGC + /// de quien votara en ella (demostrado por la revisión adversaria de RV15). + pub const MAX_VOTING_PERIOD_BLOCKS: u64 = 2 * DEFAULT_VOTING_PERIOD_BLOCKS; + /// Emergency voting period canonical canonical 48 hours per CHN-4 5.7. /// 48 hours × 600 blocks/hour (@ 6s slot Aura) = 28,800 blocks. pub const EMERGENCY_VOTING_PERIOD_BLOCKS: u64 = 48 * 600; @@ -208,6 +224,35 @@ pub mod pallet { (total_stake.saturating_mul(4).saturating_add(4)) / 5 } + /// RV15: qué respalda un tramo de OGC congelado. + #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, RuntimeDebug)] + pub enum BackingKind { + /// Un voto del periodo normal (`cast_vote`). + Vote, + /// Un voto para revertir un veto (`override_council_veto`). + Override, + } + + /// RV15: el OGC que respalda UN voto vivo. + /// + /// Un registro por voto, no uno por cuenta. La primera versión fusionaba todo + /// en un único lock (máximo importe hasta el bloque más tardío) y la revisión + /// adversaria demostró por qué no sirve: un voto mínimo en una propuesta de + /// periodo enorme arrastraba el voto MAYOR de la cuenta hasta ese bloque, para + /// siempre, y un lock vencido revivía con el siguiente voto. Con un registro por + /// voto, cada tramo vence solo y el freeze es siempre el MÁXIMO DE LOS VIVOS. + #[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, RuntimeDebug)] + pub struct VoteBacking { + /// Propuesta a la que respalda. + pub proposal: ProposalId, + /// Voto normal u override. + pub kind: BackingKind, + /// OGC que respalda este voto. + pub amount: u128, + /// Último bloque (inclusive) en que este voto aún puede cambiar el resultado. + pub until: u64, + } + /// Proposal canonical struct stored on-chain per pallet (Foundation OIP). #[derive( Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, @@ -248,7 +293,21 @@ pub mod pallet { /// FIX-6 (V2-CA2/V3-C3): Currency real (fungible) para mover OGC de la /// tesorería. La tesorería deja de ser un contador u128 y pasa a ser una /// cuenta real respaldada por pallet_balances. - type Currency: Inspect + Mutate; + /// + /// RV15: además congela (`freeze`) el OGC que respalda un voto, para que el + /// mismo OGC no pueda votar dos veces pasando por una transferencia. + type Currency: Inspect + + Mutate + + freeze::Mutate; + + /// RV15: el enum agregado de razones de congelación del runtime. + type RuntimeFreezeReason: From; + + /// RV15: cuántos votos VIVOS puede respaldar una cuenta a la vez. Los + /// vencidos se descartan solos al votar o desbloquear, así que el límite es + /// de votos simultáneos, no históricos. + #[pallet::constant] + type MaxActiveVotes: Get; /// FIX-6: PalletId del que se deriva la cuenta-tesorería (vault). Cuenta /// determinística sin private key — nadie la controla por wallet, solo la @@ -271,6 +330,33 @@ pub mod pallet { #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); + /// RV15: por qué este pallet congela OGC. + #[pallet::composite_enum] + pub enum FreezeReason { + /// OGC que respalda un voto (o un override) que todavía puede cambiar un + /// resultado. + #[codec(index = 0)] + GovernanceVote, + } + + /// RV15: los votos vivos que respalda cada cuenta (ver [`VoteBacking`]). El + /// freeze `GovernanceVote` de la cuenta es el máximo de sus `amount`. + #[pallet::storage] + pub type VoteLocks = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec, + ValueQuery, + >; + + /// RV15: último bloque (inclusive) en que se acepta un override sobre una + /// propuesta vetada. Se escribe cuando el veto alcanza su umbral. Sin entrada, + /// no hay override posible: falla cerrado. + #[pallet::storage] + pub type OverrideDeadline = + StorageMap<_, Blake2_128Concat, ProposalId, u64, OptionQuery>; + /// CHN-4 canonical: `Proposals>` canonical. #[pallet::storage] #[pallet::getter(fn proposal_of)] @@ -557,6 +643,17 @@ pub mod pallet { /// Treasury balance canonical post-disbursement. remaining_balance: u128, }, + /// RV15: se liberó OGC que respaldaba votos que ya no pueden cambiar un + /// resultado. Va AL FINAL del enum a propósito: insertarlo en medio desplazaba + /// el índice SCALE de tres eventos existentes (revisión adversaria de RV15). + VoteFundsUnlocked { + /// Cuenta cuyo OGC se libera. + who: T::AccountId, + /// OGC que deja de estar congelado. + released: u128, + /// OGC que sigue congelado por votos todavía vivos. + still_frozen: u128, + }, } /// Pallet errors canonical. @@ -616,6 +713,22 @@ pub mod pallet { /// Audit 2026-07-04 (B): el council member ya endosó esta propuesta de /// emergencia (anti doble-endoso). AlreadyEndorsed, + /// RV15: el peso del voto supera el OGC de la cuenta. El peso sale del + /// saldo, no de lo que declara el votante (hueco medido por Palace el + /// 2026-09-06). + VoteWeightExceedsBalance, + /// RV15: la ventana para revertir este veto ya cerró (o la propuesta no + /// tiene ventana registrada). + OverrideWindowClosed, + /// RV15: la cuenta no tiene OGC congelado por votos. + NoVoteLock, + /// RV15: ninguno de los votos de la cuenta ha vencido todavía; su OGC sigue + /// congelado. + VoteFundsStillLocked, + /// RV15: el periodo de voto pedido supera `MAX_VOTING_PERIOD_BLOCKS`. + VotingPeriodTooLong, + /// RV15: la cuenta ya respalda `MaxActiveVotes` votos vivos. + TooManyActiveVotes, } /// Minimum stake canonical proposal submission per CHN-4 5.3. @@ -646,6 +759,10 @@ pub mod pallet { ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(stake >= MIN_PROPOSAL_STAKE, Error::::StakeInsufficient); + ensure!( + voting_period_blocks <= MAX_VOTING_PERIOD_BLOCKS, + Error::::VotingPeriodTooLong + ); let proposer_acct = AccountId::new(account_bytes::(&who)); let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash); @@ -731,6 +848,17 @@ pub mod pallet { Error::::AlreadyVoted ); + // RV15: el peso sale del saldo y queda congelado hasta que el voto ya no + // pueda cambiar el resultado (fin del periodo de voto). Sin el freeze, + // el mismo OGC votaría otra vez desde otra cuenta tras una transferencia. + Self::back_vote_with_funds( + &who, + proposal_id, + BackingKind::Vote, + stake_weight, + proposal.end_block, + )?; + match stance { VoteStance::Aye => { proposal.yes_weight = proposal.yes_weight.saturating_add(stake_weight) @@ -948,6 +1076,11 @@ pub mod pallet { // Marca: rechazado por VETO del council (no por voto popular) → // única vía elegible para override_council_veto. VetoRejected::::insert(proposal_id, ()); + // RV15: el override tiene ventana, contada desde este bloque. + OverrideDeadline::::insert( + proposal_id, + current_block_u64::().saturating_add(OVERRIDE_WINDOW_BLOCKS), + ); // Audit 2026-07-04: la propuesta salió de Voting (vetada) → devuelve // el bond al proponente (si no, el stake de una propuesta vetada // quedaría atrapado para siempre — no hay withdraw dispatchable). @@ -1060,6 +1193,22 @@ pub mod pallet { Error::::AlreadyOverrideVoted ); + // RV15: el override tiene ventana, y su peso sale del saldo y queda + // congelado hasta que la ventana cierre. + let deadline = + OverrideDeadline::::get(proposal_id).ok_or(Error::::OverrideWindowClosed)?; + ensure!( + current_block_u64::() <= deadline, + Error::::OverrideWindowClosed + ); + Self::back_vote_with_funds( + &who, + proposal_id, + BackingKind::Override, + stake_weight, + deadline, + )?; + OverrideVotes::::insert(proposal_id, &who, stake_weight); let new_total = OverrideStakeTotal::::get(proposal_id) .saturating_add(stake_weight); @@ -1296,6 +1445,111 @@ pub mod pallet { }); Ok(()) } + + /// RV15: libera el OGC que respaldaba votos de `who` que ya no pueden cambiar + /// un resultado (pasó su `until`). Lo que respalda votos todavía vivos sigue + /// congelado. + /// + /// Lo puede llamar CUALQUIERA, como `unlock` de `pallet-conviction-voting`: la + /// condición es objetiva, y quien votó con todo su saldo no puede pagar la + /// comisión de su propio desbloqueo (revisión adversaria de RV15). + #[pallet::call_index(13)] + #[pallet::weight(T::WeightInfo::unlock_vote_funds())] + pub fn unlock_vote_funds(origin: OriginFor, who: T::AccountId) -> DispatchResult { + ensure_signed(origin)?; + let mut backing = VoteLocks::::get(&who); + ensure!(!backing.is_empty(), Error::::NoVoteLock); + let before = max_backing(&backing); + let now = current_block_u64::(); + let n = backing.len(); + backing.retain(|b| b.until >= now); + ensure!(backing.len() < n, Error::::VoteFundsStillLocked); + let after = max_backing(&backing); + Self::apply_vote_freeze(&who, backing, after)?; + Self::deposit_event(Event::VoteFundsUnlocked { + who, + released: before.saturating_sub(after), + still_frozen: after, + }); + Ok(()) + } + } + + /// El mayor OGC respaldado por una lista de votos (0 si está vacía). + fn max_backing(backing: &[VoteBacking]) -> u128 { + backing.iter().map(|b| b.amount).max().unwrap_or(0) + } + + /// Bloque actual como `u64` (el tipo de `end_block` y de las ventanas). + fn current_block_u64() -> u64 { + >::block_number().saturated_into::() + } + + /// RV15 — el OGC que respalda un voto. + impl Pallet { + /// Exige que `weight` no supere el OGC LIBRE de `who` y lo deja congelado + /// mientras este voto pueda cambiar un resultado (hasta `until`). + /// + /// Antes descarta los respaldos ya vencidos de la cuenta, así que un voto + /// muerto nunca vuelve a congelarse. El freeze resultante es el MÁXIMO de los + /// votos vivos: el mismo OGC puede respaldar votos en propuestas distintas + /// (se solapan, como en Polkadot), pero no puede salir de la cuenta mientras + /// un voto que respalda siga vivo. Eso cierra votar, transferir y votar otra + /// vez desde otra cuenta. + /// + /// Un peso 0 no congela nada ni toca el respaldo que ya hubiera. + /// + /// Límite conocido: el freeze de `pallet-balances` se cumple sobre libre + + /// retenido. Si la cuenta tiene OGC en hold (staking), lo retenido puede + /// sostener el freeze mientras lo libre sale: en la práctica el OGC en hold + /// termina respaldando el voto, que es la semántica de Polkadot. No infla el + /// total: ninguna cuenta respalda más de lo que posee. + pub(crate) fn back_vote_with_funds( + who: &T::AccountId, + proposal: ProposalId, + kind: BackingKind, + weight: u128, + until: u64, + ) -> DispatchResult { + ensure!( + weight <= T::Currency::balance(who), + Error::::VoteWeightExceedsBalance + ); + if weight == 0 { + return Ok(()); + } + let now = current_block_u64::(); + let mut backing = VoteLocks::::get(who); + backing.retain(|b| b.until >= now); + backing + .try_push(VoteBacking { + proposal, + kind, + amount: weight, + until, + }) + .map_err(|_| Error::::TooManyActiveVotes)?; + let frozen = max_backing(&backing); + Self::apply_vote_freeze(who, backing, frozen) + } + + /// Deja el freeze `GovernanceVote` de `who` en `frozen` y guarda su respaldo; + /// sin votos vivos, descongela y borra el registro. + fn apply_vote_freeze( + who: &T::AccountId, + backing: BoundedVec, + frozen: u128, + ) -> DispatchResult { + let reason = FreezeReason::GovernanceVote.into(); + if backing.is_empty() { + >::thaw(&reason, who)?; + VoteLocks::::remove(who); + } else { + >::set_freeze(&reason, who, frozen)?; + VoteLocks::::insert(who, backing); + } + Ok(()) + } } /// FIX-6 — helpers de la tesorería real (cuenta respaldada por pallet_balances). diff --git a/crates/origin-chain-governance/src/mock.rs b/crates/origin-chain-governance/src/mock.rs index de5a4fd..ae4ded8 100644 --- a/crates/origin-chain-governance/src/mock.rs +++ b/crates/origin-chain-governance/src/mock.rs @@ -31,6 +31,9 @@ impl frame_system::Config for Test { impl pallet_balances::Config for Test { type AccountStore = System; type Balance = u128; + // RV15: governance congela el OGC que respalda un voto. + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = frame_support::traits::VariantCountOf; } frame_support::parameter_types! { @@ -42,6 +45,9 @@ frame_support::parameter_types! { impl origin_chain_governance::Config for Test { type RuntimeEvent = RuntimeEvent; type Currency = Balances; + type RuntimeFreezeReason = RuntimeFreezeReason; + // Bajo a propósito: el límite de votos vivos tiene que poder probarse. + type MaxActiveVotes = frame_support::traits::ConstU32<4>; type TreasuryPalletId = TestTreasuryPalletId; type WeightInfo = (); } diff --git a/crates/origin-chain-governance/src/tests_integration.rs b/crates/origin-chain-governance/src/tests_integration.rs index 4e21609..647c832 100644 --- a/crates/origin-chain-governance/src/tests_integration.rs +++ b/crates/origin-chain-governance/src/tests_integration.rs @@ -17,7 +17,8 @@ use crate::mock::{ alice_account, alice_origin, bob_account, bob_origin, charlie_account, charlie_origin, - new_test_ext, run_to_block, Balances, Governance, RuntimeEvent, System, Test, INITIAL_BALANCE, + new_test_ext, run_to_block, Balances, Governance, RuntimeEvent, RuntimeFreezeReason, + RuntimeOrigin, System, Test, INITIAL_BALANCE, }; use crate::pallet::{Proposal, ProposalStatus, MIN_PROPOSAL_STAKE}; use crate::{derive_proposal_id, Error, Event, Proposals, Votes}; @@ -1145,19 +1146,12 @@ fn scenario_10_lifecycle_walkthrough_canonical() { }); } -/// HUECO ABIERTO — medido por Palace el 2026-09-06, confirmado por Charon el -/// 2026-09-23 sobre `63918f3`. `cast_vote` (y `override_council_veto`) suman el -/// `stake_weight` que DECLARA el votante sin contrastarlo con nada: el diseño dice -/// «1 OGC = 1 voto» (doc de `lib.rs:403`) y el código es un contador que suma lo que -/// le digan. Hoy no drena la tesorería porque `treasury_disbursement` exige además -/// `ensure_root`, pero cualquiera aprueba o bloquea una propuesta con `u128::MAX`, -/// y el día que se retire el sudo (L-7) y la gobernanza ejecute (L-8) sí la drena. -/// -/// El `ignore` NO lo esconde: `cargo test -- --ignored` lo enseña en rojo, y -/// QUITARLO es el criterio de cierre de RV15 (peso derivado del saldo + un freeze -/// para que el mismo OGC no vote dos veces pasando por una transferencia). +/// CERRADO en RV15. Hueco medido por Palace el 2026-09-06 y confirmado por Charon el +/// 2026-09-23 sobre `63918f3`: `cast_vote` (y `override_council_veto`) sumaban el +/// `stake_weight` que DECLARA el votante sin contrastarlo con nada. Este test estuvo +/// publicado en rojo (`d37a278`, con `#[ignore]`) antes de la cura; quitar el +/// `ignore` era el criterio de cierre. #[test] -#[ignore = "RV15: cast_vote acepta el peso que declara el votante (hueco medido 2026-09-06)"] fn un_voto_no_puede_pesar_mas_que_el_saldo_del_votante() { new_test_ext().execute_with(|| { let id = submit_default(alice_origin()); @@ -1184,3 +1178,471 @@ fn un_voto_no_puede_pesar_mas_que_el_saldo_del_votante() { assert_eq!(p.yes_weight, saldo, "un voto legítimo no contó entero"); }); } + +// ═══════════════════════════════════════════════════════════════ +// RV15 — el peso de un voto sale del saldo y ese OGC queda congelado mientras +// el voto pueda cambiar un resultado. Cada test lleva su control positivo, y +// cada uno mata al menos una mutación que la revisión adversaria de RV15 vio +// sobrevivir a la primera versión de esta suite. +// ═══════════════════════════════════════════════════════════════ + +/// OGC congelado HOY por votos de gobernanza (lo que mide `pallet-balances`, no +/// el registro del pallet: la revisión mostró que mirar solo el registro deja +/// vivo un freeze equivocado). +fn congelado_por_votos(who: &sp_runtime::AccountId32) -> u128 { + use frame_support::traits::fungible::InspectFreeze; + let razon: RuntimeFreezeReason = crate::FreezeReason::GovernanceVote.into(); + Balances::balance_frozen(&razon, who) +} + +/// Otra propuesta de Alice, con su propio periodo. +fn propuesta_con_periodo(semilla: u8, periodo: u64) -> ProposalId { + let (t, b) = ([semilla; 32], [semilla.wrapping_add(100); 32]); + assert_ok!(Governance::submit_proposal( + alice_origin(), + t, + b, + periodo, + MIN_PROPOSAL_STAKE, + )); + derive_proposal_id(&AccountId::new(alice_account_bytes()), &t, &b) +} + +fn fin_de(id: ProposalId) -> u64 { + Proposals::::get(id).expect("propuesta").end_block +} + +/// El mismo OGC no puede votar dos veces pasando por una transferencia: el que +/// respalda un voto vivo no sale de la cuenta. +#[test] +fn el_ogc_que_respalda_un_voto_no_sale_de_la_cuenta_mientras_el_voto_vive() { + new_test_ext().execute_with(|| { + let id = submit_default(alice_origin()); + let saldo = Balances::free_balance(bob_account()); + let peso = saldo / 2; + assert_ok!(Governance::cast_vote( + bob_origin(), + id, + VoteStance::Aye, + peso + )); + assert_eq!(congelado_por_votos(&bob_account()), peso); + + let r = Balances::transfer_allow_death(bob_origin(), charlie_account(), saldo - peso + 1); + assert!( + r.is_err(), + "salió de la cuenta OGC congelado por un voto vivo" + ); + assert_eq!(Balances::free_balance(bob_account()), saldo); + + // Control positivo: lo que NO respalda el voto se mueve libre. + assert_ok!(Balances::transfer_allow_death( + bob_origin(), + charlie_account(), + saldo - peso + )); + assert_eq!(Balances::free_balance(bob_account()), peso); + }); +} + +/// Con varios votos vivos, el freeze REAL es el mayor, no el último. Primero la +/// propuesta larga con más peso, luego la corta con menos: congelar el peso del +/// voto actual dejaría 300 y liberaría 200 que respaldan un voto vivo. +#[test] +fn el_freeze_real_es_el_mayor_de_los_votos_vivos_no_el_ultimo() { + new_test_ext().execute_with(|| { + let corta = submit_default(alice_origin()); + let larga = propuesta_con_periodo(11, 3 * VOTING_PERIOD); + let saldo = Balances::free_balance(bob_account()); + assert_ok!(Governance::cast_vote( + bob_origin(), + larga, + VoteStance::Nay, + 500 + )); + assert_ok!(Governance::cast_vote( + bob_origin(), + corta, + VoteStance::Aye, + 300 + )); + assert_eq!(congelado_por_votos(&bob_account()), 500); + + // El ataque con dinero real: sacar los 200 que respaldan el voto largo. + assert!( + Balances::transfer_allow_death(bob_origin(), charlie_account(), saldo - 300).is_err() + ); + // Control positivo: lo no respaldado sí sale. + assert_ok!(Balances::transfer_allow_death( + bob_origin(), + charlie_account(), + saldo - 500 + )); + + // Acabada la corta, el voto largo sigue vivo: nada que liberar. + run_to_block(fin_de(corta) + 1); + assert_ok!(Governance::unlock_vote_funds(bob_origin(), bob_account())); + assert_eq!(congelado_por_votos(&bob_account()), 500); + }); +} + +/// Al vencer el voto grande, se libera lo que ya no respalda nada y sigue +/// congelado lo que respalda el voto pequeño todavía vivo. +#[test] +fn al_vencer_el_voto_grande_solo_queda_congelado_el_pequeno_que_sigue_vivo() { + new_test_ext().execute_with(|| { + let corta = submit_default(alice_origin()); + let larga = propuesta_con_periodo(11, 3 * VOTING_PERIOD); + assert_ok!(Governance::cast_vote( + bob_origin(), + larga, + VoteStance::Aye, + 300 + )); + assert_ok!(Governance::cast_vote( + bob_origin(), + corta, + VoteStance::Aye, + 500 + )); + assert_eq!(congelado_por_votos(&bob_account()), 500); + + run_to_block(fin_de(corta)); + assert_noop!( + Governance::unlock_vote_funds(bob_origin(), bob_account()), + Error::::VoteFundsStillLocked + ); + + run_to_block(fin_de(corta) + 1); + assert_ok!(Governance::unlock_vote_funds(bob_origin(), bob_account())); + assert_eq!(congelado_por_votos(&bob_account()), 300); + System::assert_last_event(RuntimeEvent::Governance(Event::VoteFundsUnlocked { + who: bob_account(), + released: 200, + still_frozen: 300, + })); + + run_to_block(fin_de(larga) + 1); + assert_ok!(Governance::unlock_vote_funds(bob_origin(), bob_account())); + assert_eq!(congelado_por_votos(&bob_account()), 0); + assert!(crate::VoteLocks::::get(bob_account()).is_empty()); + System::assert_last_event(RuntimeEvent::Governance(Event::VoteFundsUnlocked { + who: bob_account(), + released: 300, + still_frozen: 0, + })); + }); +} + +/// El OGC se libera cuando el voto ya no puede cambiar el resultado: en el último +/// bloque del periodo todavía se puede votar, así que sigue congelado. +#[test] +fn el_ogc_se_libera_solo_cuando_el_voto_ya_no_puede_cambiar_el_resultado() { + new_test_ext().execute_with(|| { + let id = submit_default(alice_origin()); + let saldo = Balances::free_balance(bob_account()); + assert_noop!( + Governance::unlock_vote_funds(bob_origin(), bob_account()), + Error::::NoVoteLock + ); + assert_ok!(Governance::cast_vote( + bob_origin(), + id, + VoteStance::Nay, + saldo + )); + + run_to_block(fin_de(id)); + assert_noop!( + Governance::unlock_vote_funds(bob_origin(), bob_account()), + Error::::VoteFundsStillLocked + ); + + run_to_block(fin_de(id) + 1); + assert_ok!(Governance::unlock_vote_funds(bob_origin(), bob_account())); + assert_eq!(congelado_por_votos(&bob_account()), 0); + // Control positivo: liberado, el OGC se mueve entero. + assert_ok!(Balances::transfer_allow_death( + bob_origin(), + charlie_account(), + saldo + )); + }); +} + +/// Quien votó con todo su saldo no puede pagar la comisión de su propio +/// desbloqueo; por eso cualquiera puede desbloquear un voto vencido ajeno. +#[test] +fn un_tercero_libera_el_voto_vencido_de_otra_cuenta() { + new_test_ext().execute_with(|| { + let id = submit_default(alice_origin()); + let saldo = Balances::free_balance(bob_account()); + assert_ok!(Governance::cast_vote( + bob_origin(), + id, + VoteStance::Aye, + saldo + )); + + // Control negativo: el tercero no puede liberar un voto vivo. + assert_noop!( + Governance::unlock_vote_funds(charlie_origin(), bob_account()), + Error::::VoteFundsStillLocked + ); + + run_to_block(fin_de(id) + 1); + assert_ok!(Governance::unlock_vote_funds( + charlie_origin(), + bob_account() + )); + assert_eq!(congelado_por_votos(&bob_account()), 0); + }); +} + +/// Un proponente no puede pedir un periodo casi eterno: con el voto respaldado +/// por OGC, eso congelaría casi para siempre el OGC de quien votara. +#[test] +fn una_propuesta_no_puede_pedir_un_periodo_de_voto_eterno() { + new_test_ext().execute_with(|| { + use crate::pallet::{DEFAULT_VOTING_PERIOD_BLOCKS, MAX_VOTING_PERIOD_BLOCKS}; + assert_eq!(MAX_VOTING_PERIOD_BLOCKS, 2 * DEFAULT_VOTING_PERIOD_BLOCKS); + for eterno in [u64::MAX, MAX_VOTING_PERIOD_BLOCKS + 1] { + assert_noop!( + Governance::submit_proposal( + alice_origin(), + TITLE_HASH, + BODY_HASH, + eterno, + MIN_PROPOSAL_STAKE, + ), + Error::::VotingPeriodTooLong + ); + } + // Control positivo: el máximo exacto se acepta. + assert_ok!(Governance::submit_proposal( + alice_origin(), + TITLE_HASH, + BODY_HASH, + MAX_VOTING_PERIOD_BLOCKS, + MIN_PROPOSAL_STAKE, + )); + }); +} + +/// Un voto vencido que nadie desbloqueó no revive con el siguiente voto. +#[test] +fn un_voto_vencido_no_revive_con_el_siguiente() { + new_test_ext().execute_with(|| { + let corta = submit_default(alice_origin()); + assert_ok!(Governance::cast_vote( + bob_origin(), + corta, + VoteStance::Aye, + 500 + )); + run_to_block(fin_de(corta) + 1); + let nueva = propuesta_con_periodo(12, VOTING_PERIOD); + assert_ok!(Governance::cast_vote( + bob_origin(), + nueva, + VoteStance::Aye, + 1 + )); + + assert_eq!(congelado_por_votos(&bob_account()), 1); + let respaldo = crate::VoteLocks::::get(bob_account()); + assert_eq!(respaldo.len(), 1); + assert_eq!(respaldo[0].proposal, nueva); + }); +} + +/// El mismo OGC, ENTERO, vota en dos propuestas (se solapan, como en Polkadot); +/// lo que no puede es pesar más que el saldo. +#[test] +fn el_mismo_ogc_entero_vota_en_dos_propuestas() { + new_test_ext().execute_with(|| { + let p1 = submit_default(alice_origin()); + let p2 = propuesta_con_periodo(11, 3 * VOTING_PERIOD); + let saldo = Balances::free_balance(bob_account()); + assert_ok!(Governance::cast_vote( + bob_origin(), + p1, + VoteStance::Aye, + saldo + )); + assert_noop!( + Governance::cast_vote(bob_origin(), p2, VoteStance::Nay, saldo + 1), + Error::::VoteWeightExceedsBalance + ); + assert_ok!(Governance::cast_vote( + bob_origin(), + p2, + VoteStance::Nay, + saldo + )); + assert_eq!(congelado_por_votos(&bob_account()), saldo); + assert_eq!(Proposals::::get(p1).expect("p1").yes_weight, saldo); + assert_eq!(Proposals::::get(p2).expect("p2").nay_weight, saldo); + }); +} + +/// Vota el OGC LIBRE: lo que está en reserva/hold no suma peso al votar. +#[test] +fn lo_que_esta_en_reserva_no_vota() { + new_test_ext().execute_with(|| { + use frame_support::traits::ReservableCurrency; + let id = submit_default(alice_origin()); + let saldo = Balances::free_balance(bob_account()); + assert_ok!(>::reserve( + &bob_account(), + saldo / 2 + )); + let libre = Balances::free_balance(bob_account()); + assert_noop!( + Governance::cast_vote(bob_origin(), id, VoteStance::Aye, libre + 1), + Error::::VoteWeightExceedsBalance + ); + assert_ok!(Governance::cast_vote( + bob_origin(), + id, + VoteStance::Aye, + libre + )); + }); +} + +/// Un voto de peso 0 cuenta como postura y no toca el respaldo que ya hubiera. +#[test] +fn un_voto_de_peso_cero_no_toca_el_freeze_previo() { + new_test_ext().execute_with(|| { + let p1 = submit_default(alice_origin()); + let p2 = propuesta_con_periodo(11, VOTING_PERIOD); + assert_ok!(Governance::cast_vote( + bob_origin(), + p2, + VoteStance::Abstain, + 0 + )); + assert!(Votes::::contains_key(p2, &bob_account())); + assert!(crate::VoteLocks::::get(bob_account()).is_empty()); + + assert_ok!(Governance::cast_vote( + bob_origin(), + p1, + VoteStance::Aye, + 500 + )); + let p3 = propuesta_con_periodo(12, VOTING_PERIOD); + assert_ok!(Governance::cast_vote(bob_origin(), p3, VoteStance::Nay, 0)); + assert_eq!(congelado_por_votos(&bob_account()), 500); + assert_eq!(crate::VoteLocks::::get(bob_account()).len(), 1); + }); +} + +/// La ventana de override dura 14 días (literal, no la propia constante) y se +/// cuenta desde el veto que ALCANZA el umbral, no desde el inicio de la +/// propuesta ni desde el primer veto. Y su peso también sale del saldo. +#[test] +fn la_ventana_de_override_dura_14_dias_desde_el_veto_que_alcanza_el_umbral() { + new_test_ext().execute_with(|| { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + let dave = sp_runtime::AccountId32::new([4u8; 32]); + let eve = sp_runtime::AccountId32::new([5u8; 32]); + for (m, rol) in [ + (alice_account(), CouncilRole::Chair), + (dave.clone(), CouncilRole::Member), + (eve.clone(), CouncilRole::Member), + ] { + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + m, + rol + )); + } + let id = submit_default(alice_origin()); // bloque 1 + run_to_block(10); + assert_ok!(Governance::veto_proposal(alice_origin(), id)); // 1 de 2 + assert!(crate::OverrideDeadline::::get(id).is_none()); + run_to_block(60); + assert_ok!(Governance::veto_proposal(RuntimeOrigin::signed(dave), id)); // 2 de 2 + + let cierre = 60 + 201_600; + assert_eq!(crate::OverrideDeadline::::get(id), Some(cierre)); + + let saldo = Balances::free_balance(bob_account()); + assert_noop!( + Governance::override_council_veto(bob_origin(), id, saldo + 1), + Error::::VoteWeightExceedsBalance + ); + run_to_block(cierre + 1); + assert_noop!( + Governance::override_council_veto(bob_origin(), id, 1), + Error::::OverrideWindowClosed + ); + // Control positivo: en el último bloque de la ventana sí, y congela hasta el cierre. + run_to_block(cierre); + assert_ok!(Governance::override_council_veto(bob_origin(), id, 1)); + assert_eq!(congelado_por_votos(&bob_account()), 1); + let respaldo = crate::VoteLocks::::get(bob_account()); + assert_eq!(respaldo[0].until, cierre); + assert_eq!(respaldo[0].kind, crate::BackingKind::Override); + }); +} + +/// Un veto sin ventana registrada (lo que dejaría una migración mal hecha) no +/// admite override: falla cerrado. +#[test] +fn un_veto_sin_ventana_registrada_no_admite_override() { + new_test_ext().execute_with(|| { + let id = submit_default(alice_origin()); + let mut p = Proposals::::get(id).expect("propuesta"); + p.status = ProposalStatus::Rejected; + Proposals::::insert(id, p); + crate::VetoRejected::::insert(id, ()); + assert_noop!( + Governance::override_council_veto(bob_origin(), id, 1), + Error::::OverrideWindowClosed + ); + }); +} + +/// Una cuenta respalda como mucho `MaxActiveVotes` votos VIVOS (4 en el mock); los +/// vencidos no ocupan sitio. +#[test] +fn el_limite_es_de_votos_vivos_y_los_vencidos_no_ocupan_sitio() { + new_test_ext().execute_with(|| { + let corta = submit_default(alice_origin()); + let mut largas = Vec::new(); + for semilla in 11..15u8 { + largas.push(propuesta_con_periodo(semilla, 3 * VOTING_PERIOD)); + } + assert_ok!(Governance::cast_vote( + bob_origin(), + corta, + VoteStance::Aye, + 10 + )); + for id in &largas[..3] { + assert_ok!(Governance::cast_vote( + bob_origin(), + *id, + VoteStance::Aye, + 10 + )); + } + assert_noop!( + Governance::cast_vote(bob_origin(), largas[3], VoteStance::Aye, 10), + Error::::TooManyActiveVotes + ); + // Control positivo: vencida la corta, su sitio queda libre. + run_to_block(fin_de(corta) + 1); + assert_ok!(Governance::cast_vote( + bob_origin(), + largas[3], + VoteStance::Aye, + 10 + )); + assert_eq!(crate::VoteLocks::::get(bob_account()).len(), 4); + }); +} diff --git a/crates/origin-chain-governance/src/weights.rs b/crates/origin-chain-governance/src/weights.rs index 1e9d157..e86182d 100644 --- a/crates/origin-chain-governance/src/weights.rs +++ b/crates/origin-chain-governance/src/weights.rs @@ -14,17 +14,17 @@ //! procedimiento completo y el hardware al que hay que creerle). //! //! GENERADO POR SUBSTRATE BENCHMARK CLI VERSION 51.0.0 -//! FECHA: 2026-09-09, STEPS: `50`, REPEAT: `20` +//! FECHA: 2026-09-24, STEPS: `50`, REPEAT: `20` //! HOSTNAME: `MacBookProM3Max.local`, CPU: `` //! WASM-EXECUTION: `Compiled`, DB CACHE: 1024 // Comando ejecutado: -// /Users/davidherran/.cargo/bin/frame-omni-bencher +// frame-omni-bencher // v1 // benchmark // pallet // --runtime -// /Users/davidherran/originmetaverse/artifacts/origin-chain/origin_chain_runtime_rv12_bench.compact.compressed.wasm +// /Users/davidherran/originmetaverse/external/arkhe-origin-chain/target-bench/release/wbuild/origin-chain-runtime/origin_chain_runtime.compact.compressed.wasm // --genesis-builder-preset // development // --pallet @@ -78,6 +78,8 @@ pub trait WeightInfo { fn treasury_deposit() -> Weight; /// Peso de `endorse_emergency_proposal` — MEDIDO sobre el WASM, no estimado a mano. fn endorse_emergency_proposal() -> Weight; + /// Peso de `unlock_vote_funds` — MEDIDO sobre el WASM, no estimado a mano. + fn unlock_vote_funds() -> Weight; } /// Weights parametrizados por el `DbWeight` real del runtime. @@ -103,8 +105,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `107` // Estimado: `3722` - // Tiempo minimo de ejecucion: 46_000_000 picosegundos. - Weight::from_parts(46_000_000, 0) + // Tiempo minimo de ejecucion: 54_000_000 picosegundos. + Weight::from_parts(55_000_000, 0) .saturating_add(Weight::from_parts(0, 3722)) .saturating_add(T::DbWeight::get().reads(7_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) @@ -117,6 +119,12 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `Governance::Votes` (r:1 w:1) /// Proof: `Governance::Votes` (`max_values`: None, `max_size`: Some(97), added: 2572, mode: `MaxEncodedLen`) + /// Storage: `Governance::VoteLocks` (r:1 w:1) + /// Proof: `Governance::VoteLocks` (`max_values`: None, `max_size`: Some(1873), added: 4348, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:0) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) /// Storage: `System::ExecutionPhase` (r:1 w:0) /// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) /// Storage: `System::EventCount` (r:1 w:1) @@ -125,20 +133,24 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn cast_vote() -> Weight { // Proof size en bytes: - // Medido: `310` - // Estimado: `3722` - // Tiempo minimo de ejecucion: 16_000_000 picosegundos. - Weight::from_parts(18_000_000, 0) - .saturating_add(Weight::from_parts(0, 3722)) - .saturating_add(T::DbWeight::get().reads(7_u64)) - .saturating_add(T::DbWeight::get().writes(4_u64)) + // Medido: `2320` + // Estimado: `5338` + // Tiempo minimo de ejecucion: 46_000_000 picosegundos. + Weight::from_parts(48_000_000, 0) + .saturating_add(Weight::from_parts(0, 5338)) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) } /// Storage: `Governance::Proposals` (r:1 w:1) /// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`) /// Storage: `System::Number` (r:1 w:0) /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Governance::ProposalBond` (r:1 w:0) + /// Storage: `Governance::ProposalBond` (r:1 w:1) /// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`) + /// Storage: `Balances::TotalIssuance` (r:1 w:0) + /// Proof: `Balances::TotalIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `System::ExecutionPhase` (r:1 w:0) /// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) /// Storage: `System::EventCount` (r:1 w:1) @@ -147,13 +159,13 @@ impl WeightInfo for SubstrateWeight { /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) fn finalize_proposal() -> Weight { // Proof size en bytes: - // Medido: `310` - // Estimado: `3722` - // Tiempo minimo de ejecucion: 13_000_000 picosegundos. - Weight::from_parts(14_000_000, 0) - .saturating_add(Weight::from_parts(0, 3722)) - .saturating_add(T::DbWeight::get().reads(6_u64)) - .saturating_add(T::DbWeight::get().writes(3_u64)) + // Medido: `614` + // Estimado: `6196` + // Tiempo minimo de ejecucion: 61_000_000 picosegundos. + Weight::from_parts(62_000_000, 0) + .saturating_add(Weight::from_parts(0, 6196)) + .saturating_add(T::DbWeight::get().reads(9_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) } /// Storage: `Governance::CouncilSize` (r:1 w:1) /// Proof: `Governance::CouncilSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) @@ -171,8 +183,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `25` // Estimado: `3554` - // Tiempo minimo de ejecucion: 10_000_000 picosegundos. - Weight::from_parts(11_000_000, 0) + // Tiempo minimo de ejecucion: 12_000_000 picosegundos. + Weight::from_parts(12_000_000, 0) .saturating_add(Weight::from_parts(0, 3554)) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) @@ -193,43 +205,49 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `153` // Estimado: `3554` - // Tiempo minimo de ejecucion: 11_000_000 picosegundos. - Weight::from_parts(12_000_000, 0) + // Tiempo minimo de ejecucion: 13_000_000 picosegundos. + Weight::from_parts(14_000_000, 0) .saturating_add(Weight::from_parts(0, 3554)) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(4_u64)) } - /// Storage: `Governance::Council` (r:1 w:0) + /// Storage: `Governance::Council` (r:5 w:0) /// Proof: `Governance::Council` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) /// Storage: `Governance::Proposals` (r:1 w:1) /// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`) /// Storage: `Governance::CouncilSize` (r:1 w:0) /// Proof: `Governance::CouncilSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) - /// Storage: `Governance::VetoVotes` (r:2 w:1) + /// Storage: `Governance::VetoVotes` (r:6 w:1) /// Proof: `Governance::VetoVotes` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`) - /// Storage: `Governance::ProposalBond` (r:1 w:0) - /// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`) /// Storage: `System::Number` (r:1 w:0) /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Governance::ProposalBond` (r:1 w:1) + /// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`) + /// Storage: `Balances::TotalIssuance` (r:1 w:0) + /// Proof: `Balances::TotalIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) /// Storage: `System::ExecutionPhase` (r:1 w:0) /// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) /// Storage: `System::EventCount` (r:1 w:1) /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) /// Storage: `System::Events` (r:1 w:1) /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Governance::OverrideDeadline` (r:0 w:1) + /// Proof: `Governance::OverrideDeadline` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) /// Storage: `Governance::VetoRejected` (r:0 w:1) /// Proof: `Governance::VetoRejected` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `Governance::VetoCount` (r:0 w:1) /// Proof: `Governance::VetoCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) fn veto_proposal() -> Weight { // Proof size en bytes: - // Medido: `435` - // Estimado: `6132` - // Tiempo minimo de ejecucion: 28_000_000 picosegundos. - Weight::from_parts(30_000_000, 0) - .saturating_add(Weight::from_parts(0, 6132)) - .saturating_add(T::DbWeight::get().reads(10_u64)) - .saturating_add(T::DbWeight::get().writes(6_u64)) + // Medido: `1440` + // Estimado: `16416` + // Tiempo minimo de ejecucion: 109_000_000 picosegundos. + Weight::from_parts(112_000_000, 0) + .saturating_add(Weight::from_parts(0, 16416)) + .saturating_add(T::DbWeight::get().reads(21_u64)) + .saturating_add(T::DbWeight::get().writes(10_u64)) } /// Storage: `Governance::AdvisorFlags` (r:1 w:1) /// Proof: `Governance::AdvisorFlags` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) @@ -265,13 +283,13 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `98` // Estimado: `3513` - // Tiempo minimo de ejecucion: 10_000_000 picosegundos. - Weight::from_parts(10_000_000, 0) + // Tiempo minimo de ejecucion: 11_000_000 picosegundos. + Weight::from_parts(11_000_000, 0) .saturating_add(Weight::from_parts(0, 3513)) .saturating_add(T::DbWeight::get().reads(5_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } - /// Storage: `Governance::Proposals` (r:1 w:0) + /// Storage: `Governance::Proposals` (r:1 w:1) /// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`) /// Storage: `Governance::VetoRejected` (r:1 w:0) /// Proof: `Governance::VetoRejected` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) @@ -279,17 +297,33 @@ impl WeightInfo for SubstrateWeight { /// Proof: `Governance::AdvisorFlags` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) /// Storage: `Governance::OverrideVotes` (r:1 w:1) /// Proof: `Governance::OverrideVotes` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`) + /// Storage: `Governance::OverrideDeadline` (r:1 w:0) + /// Proof: `Governance::OverrideDeadline` (`max_values`: None, `max_size`: Some(56), added: 2531, mode: `MaxEncodedLen`) + /// Storage: `System::Number` (r:1 w:0) + /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Governance::VoteLocks` (r:1 w:1) + /// Proof: `Governance::VoteLocks` (`max_values`: None, `max_size`: Some(1873), added: 4348, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:0) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `System::ExecutionPhase` (r:1 w:0) + /// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) + /// Storage: `System::EventCount` (r:1 w:1) + /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Events` (r:1 w:1) + /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) /// Storage: `Governance::OverrideStakeTotal` (r:1 w:1) /// Proof: `Governance::OverrideStakeTotal` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) fn override_council_veto() -> Weight { // Proof size en bytes: - // Medido: `361` - // Estimado: `3722` - // Tiempo minimo de ejecucion: 16_000_000 picosegundos. - Weight::from_parts(16_000_000, 0) - .saturating_add(Weight::from_parts(0, 3722)) - .saturating_add(T::DbWeight::get().reads(5_u64)) - .saturating_add(T::DbWeight::get().writes(2_u64)) + // Medido: `2468` + // Estimado: `5338` + // Tiempo minimo de ejecucion: 56_000_000 picosegundos. + Weight::from_parts(58_000_000, 0) + .saturating_add(Weight::from_parts(0, 5338)) + .saturating_add(T::DbWeight::get().reads(13_u64)) + .saturating_add(T::DbWeight::get().writes(7_u64)) } /// Storage: `Governance::Council` (r:1 w:0) /// Proof: `Governance::Council` (`max_values`: None, `max_size`: Some(89), added: 2564, mode: `MaxEncodedLen`) @@ -317,8 +351,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `353` // Estimado: `6132` - // Tiempo minimo de ejecucion: 60_000_000 picosegundos. - Weight::from_parts(61_000_000, 0) + // Tiempo minimo de ejecucion: 68_000_000 picosegundos. + Weight::from_parts(69_000_000, 0) .saturating_add(Weight::from_parts(0, 6132)) .saturating_add(T::DbWeight::get().reads(11_u64)) .saturating_add(T::DbWeight::get().writes(6_u64)) @@ -343,8 +377,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `494` // Estimado: `6196` - // Tiempo minimo de ejecucion: 51_000_000 picosegundos. - Weight::from_parts(52_000_000, 0) + // Tiempo minimo de ejecucion: 56_000_000 picosegundos. + Weight::from_parts(58_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(T::DbWeight::get().reads(9_u64)) .saturating_add(T::DbWeight::get().writes(5_u64)) @@ -365,8 +399,8 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `101` // Estimado: `3593` - // Tiempo minimo de ejecucion: 38_000_000 picosegundos. - Weight::from_parts(42_000_000, 0) + // Tiempo minimo de ejecucion: 43_000_000 picosegundos. + Weight::from_parts(43_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(T::DbWeight::get().reads(6_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) @@ -389,51 +423,77 @@ impl WeightInfo for SubstrateWeight { // Proof size en bytes: // Medido: `153` // Estimado: `6132` - // Tiempo minimo de ejecucion: 20_000_000 picosegundos. - Weight::from_parts(21_000_000, 0) + // Tiempo minimo de ejecucion: 22_000_000 picosegundos. + Weight::from_parts(23_000_000, 0) .saturating_add(Weight::from_parts(0, 6132)) .saturating_add(T::DbWeight::get().reads(8_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } + /// Storage: `Governance::VoteLocks` (r:1 w:1) + /// Proof: `Governance::VoteLocks` (`max_values`: None, `max_size`: Some(1873), added: 4348, mode: `MaxEncodedLen`) + /// Storage: `System::Number` (r:1 w:0) + /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:1) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:0) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `System::ExecutionPhase` (r:1 w:0) + /// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`) + /// Storage: `System::EventCount` (r:1 w:1) + /// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `System::Events` (r:1 w:1) + /// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + fn unlock_vote_funds() -> Weight { + // Proof size en bytes: + // Medido: `2197` + // Estimado: `5338` + // Tiempo minimo de ejecucion: 34_000_000 picosegundos. + Weight::from_parts(40_000_000, 0) + .saturating_add(Weight::from_parts(0, 5338)) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } } /// Impl para mocks y tests — `RocksDbWeight` directo, sin necesitar `T`. impl WeightInfo for () { fn submit_proposal() -> Weight { - Weight::from_parts(46_000_000, 0) + Weight::from_parts(55_000_000, 0) .saturating_add(Weight::from_parts(0, 3722)) .saturating_add(RocksDbWeight::get().reads(7_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } fn cast_vote() -> Weight { - Weight::from_parts(18_000_000, 0) - .saturating_add(Weight::from_parts(0, 3722)) - .saturating_add(RocksDbWeight::get().reads(7_u64)) - .saturating_add(RocksDbWeight::get().writes(4_u64)) + Weight::from_parts(48_000_000, 0) + .saturating_add(Weight::from_parts(0, 5338)) + .saturating_add(RocksDbWeight::get().reads(10_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) } fn finalize_proposal() -> Weight { - Weight::from_parts(14_000_000, 0) - .saturating_add(Weight::from_parts(0, 3722)) - .saturating_add(RocksDbWeight::get().reads(6_u64)) - .saturating_add(RocksDbWeight::get().writes(3_u64)) + Weight::from_parts(62_000_000, 0) + .saturating_add(Weight::from_parts(0, 6196)) + .saturating_add(RocksDbWeight::get().reads(9_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) } fn add_council_member() -> Weight { - Weight::from_parts(11_000_000, 0) - .saturating_add(Weight::from_parts(0, 3554)) - .saturating_add(RocksDbWeight::get().reads(6_u64)) - .saturating_add(RocksDbWeight::get().writes(4_u64)) - } - fn remove_council_member() -> Weight { Weight::from_parts(12_000_000, 0) .saturating_add(Weight::from_parts(0, 3554)) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(4_u64)) } + fn remove_council_member() -> Weight { + Weight::from_parts(14_000_000, 0) + .saturating_add(Weight::from_parts(0, 3554)) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } fn veto_proposal() -> Weight { - Weight::from_parts(30_000_000, 0) - .saturating_add(Weight::from_parts(0, 6132)) - .saturating_add(RocksDbWeight::get().reads(10_u64)) - .saturating_add(RocksDbWeight::get().writes(6_u64)) + Weight::from_parts(112_000_000, 0) + .saturating_add(Weight::from_parts(0, 16416)) + .saturating_add(RocksDbWeight::get().reads(21_u64)) + .saturating_add(RocksDbWeight::get().writes(10_u64)) } fn mark_as_advisor() -> Weight { Weight::from_parts(10_000_000, 0) @@ -442,41 +502,47 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes(3_u64)) } fn unmark_as_advisor() -> Weight { - Weight::from_parts(10_000_000, 0) + Weight::from_parts(11_000_000, 0) .saturating_add(Weight::from_parts(0, 3513)) .saturating_add(RocksDbWeight::get().reads(5_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } fn override_council_veto() -> Weight { - Weight::from_parts(16_000_000, 0) - .saturating_add(Weight::from_parts(0, 3722)) - .saturating_add(RocksDbWeight::get().reads(5_u64)) - .saturating_add(RocksDbWeight::get().writes(2_u64)) + Weight::from_parts(58_000_000, 0) + .saturating_add(Weight::from_parts(0, 5338)) + .saturating_add(RocksDbWeight::get().reads(13_u64)) + .saturating_add(RocksDbWeight::get().writes(7_u64)) } fn submit_emergency_proposal() -> Weight { - Weight::from_parts(61_000_000, 0) + Weight::from_parts(69_000_000, 0) .saturating_add(Weight::from_parts(0, 6132)) .saturating_add(RocksDbWeight::get().reads(11_u64)) .saturating_add(RocksDbWeight::get().writes(6_u64)) } fn treasury_disbursement() -> Weight { - Weight::from_parts(52_000_000, 0) + Weight::from_parts(58_000_000, 0) .saturating_add(Weight::from_parts(0, 6196)) .saturating_add(RocksDbWeight::get().reads(9_u64)) .saturating_add(RocksDbWeight::get().writes(5_u64)) } fn treasury_deposit() -> Weight { - Weight::from_parts(42_000_000, 0) + Weight::from_parts(43_000_000, 0) .saturating_add(Weight::from_parts(0, 3593)) .saturating_add(RocksDbWeight::get().reads(6_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } fn endorse_emergency_proposal() -> Weight { - Weight::from_parts(21_000_000, 0) + Weight::from_parts(23_000_000, 0) .saturating_add(Weight::from_parts(0, 6132)) .saturating_add(RocksDbWeight::get().reads(8_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } + fn unlock_vote_funds() -> Weight { + Weight::from_parts(40_000_000, 0) + .saturating_add(Weight::from_parts(0, 5338)) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + } } #[cfg(test)] @@ -517,6 +583,7 @@ mod guard { ("treasury_disbursement", <() as WeightInfo>::treasury_disbursement()), ("treasury_deposit", <() as WeightInfo>::treasury_deposit()), ("endorse_emergency_proposal", <() as WeightInfo>::endorse_emergency_proposal()), + ("unlock_vote_funds", <() as WeightInfo>::unlock_vote_funds()), ]; for (nombre, w) in casos { assert!( diff --git a/crates/origin-chain-runtime/src/configs.rs b/crates/origin-chain-runtime/src/configs.rs index 1b5d7e1..c0ca154 100644 --- a/crates/origin-chain-runtime/src/configs.rs +++ b/crates/origin-chain-runtime/src/configs.rs @@ -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; diff --git a/crates/origin-chain-runtime/src/genesis_config_presets.rs b/crates/origin-chain-runtime/src/genesis_config_presets.rs index 57caeeb..4f94329 100644 --- a/crates/origin-chain-runtime/src/genesis_config_presets.rs +++ b/crates/origin-chain-runtime/src/genesis_config_presets.rs @@ -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::(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, + )); + }); + } } diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index bdf7403..cb32739 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -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); } diff --git a/scripts/bench/regen-weights.sh b/scripts/bench/regen-weights.sh index 0edf3b5..4748895 100755 --- a/scripts/bench/regen-weights.sh +++ b/scripts/bench/regen-weights.sh @@ -12,7 +12,13 @@ # QUE HACE # ──────── # 1. Construye el runtime WASM con `runtime-benchmarks`. -# 2. Corre `frame-omni-bencher` sobre ESE blob (el mismo que se despliega). +# 2. Corre `frame-omni-bencher` sobre ESE blob. OJO: NO es el que se despliega. +# Lleva `runtime-benchmarks` (expone la runtime API Benchmark: superficie de DoS +# en los nodos RPC). Lo que tiene que coincidir con lo desplegado es el COMMIT, +# no el blob. Por eso se construye en su propio target (`target-bench/`) y nunca +# en `target/release/wbuild`, que es de donde sale el blob de un set_code. +# (Corregido en RV15: la revisión adversaria encontró el blob de bench en la +# ruta de despliegue, justo después de regenerar pesos.) # 3. Reescribe los 5 `weights.rs` con la plantilla de la casa, que emite # trait + SubstrateWeight + () + un guard generado que reprueba cualquier # proof_size = 0. @@ -41,10 +47,15 @@ ROOT="$PWD" STEPS="${STEPS:-50}" REPEAT="${REPEAT:-20}" BENCHER="${BENCHER:-frame-omni-bencher}" -WASM="${WASM:-$ROOT/target/release/wbuild/origin-chain-runtime/origin_chain_runtime.compact.compressed.wasm}" -# SKIP_BUILD=1 mide un blob YA construido. Es el modo del banco de referencia: -# el Pi 5 no compila Substrate, se le lleva el blob que se va a desplegar — asi -# el binario medido y el desplegado son el MISMO, que es la condicion de Laurha. +# DENTRO del repo, no en /tmp: substrate-wasm-builder busca el Cargo.lock subiendo +# desde el target. Fuera del workspace no lo encuentra, resuelve dependencias desde +# cero y trae crates que exigen otro rustc (medido en RV15). +BENCH_TARGET="${BENCH_TARGET:-$ROOT/target-bench}" +WASM="${WASM:-$BENCH_TARGET/release/wbuild/origin-chain-runtime/origin_chain_runtime.compact.compressed.wasm}" +# SKIP_BUILD=1 mide un blob de bench YA construido. Es el modo del banco de +# referencia: el Pi 5 no compila Substrate, se le lleva el blob de bench del MISMO +# COMMIT que se va a desplegar. La condicion de Laurha (medir lo que se despliega) +# se cumple por commit: el blob desplegado es el mismo codigo SIN runtime-benchmarks. command -v "$BENCHER" >/dev/null || { echo "falta $BENCHER — ver cabecera"; exit 1; } "$BENCHER" --version | grep -q "0\.15\.0" || echo "⚠ el bencher NO es 0.15.0; si falla al decodificar TrackedStorageKey, es esto" @@ -53,7 +64,8 @@ if [ "${SKIP_BUILD:-0}" = "1" ]; then echo "═══ 1/2 SALTADO — midiendo blob ya construido ═══" else echo "═══ 1/2 construyendo el WASM con runtime-benchmarks ═══" - WASM_BUILD_STD=0 cargo build --release -p origin-chain-runtime --features wasm-build,runtime-benchmarks + CARGO_TARGET_DIR="$BENCH_TARGET" WASM_BUILD_STD=0 \ + cargo build --release -p origin-chain-runtime --features wasm-build,runtime-benchmarks fi echo "blob: $( (shasum -a 256 "$WASM" 2>/dev/null || sha256sum "$WASM") | cut -d' ' -f1)"