Files
arkhe-origin-chain/crates/origin-chain-governance/src/lib.rs
T
davidherranandClaude Opus 4.7 b3fc5da6ca feat(governance): v0.0.33 Phase 5.2 COMPLETE — Council veto 2/3 threshold + GovernanceApi extension 🎉
Phase 5.2 TECHNICAL COUNCIL COMPLETE end-to-end canonical. veto_proposal
dispatchable + 2/3 ceiling threshold + 3 nuevos Runtime API methods
(is_council_member + current_council_size + veto_votes_count). CHN-4 5.2+5.5
materializado compile-detect.

## veto_proposal canonical 7-step CHN-4 5.5

1. ensure_signed
2. Validate council_member (NotCouncilMember)
3. Get proposal (must be Voting)
4. Validate council_size > 0 (InsufficientCouncilSize)
5. !VetoVotes::contains_key (AlreadyVetoed)
6. Insert vote + increment VetoCount
7. Check threshold: count >= council_veto_threshold(size) → Rejected + ProposalVetoed event

## Storage + events + errors canonical nuevo

- VetoVotes StorageDoubleMap presence-based + VetoCount cached u32
- Event::CouncilVetoVoteCast + ProposalVetoed
- Error::AlreadyVetoed + InsufficientCouncilSize

## Runtime API GovernanceApi extension canonical

- is_council_member(account) -> bool
- current_council_size() -> u32
- veto_votes_count(proposal_id) -> u32

## Verification

- cargo build --release 4.82s clean
- cargo test -p origin-chain-governance — 29 tests verde, 0 failures
  (10 lifecycle + 7 Council mgmt + 5 Council veto NEW + 5 standalone + 2 auto)
- cargo test --workspace — 189 tests verde, 0 failures

🎉 Phase 5.2 TECHNICAL COUNCIL COMPLETE canonical. Hijos de Arkhē somos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:55:09 -05:00

