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>
This commit is contained in:
davidherranandClaude Opus 4.7 committed 2026-05-25 17:55:09 -05:00
1 parent afefcb2c04
commit b3fc5da6ca
5 files changed
+381 -10

No files matched your search

+136 -8
View File
@@ -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<Proposal>;
/// 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<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;
}
}
@@ -238,6 +240,31 @@ pub mod pallet {
#[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)]
@@ -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<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.
@@ -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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::get(id), 0);
assert_ok!(Governance::veto_proposal(alice_origin(), id));
assert_eq!(crate::VetoCount::<Test>::get(id), 1);
});
}
// ─── Scenario Council 7: 2/3 threshold canonical compile-detect ─────
#[test]