diff --git a/CHANGELOG.md b/CHANGELOG.md index 130bcc3..2dbddf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,65 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow ## [Unreleased] +## [0.0.28] — 2026-05-25 — Phase 5.1 scaffold — origin-chain-governance pallet (CHN-4 Foundation Treasury) + +**🏛️ Phase 5.1 GOVERNANCE pallet SCAFFOLD trait-first foundation** — Foundation +Treasury governance on-chain per CHN-4. AI-Driven Human-Guarded canonical: 5 AI +governance agents advise-only, humans-only vote canonical non-negotiable. Reemplaza +placeholder `GovernanceConfig` stub canonical en existing `origin-chain-governance` +crate. Pattern Phase 2.7 mirror. + +### Deliverables v0.0.28 + +**Pallet scaffold canonical en existing crate**: +- `crates/origin-chain-governance/Cargo.toml` (UPDATED): stub deps → FRAME pallet canonical deps +- `crates/origin-chain-governance/src/lib.rs` (REPLACED placeholder): real pallet scaffold ~360 LOC + +**Scale derives canonical types canonical**: +- `crates/origin-chain-types/src/governance.rs` (UPDATED): added `Encode + Decode + DecodeWithMemTracking + MaxEncodedLen + TypeInfo` derives a `ProposalId` + `VoteStance` + `Vote` para FRAME-compatibility + +**Pallet surface canonical**: +- `Pallet` struct + `Config` trait canonical (RuntimeEvent association) +- `Proposal` struct canonical (11 fields: id + proposer + title_hash + body_hash + stake + start_block + end_block + status + yes/nay/abstain_weight) +- `ProposalStatus` enum canonical (Voting + Approved + Rejected + Withdrawn) +- `Proposals>` per-proposal O(1) +- `Votes>` double-vote prevention +- Events: `ProposalSubmitted` + `VoteCast` + `ProposalFinalized` +- Errors canonical (8 variants: ProposalNotFound + AlreadyVoted + VotingPeriodClosed/Open + StakeInsufficient + ProposalAlreadyFinalized + AdvisorMayNotVote + ProposalIdCollision) +- Scaffold dispatchables: `submit_proposal` + `cast_vote` + `finalize_proposal` + +**Helper canonical**: +- `derive_proposal_id(proposer, title_hash, body_hash) -> ProposalId` pure fn via BLAKE2-256 hash (Inmutable #16 byte-determinism canonical) + +**Runtime API declaration canonical**: +- `pub trait GovernanceApi { fn query_proposal(id) -> Option; fn query_vote(...) -> Option; }` +- Implementación en runtime canonical pending v0.0.31 + +### Verification end-to-end canonical + +- ✅ `cargo build -p origin-chain-governance` clean canonical +- ✅ `cargo test -p origin-chain-governance` — **5 tests verde, 0 failures**: + - pallet_compiles_canonical + - derive_proposal_id_deterministic_canonical (Inmutable #16) + - derive_proposal_id_different_inputs_different_ids_canonical + - proposal_status_variants_canonical + - vote_stance_re_export_canonical + +### Roadmap canonical post-v0.0.28 + +| Version | Trigger | +|---------|---------| +| **v0.0.29** | **Wire Governance pallet runtime composition Index 10 + Config canonical (sesión 3+ shipping ahora)** | +| v0.0.30 | Real submit_proposal + cast_vote + finalize_proposal + mock runtime integration tests E2E | +| v0.0.31 | Runtime API canonical GovernanceApi impl runtime → **Phase 5 FOUNDATION COMPLETE** | + +### Filosofía canonical sesión 3+ aplicada + +CHN-4 ratificado canonical: *"5 AI governance agents ADVISE only — humans-only +vote canonical non-negotiable"*. Materializado canonical via `Error::AdvisorMayNotVote` ++ runtime programmatic enforcement v0.0.30 forward. Inmutable #41 AGI sovereignty +preserved: AGIs NO advisor pueden votar canonical en sus governance proposals. + ## [0.0.27] — 2026-05-25 — Phase 2.7 COMPLETE — Runtime API AgentIdentityApi.query_identity 🎉 **🎉 Phase 2.7 AGENT IDENTITY pallet COMPLETE end-to-end** — Runtime API diff --git a/Cargo.lock b/Cargo.lock index 671f6b9..3e257a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2212,7 +2212,15 @@ dependencies = [ name = "origin-chain-governance" version = "0.0.1" dependencies = [ + "frame-support", + "frame-system", "origin-chain-types", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-core", + "sp-io", + "sp-runtime", ] [[package]] diff --git a/crates/origin-chain-governance/Cargo.toml b/crates/origin-chain-governance/Cargo.toml index 17fa5b3..ec60f4c 100644 --- a/crates/origin-chain-governance/Cargo.toml +++ b/crates/origin-chain-governance/Cargo.toml @@ -7,7 +7,50 @@ license.workspace = true authors.workspace = true repository.workspace = true homepage.workspace = true -description = "Foundation governance on-chain: Pallet Collective + Pallet Democracy + 5 AI advisor agents, humans-only vote canonical per CHN-4. STUB v0.0.1 — implementation lands Phase 5 of Charon roadmap." +description = "Foundation governance on-chain canonical: Proposal + Vote + Tally per CHN-4 AI-Driven Human-Guarded. Phase 5.1 v0.0.28 scaffold — humans-only vote enforced canonical, 5 AI advisor agents advise-only canonical." [dependencies] -origin-chain-types.workspace = true +# Origin Chain canonical types (ProposalId + VoteStance + Vote source-of-truth) +origin-chain-types = { workspace = true } + +# FRAME canonical pallet macro + storage + dispatch + events + errors +frame-support = { workspace = true } +frame-system = { workspace = true } + +# Runtime API canonical (v0.0.31 — GovernanceApi.query_proposal read-only RPC) +sp-api = { workspace = true } + +# Substrate primitives canonical (BLAKE2 hashing canonical para derive_proposal_id) +sp-core = { workspace = true } +sp-runtime = { workspace = true } + +# SCALE codec canonical +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } + +[dev-dependencies] +# Mock runtime canonical para integration tests v0.0.30 — Pattern v0.0.26 mirror. +sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "origin-chain-types/std", + "frame-support/std", + "frame-system/std", + "sp-api/std", + "sp-core/std", + "sp-runtime/std", + "parity-scale-codec/std", + "scale-info/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/crates/origin-chain-governance/src/lib.rs b/crates/origin-chain-governance/src/lib.rs index 8932163..b1dcbf5 100644 --- a/crates/origin-chain-governance/src/lib.rs +++ b/crates/origin-chain-governance/src/lib.rs @@ -1,45 +1,355 @@ -//! # origin-chain-governance +//! # origin-chain-governance — Phase 5 scaffold v0.0.28 (Foundation Treasury Governance) //! -//! Foundation Treasury governance on-chain per **CHN-4**. AI-Driven Human-Guarded: +//! 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 (see `ChainError::AdvisorMayNotVote`). +//! from `governance_advisor` accounts. //! -//! ## Phase 5 milestones (Sesiones 29-35) +//! ## Status canonical: v0.0.28 — Scaffold trait-first foundation //! -//! - 5.1 `origin-foundation-governance-onchain` crate -//! - 5.2 `pallet_collective` (validator council Phase 1 governance) -//! - 5.3 OIP proposal submission (1,000 OGC stake minimum) -//! - 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 +//! 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` ✅ //! -//! ## Inmutables enforced +//! ## 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 -//! take precedence over treasury allocation efficiency +//! - **#36** Cognitive Liberty — daimonion opt-in privacy //! - **#41** Cross-Species Co-authorship — AGI accounts have full vote rights -//! UNLESS flagged `governance_advisor` (those advise only, not vote) +//! UNLESS flagged `governance_advisor` (advise only, not vote) -#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(missing_docs)] -/// Placeholder marker for the governance module. Replaced in Phase 5.1. -#[derive(Debug, Clone, Copy, Default)] -pub struct GovernanceConfig; +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::{ProposalId, VoteStance}; + use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; + use scale_info::TypeInfo; + + /// 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, + } + + /// Dispatchables canonical (scaffold v0.0.28 — real impl v0.0.30). + #[pallet::call] + impl Pallet { + /// CHN-4 5.3 canonical: `submit_proposal` scaffold dispatchable. + #[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)?; + // SCAFFOLD v0.0.28: real impl v0.0.30 + Ok(()) + } + + /// CHN-4 5.4 canonical: `cast_vote` scaffold dispatchable. + #[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)?; + // SCAFFOLD v0.0.28: real impl v0.0.30 + Ok(()) + } + + /// CHN-4 canonical: `finalize_proposal` scaffold dispatchable. + #[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)?; + // SCAFFOLD v0.0.28: real impl v0.0.30 + Ok(()) + } + } + + /// 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) + } + } +} #[cfg(test)] mod tests { use super::*; - use origin_chain_types::VoteStance; + use origin_chain_types::AccountId; #[test] - fn three_stances_canonical() { + 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; - let _cfg = GovernanceConfig; } } diff --git a/crates/origin-chain-types/src/governance.rs b/crates/origin-chain-types/src/governance.rs index 0537f0c..1e51e04 100644 --- a/crates/origin-chain-types/src/governance.rs +++ b/crates/origin-chain-types/src/governance.rs @@ -12,12 +12,18 @@ use crate::{AccountId, ChainError}; #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, )] +#[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)] +#[derive(parity_scale_codec::DecodeWithMemTracking)] +#[derive(parity_scale_codec::MaxEncodedLen, scale_info::TypeInfo)] pub struct ProposalId(pub [u8; 32]); /// Stance taken by a voter on a proposal. #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, )] +#[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)] +#[derive(parity_scale_codec::DecodeWithMemTracking)] +#[derive(parity_scale_codec::MaxEncodedLen, scale_info::TypeInfo)] pub enum VoteStance { /// In favor of the proposal. Aye, @@ -29,6 +35,9 @@ pub enum VoteStance { /// A single cast vote, stake-weighted at the time of the extrinsic. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)] +#[derive(parity_scale_codec::DecodeWithMemTracking)] +#[derive(parity_scale_codec::MaxEncodedLen, scale_info::TypeInfo)] pub struct Vote { /// Wallet that cast the vote. MUST NOT be flagged `governance_advisor`. pub voter: AccountId,