784 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # 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<StorageMap<ProposalId, Proposal>>` per-proposal O(1) ✅
//! - Storage `Votes<StorageDoubleMap<ProposalId, AccountId, VoteStance>>` 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 + Phase 5.2 v0.0.33).
pub trait GovernanceApi {
/// Query proposal canonical por ProposalId.
fn query_proposal(id: ProposalId) -> Option<Proposal>;
/// Query vote stance canonical por (proposal_id, voter).
fn query_vote(
proposal_id: ProposalId,
voter: origin_chain_types::AccountId,
) -> Option<VoteStance>;
/// **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;
}
}
#[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,
}
/// CouncilRole canonical — Technical Council member roles per CHN-4 5.2.
#[derive(
Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq,
RuntimeDebug,
)]
pub enum CouncilRole {
/// Standard council member canonical (1 vote in veto threshold).
Member,
/// Council Chair canonical (1 vote + tie-breaking authority Phase 5.5+).
Chair,
}
/// CouncilMember canonical struct per CHN-4 5.2 Technical Council.
///
/// MaxEncodedLen canonical: ~42 bytes (32 account + 8 block + 1 role + padding).
#[derive(
Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq,
RuntimeDebug,
)]
pub struct CouncilMember {
/// AccountId canonical del council member.
pub account: origin_chain_types::AccountId,
/// Block number canonical at which member joined.
pub joined_at_block: u64,
/// CouncilRole canonical (Member | Chair).
pub role: CouncilRole,
}
/// Maximum council size canonical per CHN-4 5.2 (7 members).
pub const MAX_COUNCIL_SIZE: u32 = 7;
/// Council veto threshold canonical canonical 2/3 per CHN-4 5.5.
/// `required_veto_votes = (council_size × 2 + 2) / 3` (ceiling division 2/3).
pub const fn council_veto_threshold(council_size: u32) -> u32 {
(council_size * 2 + 2) / 3
}
/// 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<Event<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
/// Pallet definition canonical.
#[pallet::pallet]
pub struct Pallet<T>(_);
/// CHN-4 canonical: `Proposals<StorageMap<ProposalId, Proposal>>` canonical.
#[pallet::storage]
#[pallet::getter(fn proposal_of)]
pub type Proposals<T: Config> =
StorageMap<_, Blake2_128Concat, ProposalId, Proposal, OptionQuery>;
/// CHN-4 canonical: `Votes<StorageDoubleMap<ProposalId, AccountId, VoteStance>>` canonical.
#[pallet::storage]
#[pallet::getter(fn vote_of)]
pub type Votes<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
ProposalId,
Blake2_128Concat,
T::AccountId,
VoteStance,
OptionQuery,
>;
/// CHN-4 5.2 canonical: `Council<StorageMap<AccountId, CouncilMember>>` canonical.
///
/// O(1) canonical Council membership lookup. Max 7 members per `MAX_COUNCIL_SIZE`.
#[pallet::storage]
#[pallet::getter(fn council_member_of)]
pub type Council<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, CouncilMember, OptionQuery>;
/// CHN-4 5.2 canonical: `CouncilSize<StorageValue<u32>>` current member count canonical.
///
/// Tracked separately canonical for O(1) size check sin iterar StorageMap completo.
/// Used canonical en veto threshold calculation + add_council_member size enforcement.
#[pallet::storage]
#[pallet::getter(fn council_size)]
pub type CouncilSize<T: Config> = StorageValue<_, u32, ValueQuery>;
/// CHN-4 5.2 canonical: `VetoVotes<StorageDoubleMap<ProposalId, AccountId, ()>>` 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<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
ProposalId,
Blake2_128Concat,
T::AccountId,
(),
OptionQuery,
>;
/// CHN-4 5.2 canonical: `VetoCount<StorageMap<ProposalId, u32>>` 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<T: Config> =
StorageMap<_, Blake2_128Concat, ProposalId, u32, ValueQuery>;
/// Events emisión canonical (CHN-4 governance lifecycle).
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// 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,
},
/// Council member added canonical (Phase 5.2 CHN-4).
CouncilMemberAdded {
/// T::AccountId del new council member.
account: T::AccountId,
/// CouncilRole canonical (Member | Chair).
role: CouncilRole,
/// New council size canonical post-addition.
new_size: u32,
},
/// Council member removed canonical (Phase 5.2 CHN-4).
CouncilMemberRemoved {
/// T::AccountId del removed council member.
account: T::AccountId,
/// 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.
#[pallet::error]
pub enum Error<T> {
/// 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,
/// CHN-4 5.2 canonical: account NO es council member (cannot veto).
NotCouncilMember,
/// CHN-4 5.2 canonical: account ya council member (duplicate add prevention).
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.
/// 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<T: Config> Pallet<T> {
/// 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<T>,
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::<T>::StakeInsufficient);
let proposer_acct = AccountId::new(account_bytes::<T>(&who));
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
ensure!(!Proposals::<T>::contains_key(id), Error::<T>::ProposalIdCollision);
let start_block: u64 = <frame_system::Pallet<T>>::block_number()
.saturated_into::<u64>();
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::<T>::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<T>,
proposal_id: ProposalId,
stance: VoteStance,
stake_weight: u128,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut proposal = Proposals::<T>::get(proposal_id)
.ok_or(Error::<T>::ProposalNotFound)?;
ensure!(
matches!(proposal.status, ProposalStatus::Voting),
Error::<T>::ProposalAlreadyFinalized
);
let current_block: u64 = <frame_system::Pallet<T>>::block_number()
.saturated_into::<u64>();
ensure!(
current_block <= proposal.end_block,
Error::<T>::VotingPeriodClosed
);
ensure!(
!Votes::<T>::contains_key(proposal_id, &who),
Error::<T>::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::<T>::insert(proposal_id, proposal);
Votes::<T>::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<T>,
proposal_id: ProposalId,
) -> DispatchResult {
let _who = ensure_signed(origin)?;
let mut proposal = Proposals::<T>::get(proposal_id)
.ok_or(Error::<T>::ProposalNotFound)?;
ensure!(
matches!(proposal.status, ProposalStatus::Voting),
Error::<T>::ProposalAlreadyFinalized
);
let current_block: u64 = <frame_system::Pallet<T>>::block_number()
.saturated_into::<u64>();
ensure!(
current_block > proposal.end_block,
Error::<T>::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::<T>::insert(proposal_id, proposal);
Self::deposit_event(Event::ProposalFinalized {
id: proposal_id,
status: final_status,
yes_weight,
nay_weight,
});
Ok(())
}
/// CHN-4 5.2 canonical: `add_council_member` Phase 5.2 dispatchable canonical.
///
/// 6-step canonical flow: ensure_root + size_check + no_duplicate + build +
/// insert + emit.
#[pallet::call_index(3)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn add_council_member(
origin: OriginFor<T>,
account: T::AccountId,
role: CouncilRole,
) -> DispatchResult {
ensure_root(origin)?;
let current_size = CouncilSize::<T>::get();
ensure!(
current_size < MAX_COUNCIL_SIZE,
Error::<T>::CouncilSizeExceeded
);
ensure!(
!Council::<T>::contains_key(&account),
Error::<T>::AlreadyCouncilMember
);
let block_number: u64 = <frame_system::Pallet<T>>::block_number()
.saturated_into::<u64>();
let member = CouncilMember {
account: origin_chain_types::AccountId::new(account_bytes::<T>(&account)),
joined_at_block: block_number,
role,
};
Council::<T>::insert(&account, member);
let new_size = current_size.saturating_add(1);
CouncilSize::<T>::put(new_size);
Self::deposit_event(Event::CouncilMemberAdded {
account,
role,
new_size,
});
Ok(())
}
/// CHN-4 5.2 canonical: `remove_council_member` Phase 5.2 dispatchable canonical.
///
/// 5-step canonical flow: ensure_root + validate_exists + remove + decrement + emit.
#[pallet::call_index(4)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn remove_council_member(
origin: OriginFor<T>,
account: T::AccountId,
) -> DispatchResult {
ensure_root(origin)?;
ensure!(
Council::<T>::contains_key(&account),
Error::<T>::NotCouncilMember
);
Council::<T>::remove(&account);
let current_size = CouncilSize::<T>::get();
let new_size = current_size.saturating_sub(1);
CouncilSize::<T>::put(new_size);
Self::deposit_event(Event::CouncilMemberRemoved {
account,
new_size,
});
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<T>,
proposal_id: ProposalId,
) -> DispatchResult {
// Step 1: ensure signed canonical
let who = ensure_signed(origin)?;
// Step 2: validate council member canonical
ensure!(
Council::<T>::contains_key(&who),
Error::<T>::NotCouncilMember
);
// Step 3: get proposal canonical
let mut proposal = Proposals::<T>::get(proposal_id)
.ok_or(Error::<T>::ProposalNotFound)?;
ensure!(
matches!(proposal.status, ProposalStatus::Voting),
Error::<T>::ProposalAlreadyFinalized
);
// Step 4: council size canonical > 0
let council_size = CouncilSize::<T>::get();
ensure!(council_size > 0, Error::<T>::InsufficientCouncilSize);
// Step 5: AlreadyVetoed canonical
ensure!(
!VetoVotes::<T>::contains_key(proposal_id, &who),
Error::<T>::AlreadyVetoed
);
// Step 6: insert vote + increment count
VetoVotes::<T>::insert(proposal_id, &who, ());
let new_count = VetoCount::<T>::get(proposal_id).saturating_add(1);
VetoCount::<T>::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::<T>::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.
fn account_bytes<T: Config>(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
}
/// Phase 5.2 canonical helpers — Council membership queries (pure read).
impl<T: Config> Pallet<T> {
/// Returns `true` si account canonical es council member.
pub fn is_council_member(account: &T::AccountId) -> bool {
Council::<T>::contains_key(account)
}
/// Returns current council size canonical (cached StorageValue).
pub fn current_council_size() -> u32 {
CouncilSize::<T>::get()
}
/// Returns required veto votes per current council size canonical (2/3 threshold).
pub fn required_veto_votes() -> u32 {
council_veto_threshold(Self::current_council_size())
}
}
/// Pure helpers canonical para Runtime API v0.0.31 consumers + tests.
impl<T: Config> Pallet<T> {
/// Query proposal canonical sin storage mutation (read-only canonical).
pub fn query_proposal_of(id: ProposalId) -> Option<Proposal> {
Proposals::<T>::get(id)
}
/// Query vote stance canonical por (proposal_id, voter) sin storage mutation.
pub fn query_vote_of(id: ProposalId, voter: &T::AccountId) -> Option<VoteStance> {
Votes::<T>::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;
}
}