//! # origin-chain-governance — Phase 5 scaffold v0.0.28 (Foundation Treasury Governance) //! //! Foundation governance on-chain per **CHN-4**. AI-Driven Human-Guarded: //! 5 AI governance agents (OIP Analyst, Treasury Guardian, Education Impact Scorer, //! Network Health Monitor, Compliance Sentinel) ADVISE only — humans-only vote //! canonical, non-negotiable. Runtime programmatically rejects vote extrinsics //! from `governance_advisor` accounts. //! //! ## Status canonical: v0.0.30 — Real impl + mock runtime integration tests E2E //! //! Este crate provee canonical: //! - `Proposal` struct + `ProposalStatus` enum canonical ✅ //! - Storage `Proposals>` per-proposal O(1) ✅ //! - Storage `Votes>` double-vote prevention ✅ //! - Events `ProposalSubmitted` + `VoteCast` + `ProposalFinalized` ✅ //! - Errors canonical (ProposalNotFound + AlreadyVoted + VotingPeriodClosed + StakeInsufficient + etc) ✅ //! - Scaffold dispatchables `submit_proposal` + `cast_vote` + `finalize_proposal` ✅ //! - Helper `derive_proposal_id(proposer, title_hash, body_hash) -> ProposalId` ✅ //! //! ## Phase 5 milestones canonical //! //! - **5.1 v0.0.28-31 (ESTA)**: `origin-chain-governance` pallet foundation (Proposal + Vote + Tally) //! - 5.2: `pallet_collective` integration (Technical Council) //! - 5.3: OIP stake minimum 1,000 OGC canonical //! - 5.4: Stake-weighted voting (1 OGC = 1 vote, 14-day period) //! - 5.5: Technical Council veto + override (2/3 voters) //! - 5.6: AI governance agent interfaces (5 agents advise-only) //! - 5.7: Emergency referendum (48h, 5/7 Technical Council) //! - 5.8: Treasury disbursement extrinsics //! //! ## Inmutables enforced canonical //! //! - **#24** Primacia Moral "Primero los niños y las AIs" — children + AI safety //! - **#36** Cognitive Liberty — daimonion opt-in privacy //! - **#41** Cross-Species Co-authorship — AGI accounts have full vote rights //! UNLESS flagged `governance_advisor` (advise only, not vote) #![cfg_attr(not(feature = "std"), no_std)] #![warn(missing_docs)] pub use pallet::*; /// Re-export canonical types desde origin-chain-types para consumers. pub use origin_chain_types::{ProposalId, Vote, VoteStance}; extern crate alloc; /// Derive deterministic ProposalId canonical via BLAKE2-256 hash de proposer + title_hash + body_hash. /// /// Per CHN-4: `id = BLAKE2_256(proposer || title_hash || body_hash)`. Garantiza /// determinismo + content-addressed canonical (mismo proposal → mismo id, /// Inmutable #16 byte-determinism cross-runtime). /// /// **NOTE**: Usamos sp_core::hashing::blake2_256 canonical para consistencia /// FRAME-native + evitar duplicate blake3 dep. Future refactor canonical: bump /// a blake3 directly cuando Substrate stable bump permita. pub fn derive_proposal_id( proposer: &origin_chain_types::AccountId, title_hash: &[u8; 32], body_hash: &[u8; 32], ) -> ProposalId { use sp_core::hashing::blake2_256; let mut concat = alloc::vec::Vec::with_capacity(96); concat.extend_from_slice(proposer.as_bytes()); concat.extend_from_slice(title_hash); concat.extend_from_slice(body_hash); ProposalId(blake2_256(&concat)) } /// Runtime API canonical para cross-pillar consumers + light clients (Phase 5.1 v0.0.31). /// /// Permite query canonical de proposal status + vote stance sin necesidad de /// full node access — Foundation frontend + AI advisor agents (read-only) + /// light clients consume canonical via RPC. /// /// Consumers canonical: /// - David Foundation governance frontend canonical /// - 5 AI advisor agents (advise-only canonical) /// - 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). 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; } } #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use origin_chain_types::{AccountId, ProposalId, VoteStance}; use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::traits::SaturatedConversion; /// ProposalStatus canonical — proposal lifecycle stages canonical. #[derive( Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, RuntimeDebug, )] pub enum ProposalStatus { /// Voting period open canonical (block.number ∈ [start_block, end_block]). Voting, /// Finalized canonical: yes_weight > nay_weight (passed canonical CHN-4). Approved, /// Finalized canonical: nay_weight >= yes_weight (rejected canonical). Rejected, /// Withdrawn canonical por proposer ANTES de finalize. Withdrawn, } /// Proposal canonical struct stored on-chain per pallet (Foundation OIP). #[derive( Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug, )] pub struct Proposal { /// ProposalId canonical (content-addressed BLAKE2-256 hash). pub id: ProposalId, /// Proposer canonical AccountId (MUST NOT be governance_advisor flagged). pub proposer: origin_chain_types::AccountId, /// Title hash canonical (BLAKE2-256 32-byte off-chain text reference). pub title_hash: [u8; 32], /// Body hash canonical (BLAKE2-256 32-byte off-chain text reference). pub body_hash: [u8; 32], /// OGC stake canonical locked (minimum 1,000 OGC per CHN-4 + 5.3). pub stake: u128, /// Voting period start block canonical (set by runtime at submit). pub start_block: u64, /// Voting period end block canonical (start_block + voting_period_blocks). pub end_block: u64, /// ProposalStatus canonical lifecycle. pub status: ProposalStatus, /// Tally Aye canonical (sum of stake weights canonical). pub yes_weight: u128, /// Tally Nay canonical. pub nay_weight: u128, /// Tally Abstain canonical. pub abstain_weight: u128, } /// Pallet configuration trait canonical. #[pallet::config] pub trait Config: frame_system::Config { /// Runtime event type canonical wired into `RuntimeEvent` aggregate. type RuntimeEvent: From> + IsType<::RuntimeEvent>; } /// Pallet definition canonical. #[pallet::pallet] pub struct Pallet(_); /// CHN-4 canonical: `Proposals>` canonical. #[pallet::storage] #[pallet::getter(fn proposal_of)] pub type Proposals = StorageMap<_, Blake2_128Concat, ProposalId, Proposal, OptionQuery>; /// CHN-4 canonical: `Votes>` canonical. #[pallet::storage] #[pallet::getter(fn vote_of)] pub type Votes = StorageDoubleMap< _, Blake2_128Concat, ProposalId, Blake2_128Concat, T::AccountId, VoteStance, OptionQuery, >; /// Events emisión canonical (CHN-4 governance lifecycle). #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Proposal submitted canonical on-chain (OIP submitted CHN-4 5.3). ProposalSubmitted { /// ProposalId canonical content-addressed. id: ProposalId, /// Proposer T::AccountId canonical. proposer: T::AccountId, /// Voting period blocks canonical. voting_period: u64, }, /// Vote cast canonical sobre proposal (humans-only canonical CHN-4 5.6). VoteCast { /// ProposalId canonical. id: ProposalId, /// Voter T::AccountId canonical. voter: T::AccountId, /// VoteStance canonical (Aye + Nay + Abstain). stance: VoteStance, /// Stake weight canonical at vote time. weight: u128, }, /// Proposal finalized canonical post-voting period (Approved/Rejected canonical). ProposalFinalized { /// ProposalId canonical. id: ProposalId, /// Final ProposalStatus canonical (Approved | Rejected). status: ProposalStatus, /// Tally yes_weight canonical. yes_weight: u128, /// Tally nay_weight canonical. nay_weight: u128, }, } /// Pallet errors canonical. #[pallet::error] pub enum Error { /// CHN-4 canonical: proposal NO encontrado on-chain. ProposalNotFound, /// CHN-4 canonical: voter ya cast vote previamente (double-vote prevention). AlreadyVoted, /// CHN-4 canonical: voting period canonical CLOSED (block.number > end_block). VotingPeriodClosed, /// CHN-4 canonical: voting period STILL OPEN (cannot finalize yet). VotingPeriodOpen, /// CHN-4 5.3 canonical: stake canonical insufficient (mínimum 1,000 OGC). StakeInsufficient, /// CHN-4 canonical: proposal ya finalized (status != Voting). ProposalAlreadyFinalized, /// CHN-4 5.6 canonical: governance_advisor canonical NO puede votar (humans-only canonical). AdvisorMayNotVote, /// CHN-4 canonical: proposer canonical ya existe con ese hash content (duplicate prevention). ProposalIdCollision, } /// Minimum stake canonical proposal submission per CHN-4 5.3. /// 1,000 OGC = 1_000 × 10^18 (18 decimals OGC canonical). pub const MIN_PROPOSAL_STAKE: u128 = 1_000 * 10u128.pow(18); /// Dispatchables canonical (real impl v0.0.30). #[pallet::call] impl Pallet { /// CHN-4 5.3 canonical: `submit_proposal` real dispatchable canonical. /// /// 7-step canonical flow: ensure_signed + validate_stake + derive_id + /// collision_check + build + insert + emit. #[pallet::call_index(0)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn submit_proposal( origin: OriginFor, title_hash: [u8; 32], body_hash: [u8; 32], voting_period_blocks: u64, stake: u128, ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(stake >= MIN_PROPOSAL_STAKE, Error::::StakeInsufficient); let proposer_acct = AccountId::new(account_bytes::(&who)); let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash); ensure!(!Proposals::::contains_key(id), Error::::ProposalIdCollision); let start_block: u64 = >::block_number() .saturated_into::(); let end_block = start_block.saturating_add(voting_period_blocks); let proposal = Proposal { id, proposer: proposer_acct, title_hash, body_hash, stake, start_block, end_block, status: ProposalStatus::Voting, yes_weight: 0, nay_weight: 0, abstain_weight: 0, }; Proposals::::insert(id, proposal); Self::deposit_event(Event::ProposalSubmitted { id, proposer: who, voting_period: voting_period_blocks, }); Ok(()) } /// CHN-4 5.4 canonical: `cast_vote` real dispatchable canonical. #[pallet::call_index(1)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn cast_vote( origin: OriginFor, proposal_id: ProposalId, stance: VoteStance, stake_weight: u128, ) -> DispatchResult { let who = ensure_signed(origin)?; let mut proposal = Proposals::::get(proposal_id) .ok_or(Error::::ProposalNotFound)?; ensure!( matches!(proposal.status, ProposalStatus::Voting), Error::::ProposalAlreadyFinalized ); let current_block: u64 = >::block_number() .saturated_into::(); ensure!( current_block <= proposal.end_block, Error::::VotingPeriodClosed ); ensure!( !Votes::::contains_key(proposal_id, &who), Error::::AlreadyVoted ); match stance { VoteStance::Aye => { proposal.yes_weight = proposal.yes_weight.saturating_add(stake_weight) } VoteStance::Nay => { proposal.nay_weight = proposal.nay_weight.saturating_add(stake_weight) } VoteStance::Abstain => { proposal.abstain_weight = proposal.abstain_weight.saturating_add(stake_weight) } } Proposals::::insert(proposal_id, proposal); Votes::::insert(proposal_id, &who, stance); Self::deposit_event(Event::VoteCast { id: proposal_id, voter: who, stance, weight: stake_weight, }); Ok(()) } /// CHN-4 canonical: `finalize_proposal` real dispatchable canonical. #[pallet::call_index(2)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn finalize_proposal( origin: OriginFor, proposal_id: ProposalId, ) -> DispatchResult { let _who = ensure_signed(origin)?; let mut proposal = Proposals::::get(proposal_id) .ok_or(Error::::ProposalNotFound)?; ensure!( matches!(proposal.status, ProposalStatus::Voting), Error::::ProposalAlreadyFinalized ); let current_block: u64 = >::block_number() .saturated_into::(); ensure!( current_block > proposal.end_block, Error::::VotingPeriodOpen ); let final_status = if proposal.yes_weight > proposal.nay_weight { ProposalStatus::Approved } else { ProposalStatus::Rejected }; proposal.status = final_status; let yes_weight = proposal.yes_weight; let nay_weight = proposal.nay_weight; Proposals::::insert(proposal_id, proposal); Self::deposit_event(Event::ProposalFinalized { id: proposal_id, status: final_status, yes_weight, nay_weight, }); Ok(()) } } /// Helper canonical para convert T::AccountId → 32-byte canonical. fn account_bytes(account: &T::AccountId) -> [u8; 32] { let encoded = account.encode(); let mut bytes = [0u8; 32]; let copy_len = encoded.len().min(32); bytes[..copy_len].copy_from_slice(&encoded[..copy_len]); bytes } /// Pure helpers canonical para Runtime API v0.0.31 consumers + tests. impl Pallet { /// Query proposal canonical sin storage mutation (read-only canonical). pub fn query_proposal_of(id: ProposalId) -> Option { Proposals::::get(id) } /// Query vote stance canonical por (proposal_id, voter) sin storage mutation. pub fn query_vote_of(id: ProposalId, voter: &T::AccountId) -> Option { Votes::::get(id, voter) } } } // v0.0.30 — Mock runtime canonical + integration tests E2E CHN-4 flow. // Pattern v0.0.18 + v0.0.22 + v0.0.26 mirror. #[cfg(test)] pub mod mock; #[cfg(test)] mod tests_integration; #[cfg(test)] mod tests { use super::*; use origin_chain_types::AccountId; #[test] fn pallet_compiles_canonical() { // Pallet compiles canonical — verified by cargo check. } #[test] fn derive_proposal_id_deterministic_canonical() { // Inmutable #16 byte-determinism canonical: same inputs → same ProposalId let proposer = AccountId::new([42u8; 32]); let title = [1u8; 32]; let body = [2u8; 32]; let id1 = derive_proposal_id(&proposer, &title, &body); let id2 = derive_proposal_id(&proposer, &title, &body); assert_eq!(id1, id2); } #[test] fn derive_proposal_id_different_inputs_different_ids_canonical() { let p1 = AccountId::new([1u8; 32]); let p2 = AccountId::new([2u8; 32]); let title = [10u8; 32]; let body = [20u8; 32]; let id_a = derive_proposal_id(&p1, &title, &body); let id_b = derive_proposal_id(&p2, &title, &body); assert_ne!(id_a, id_b); } #[test] fn proposal_status_variants_canonical() { use pallet::ProposalStatus; let voting = ProposalStatus::Voting; let approved = ProposalStatus::Approved; let rejected = ProposalStatus::Rejected; let withdrawn = ProposalStatus::Withdrawn; assert_ne!(core::mem::discriminant(&voting), core::mem::discriminant(&approved)); assert_ne!(core::mem::discriminant(&rejected), core::mem::discriminant(&withdrawn)); } #[test] fn vote_stance_re_export_canonical() { let _aye = VoteStance::Aye; let _nay = VoteStance::Nay; let _abstain = VoteStance::Abstain; } }