diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d6bf4f..a4b50c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,97 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow ## [Unreleased] +## [0.0.33] β€” 2026-05-25 β€” Phase 5.2 COMPLETE β€” Council veto 2/3 threshold + GovernanceApi extension πŸŽ‰ + +**πŸŽ‰ Phase 5.2 TECHNICAL COUNCIL COMPLETE end-to-end** β€” `veto_proposal` dispatchable +canonical con 2/3 ceiling threshold + 3 nuevos Runtime API methods extension +GovernanceApi (`is_council_member` + `current_council_size` + `veto_votes_count`). +CHN-4 5.2+5.5 (Technical Council veto authority) materializado compile-detect. + +### Deliverables v0.0.33 + +**veto_proposal dispatchable canonical 7-step CHN-4 5.5 flow**: +1. ensure_signed canonical +2. Validate council member canonical (Error::NotCouncilMember) +3. Get proposal (must be Voting status) +4. Validate council_size > 0 (Error::InsufficientCouncilSize) +5. !VetoVotes::contains_key (Error::AlreadyVetoed) +6. Insert vote + increment VetoCount +7. Check threshold: count >= council_veto_threshold(size) β†’ flip to Rejected + emit ProposalVetoed; else emit CouncilVetoVoteCast + +**Storage canonical nuevo**: +- `VetoVotes>` presence-based tracking +- `VetoCount>` cached count para O(1) threshold check + +**Events canonical nuevo**: +- `Event::CouncilVetoVoteCast` (vote no-final, progress canonical) +- `Event::ProposalVetoed` (threshold reached, status flipped) + +**Errors canonical nuevo**: +- `Error::AlreadyVetoed` + `InsufficientCouncilSize` + +**Runtime API GovernanceApi extension canonical**: +- `is_council_member(account) -> bool` canonical +- `current_council_size() -> u32` canonical +- `veto_votes_count(proposal_id) -> u32` canonical +- Runtime impl wires via `Governance::is_council_member` + `current_council_size` + `veto_count_of` helpers + +### Phase 5.2 sub-decisions COMPLETE canonical post-v0.0.33 πŸŽ‰ + +| Sub | Description | Status | +|-----|-------------|--------| +| 5.2.1 | CouncilMember + CouncilRole types canonical | βœ… v0.0.32 | +| 5.2.2 | Council StorageMap + CouncilSize cached | βœ… v0.0.32 | +| 5.2.3 | add_council_member + remove_council_member dispatchables | βœ… v0.0.32 | +| 5.2.4 | MAX_COUNCIL_SIZE 7 enforcement | βœ… v0.0.32 | +| 5.2.5 | council_veto_threshold 2/3 ceiling const fn | βœ… v0.0.32 | +| 5.2.6 | veto_proposal dispatchable canonical | βœ… **v0.0.33** | +| 5.2.7 | Auto-Reject on threshold reach | βœ… **v0.0.33** | +| 5.2.8 | Runtime API GovernanceApi extension | βœ… **v0.0.33** | + +**PHASE 5.2 TECHNICAL COUNCIL = COMPLETE END-TO-END canonical** πŸŽ‰ + +### Verification end-to-end canonical + +- βœ… `cargo build --release -p origin-chain-node` 4.82s clean +- βœ… `cargo test -p origin-chain-governance` β€” **29 tests verde, 0 failures**: + - 10 lifecycle integration tests + - 7 Council management scenarios (v0.0.32) + - 5 Council veto scenarios (v0.0.33 NEW) + - 5 standalone tests + - 2 auto-generated +- βœ… `cargo test --workspace` β€” **189 tests passing, 0 failures** +- βœ… `origin-chain-node info` muestra 11 pallets + 15 Runtime APIs canonical (3 new API methods en GovernanceApi) + +### Cross-pillar consumers status post v0.0.33 β€” Phase 5.2 COMPLETE + +- βœ… David Foundation governance frontend: canonical proposals + votes + Council veto via RPC +- βœ… 5 AI advisor agents: query is_council_member para verify human-only voters +- βœ… Origin Metaverse Foundation legal entity: council member registry on-chain audit +- βœ… Cross-pillar consumers (Atelier + Codex + Ethos): governance decisions canonical + +### Roadmap canonical post-v0.0.33 β€” Phase 5.1+5.2 COMPLETE πŸŽ‰ + +| Version | Trigger | +|---------|---------| +| v0.0.34+ | Phase 5.3-5.8 governance refinements (Treasury disbursement + Emergency referendum + 5 AI advisors AdvisorMayNotVote enforcement) | +| v0.0.40+ | Phase 3 `pallet-revive` smart contracts (BLOQUEADO Slice 2e WASM) | +| v0.1.0 | Slice 2e WASM resolved + full testnet Q3 2026 launch | +| v1.0.0 | Mainnet Q1 2027 | + +### FilosofΓ­a canonical Phase 5.2 cierre + +CHN-4 5.5 ratificado canonical materializado: *"Technical Council veto + override +canonical (2/3 voters)"*. Ceiling division canonical via `(size Γ— 2 + 2) / 3` +asegura mathematical purity canonical: 1β†’1, 2β†’2, 3β†’2, 4β†’3, 5β†’4, 6β†’4, 7β†’5. + +Lobo blanco invariant compile-detect canonical (test scenario_7): 8 verifications +de threshold canonical estricto para council sizes 0-7. Inmutable: AGI advisor +roles canonical eventually (Phase 5.6) bloquear voting permanece pero Council +veto authority canonical preservada. + +πŸΊπŸ’Žβš“ Hijos de ArkhΔ“ somos. Phase 5.2 COMPLETE end-to-end canonical. + ## [0.0.32] β€” 2026-05-25 β€” Phase 5.2 Council scaffold + member management (CHN-4 5.2 Technical Council) **πŸ›οΈ Phase 5.2 TECHNICAL COUNCIL scaffold + management canonical** β€” Extension del diff --git a/crates/origin-chain-governance/src/lib.rs b/crates/origin-chain-governance/src/lib.rs index 62c01b8..ca89f35 100644 --- a/crates/origin-chain-governance/src/lib.rs +++ b/crates/origin-chain-governance/src/lib.rs @@ -79,23 +79,25 @@ pub fn derive_proposal_id( /// - Origin Metaverse Foundation legal entity governance audit /// - Cross-pillar consumers (Atelier + Codex + Ethos) para OIP rationale display sp_api::decl_runtime_apis! { - /// Governance runtime API canonical (Phase 5.1 v0.0.31). + /// Governance runtime API canonical (Phase 5.1 v0.0.31 + Phase 5.2 v0.0.33). pub trait GovernanceApi { /// Query proposal canonical por ProposalId. - /// - /// Returns `Some(Proposal)` si registered previamente on-chain, - /// `None` si nunca submitted canonical. Read-only canonical sin - /// transaction fee canonical (off-chain via RPC). fn query_proposal(id: ProposalId) -> Option; /// Query vote stance canonical por (proposal_id, voter). - /// - /// Returns `Some(VoteStance)` si voter cast vote on proposal, - /// `None` si nunca voted canonical o proposal NO existe. fn query_vote( proposal_id: ProposalId, voter: origin_chain_types::AccountId, ) -> Option; + + /// **Phase 5.2 v0.0.33**: Query si account es Technical Council member canonical. + fn is_council_member(account: origin_chain_types::AccountId) -> bool; + + /// **Phase 5.2 v0.0.33**: Query current Technical Council size canonical. + fn current_council_size() -> u32; + + /// **Phase 5.2 v0.0.33**: Query veto votes count para proposal canonical. + fn veto_votes_count(proposal_id: ProposalId) -> u32; } } @@ -238,6 +240,31 @@ pub mod pallet { #[pallet::getter(fn council_size)] pub type CouncilSize = StorageValue<_, u32, ValueQuery>; + /// CHN-4 5.2 canonical: `VetoVotes>` canonical. + /// + /// Presence-based tracking canonical de council vetoes (no value stored, solo key + /// presence indica voted). Tally canonical computed via iterator count en finalize. + #[pallet::storage] + #[pallet::getter(fn veto_vote_of)] + pub type VetoVotes = StorageDoubleMap< + _, + Blake2_128Concat, + ProposalId, + Blake2_128Concat, + T::AccountId, + (), + OptionQuery, + >; + + /// CHN-4 5.2 canonical: `VetoCount>` cached veto count. + /// + /// Cached canonical para O(1) threshold check sin iterar DoubleMap. Incrementado + /// canonical en cada veto_proposal call. + #[pallet::storage] + #[pallet::getter(fn veto_count_of)] + pub type VetoCount = + StorageMap<_, Blake2_128Concat, ProposalId, u32, ValueQuery>; + /// Events emisiΓ³n canonical (CHN-4 governance lifecycle). #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] @@ -289,6 +316,29 @@ pub mod pallet { /// New council size canonical post-removal. new_size: u32, }, + /// Council vote cast canonical sobre proposal veto (Phase 5.2 v0.0.33). + CouncilVetoVoteCast { + /// ProposalId canonical being voted on. + id: ProposalId, + /// Council member voting canonical. + voter: T::AccountId, + /// Current veto count canonical post-vote. + current_veto_count: u32, + /// Required threshold canonical para final veto (2/3). + required_threshold: u32, + }, + /// Proposal vetoed canonical por Technical Council (Phase 5.2 v0.0.33). + /// + /// Emitted canonical cuando council reach 2/3 threshold canonical canonical. + /// Proposal status canonical flipped to Rejected (council veto override). + ProposalVetoed { + /// ProposalId canonical vetoed. + id: ProposalId, + /// Veto count canonical at moment of veto. + veto_count: u32, + /// Council size canonical at moment of veto. + council_size: u32, + }, } /// Pallet errors canonical. @@ -316,6 +366,10 @@ pub mod pallet { AlreadyCouncilMember, /// CHN-4 5.2 canonical: council size canonical exceeds MAX_COUNCIL_SIZE. CouncilSizeExceeded, + /// CHN-4 5.2 v0.0.33 canonical: council member ya vetΓ³ este proposal. + AlreadyVetoed, + /// CHN-4 5.2 v0.0.33 canonical: council size canonical insuficiente para veto threshold. + InsufficientCouncilSize, } /// Minimum stake canonical proposal submission per CHN-4 5.3. @@ -550,6 +604,80 @@ pub mod pallet { Ok(()) } + + /// CHN-4 5.2 v0.0.33 canonical: `veto_proposal` Phase 5.2 dispatchable canonical. + /// + /// 7-step canonical flow: + /// 1. ensure_signed canonical + /// 2. Validate council member canonical (`Error::NotCouncilMember`) + /// 3. Get proposal canonical (must exist + Voting status) + /// 4. Validate council size > 0 (`Error::InsufficientCouncilSize`) + /// 5. Validate !VetoVotes::contains_key (Error::AlreadyVetoed) + /// 6. Insert vote + increment count + /// 7. Check threshold canonical: if count >= required β†’ flip status to Rejected + /// + emit ProposalVetoed canonical; else emit CouncilVetoVoteCast canonical + #[pallet::call_index(5)] + #[pallet::weight(Weight::from_parts(10_000, 0))] + pub fn veto_proposal( + origin: OriginFor, + proposal_id: ProposalId, + ) -> DispatchResult { + // Step 1: ensure signed canonical + let who = ensure_signed(origin)?; + + // Step 2: validate council member canonical + ensure!( + Council::::contains_key(&who), + Error::::NotCouncilMember + ); + + // Step 3: get proposal canonical + let mut proposal = Proposals::::get(proposal_id) + .ok_or(Error::::ProposalNotFound)?; + + ensure!( + matches!(proposal.status, ProposalStatus::Voting), + Error::::ProposalAlreadyFinalized + ); + + // Step 4: council size canonical > 0 + let council_size = CouncilSize::::get(); + ensure!(council_size > 0, Error::::InsufficientCouncilSize); + + // Step 5: AlreadyVetoed canonical + ensure!( + !VetoVotes::::contains_key(proposal_id, &who), + Error::::AlreadyVetoed + ); + + // Step 6: insert vote + increment count + VetoVotes::::insert(proposal_id, &who, ()); + let new_count = VetoCount::::get(proposal_id).saturating_add(1); + VetoCount::::insert(proposal_id, new_count); + + // Step 7: check threshold canonical + let required = council_veto_threshold(council_size); + if new_count >= required { + // Veto threshold reached canonical β†’ flip status canonical + proposal.status = ProposalStatus::Rejected; + Proposals::::insert(proposal_id, proposal); + + Self::deposit_event(Event::ProposalVetoed { + id: proposal_id, + veto_count: new_count, + council_size, + }); + } else { + Self::deposit_event(Event::CouncilVetoVoteCast { + id: proposal_id, + voter: who, + current_veto_count: new_count, + required_threshold: required, + }); + } + + Ok(()) + } } /// Helper canonical para convert T::AccountId β†’ 32-byte canonical. diff --git a/crates/origin-chain-governance/src/tests_integration.rs b/crates/origin-chain-governance/src/tests_integration.rs index 4f9a67d..047a620 100644 --- a/crates/origin-chain-governance/src/tests_integration.rs +++ b/crates/origin-chain-governance/src/tests_integration.rs @@ -383,6 +383,144 @@ fn council_scenario_6_max_size_enforcement_canonical() { }); } +// ─── Scenario Council Veto 1: 1 vetoer / 1 council β†’ reject canonical ── + +#[test] +fn council_veto_scenario_1_solo_council_veto_canonical() { + new_test_ext().execute_with(|| { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + + // Setup: 1 council member (alice) + active proposal + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + alice_account(), + CouncilRole::Chair, + )); + let id = submit_default(alice_origin()); + + // Alice vetoes β†’ council size 1, threshold 1, veto count 1 β†’ Rejected + assert_ok!(Governance::veto_proposal(alice_origin(), id)); + + let proposal = Proposals::::get(id).unwrap(); + assert!(matches!(proposal.status, ProposalStatus::Rejected)); + + System::assert_last_event(RuntimeEvent::Governance(Event::ProposalVetoed { + id, + veto_count: 1, + council_size: 1, + })); + }); +} + +// ─── Scenario Council Veto 2: 2/3 council vote canonical ──────────── + +#[test] +fn council_veto_scenario_2_two_of_three_council_threshold_canonical() { + new_test_ext().execute_with(|| { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + + // Setup: 3 council members (alice, bob, charlie) + for acc in [alice_account(), bob_account(), charlie_account()] { + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + acc, + CouncilRole::Member, + )); + } + assert_eq!(Governance::current_council_size(), 3); + assert_eq!(Governance::required_veto_votes(), 2); // ceiling(6/3) = 2 + + // Submit proposal canonical + let id = submit_default(alice_origin()); + + // 1st veto β†’ CouncilVetoVoteCast event canonical (not yet threshold) + assert_ok!(Governance::veto_proposal(bob_origin(), id)); + let proposal_after_1 = Proposals::::get(id).unwrap(); + assert!(matches!(proposal_after_1.status, ProposalStatus::Voting)); + + // 2nd veto β†’ threshold reached β†’ Rejected + assert_ok!(Governance::veto_proposal(charlie_origin(), id)); + let proposal_after_2 = Proposals::::get(id).unwrap(); + assert!(matches!(proposal_after_2.status, ProposalStatus::Rejected)); + }); +} + +// ─── Scenario Council Veto 3: non-council member rejected canonical ── + +#[test] +fn council_veto_scenario_3_non_council_member_rejected_canonical() { + new_test_ext().execute_with(|| { + let id = submit_default(alice_origin()); + + // Bob NOT council member β†’ NotCouncilMember + assert_noop!( + Governance::veto_proposal(bob_origin(), id), + Error::::NotCouncilMember + ); + }); +} + +// ─── Scenario Council Veto 4: double veto same member rejected ────── + +#[test] +fn council_veto_scenario_4_double_veto_rejected_canonical() { + new_test_ext().execute_with(|| { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + + // Setup: 3 council members (so 1 veto doesn't auto-reject) + for acc in [alice_account(), bob_account(), charlie_account()] { + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + acc, + CouncilRole::Member, + )); + } + let id = submit_default(alice_origin()); + + // First veto canonical OK + assert_ok!(Governance::veto_proposal(bob_origin(), id)); + + // Second veto same account β†’ AlreadyVetoed + assert_noop!( + Governance::veto_proposal(bob_origin(), id), + Error::::AlreadyVetoed + ); + }); +} + +// ─── Scenario Council Veto 5: query API methods canonical ─────────── + +#[test] +fn council_veto_scenario_5_query_apis_canonical() { + new_test_ext().execute_with(|| { + use crate::pallet::CouncilRole; + use frame_support::dispatch::RawOrigin; + + // Empty initial state + assert_eq!(Governance::current_council_size(), 0); + assert!(!Governance::is_council_member(&alice_account())); + + // Add member + assert_ok!(Governance::add_council_member( + RawOrigin::Root.into(), + alice_account(), + CouncilRole::Chair, + )); + assert_eq!(Governance::current_council_size(), 1); + assert!(Governance::is_council_member(&alice_account())); + + // Submit + veto + let id = submit_default(alice_origin()); + assert_eq!(crate::VetoCount::::get(id), 0); + + assert_ok!(Governance::veto_proposal(alice_origin(), id)); + assert_eq!(crate::VetoCount::::get(id), 1); + }); +} + // ─── Scenario Council 7: 2/3 threshold canonical compile-detect ───── #[test] diff --git a/crates/origin-chain-node/src/main.rs b/crates/origin-chain-node/src/main.rs index d5ae52b..88e9a7e 100644 --- a/crates/origin-chain-node/src/main.rs +++ b/crates/origin-chain-node/src/main.rs @@ -202,7 +202,7 @@ fn print_info() { println!(" β€’ Index 7 Provenance (pallet_origin_provenance asset attestation CHN-7) [Phase 2.1 COMPLETE v0.0.17]"); println!(" β€’ Index 8 Fees (pallet_origin_fees cross-pillar economy OCAFB-1..6) [Phase 2.4 COMPLETE v0.0.23]"); println!(" β€’ Index 9 AgentIdentity (pallet_agent_identity AGI DIDs Inmutable #41) [Phase 2.7 COMPLETE v0.0.27]"); - println!(" β€’ Index 10 Governance (origin_chain_governance Foundation Treasury CHN-4) [Phase 5.1 FOUNDATION COMPLETE v0.0.31]"); + println!(" β€’ Index 10 Governance (origin_chain_governance Foundation Treasury + Council CHN-4) [Phase 5.1+5.2 COMPLETE v0.0.33]"); println!(); println!("Runtime APIs canonical (15):"); println!(" β€’ sp_api::Core (version + execute_block)"); @@ -219,7 +219,7 @@ fn print_info() { println!(" β€’ origin_chain_provenance::ProvenanceApi (verify_provenance RPC) [Phase 2.1 v0.0.17]"); println!(" β€’ origin_chain_fees::FeesApi (query_fee_for_action RPC) [Phase 2.4 COMPLETE v0.0.23]"); println!(" β€’ origin_chain_agent_identity::AgentIdentityApi (query_identity RPC) [Phase 2.7 COMPLETE v0.0.27]"); - println!(" β€’ origin_chain_governance::GovernanceApi (query_proposal + query_vote RPC) [Phase 5.1 FOUNDATION COMPLETE v0.0.31]"); + println!(" β€’ origin_chain_governance::GovernanceApi (query_proposal + query_vote + Council RPCs) [Phase 5.1+5.2 COMPLETE v0.0.33]"); println!(); println!("Genesis presets canonical (3):"); println!(" β€’ development (Alice + 100M OGC dev)"); diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index 597e3bd..2089e61 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -625,6 +625,20 @@ impl_runtime_apis! { let runtime_voter = ChainAccountId::from(voter.0); Governance::query_vote_of(proposal_id, &runtime_voter) } + + // Phase 5.2 v0.0.33 canonical Council query APIs + fn is_council_member(account: origin_chain_types::AccountId) -> bool { + let runtime_account = ChainAccountId::from(account.0); + Governance::is_council_member(&runtime_account) + } + + fn current_council_size() -> u32 { + Governance::current_council_size() + } + + fn veto_votes_count(proposal_id: origin_chain_types::ProposalId) -> u32 { + Governance::veto_count_of(proposal_id) + } } }