feat(governance): v0.0.32 Phase 5.2 Council scaffold + member management (CHN-4 Technical Council)

Phase 5.2 TECHNICAL COUNCIL scaffold + management canonical. Extension del
existing origin-chain-governance pallet (NO new crate). 5-7 Council members
canonical per CHN-4 5.2.

## Council types + storage canonical

- CouncilMember struct (account + joined_at_block + role)
- CouncilRole enum (Member + Chair)
- MAX_COUNCIL_SIZE = 7 const + council_veto_threshold 2/3 ceiling
- Council StorageMap + CouncilSize StorageValue cached

## Council dispatchables + helpers canonical

- add_council_member (ensure_root, 6-step canonical)
- remove_council_member (ensure_root, 5-step canonical)
- Pallet::is_council_member + current_council_size + required_veto_votes pure helpers

## Council events + errors canonical

- Event::CouncilMemberAdded + CouncilMemberRemoved
- Error::NotCouncilMember + AlreadyCouncilMember + CouncilSizeExceeded

## Verification

- cargo test -p origin-chain-governance — 24 tests verde, 0 failures
  (7 Council scenarios + 17 prior governance preserved)

🏛️ Phase 5.2 scaffold canonical. Veto + Runtime API v0.0.33 next → Phase 5.2 COMPLETE.

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:51:59 -05:00
1 parent e6a7feecae
commit afefcb2c04
3 files changed
+371

No files matched your search

+44
View File
@@ -6,6 +6,50 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow
## [Unreleased] ## [Unreleased]
## [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
existing `origin-chain-governance` pallet con Council membership management.
Pattern Phase 5.1 extension (NO new crate, evolved existing). 5-7 Council
members canonical per CHN-4 5.2.
### Deliverables v0.0.32
**Council types canonical en pallet module**:
- `CouncilMember` struct (3 fields: account + joined_at_block + role)
- `CouncilRole` enum (Member + Chair) — Chair tie-breaking authority Phase 5.5+
- `MAX_COUNCIL_SIZE = 7` constant canonical CHN-4 5.2
- `council_veto_threshold(size) -> u32` const fn (2/3 ceiling division)
**Council storage canonical**:
- `Council<StorageMap<AccountId, CouncilMember>>` O(1) membership lookup
- `CouncilSize<StorageValue<u32>>` cached size for O(1) threshold check
**Council events + errors canonical**:
- `Event::CouncilMemberAdded` + `CouncilMemberRemoved`
- `Error::NotCouncilMember` + `AlreadyCouncilMember` + `CouncilSizeExceeded`
**Council dispatchables canonical (ensure_root governance-only)**:
- `add_council_member(origin, account, role)` 6-step canonical
- `remove_council_member(origin, account)` 5-step canonical
**Helpers canonical**:
- `Pallet::<T>::is_council_member(&account) -> bool` pure query
- `Pallet::<T>::current_council_size() -> u32`
- `Pallet::<T>::required_veto_votes() -> u32` (2/3 current size)
### Verification end-to-end canonical
- ✅ `cargo test -p origin-chain-governance` — **24 tests verde, 0 failures**:
- 7 Council scenarios (add + non-root rejected + duplicate + remove + non-member + max size + 2/3 threshold)
- 17 prior governance tests preserved (no regressions)
### Roadmap canonical post-v0.0.32
| Version | Trigger |
|---------|---------|
| **v0.0.33** | **veto_proposal dispatchable + Runtime API CouncilApi → Phase 5.2 COMPLETE** |
## [0.0.31] — 2026-05-25 — Phase 5.1 FOUNDATION COMPLETE — Runtime API GovernanceApi.query_proposal + query_vote 🎉 ## [0.0.31] — 2026-05-25 — Phase 5.1 FOUNDATION COMPLETE — Runtime API GovernanceApi.query_proposal + query_vote 🎉
**🎉 Phase 5.1 GOVERNANCE pallet FOUNDATION COMPLETE end-to-end** — Runtime API **🎉 Phase 5.1 GOVERNANCE pallet FOUNDATION COMPLETE end-to-end** — Runtime API
+167
View File
@@ -124,6 +124,43 @@ pub mod pallet {
Withdrawn, 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). /// Proposal canonical struct stored on-chain per pallet (Foundation OIP).
#[derive( #[derive(
Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq,
@@ -185,6 +222,22 @@ pub mod pallet {
OptionQuery, 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>;
/// Events emisión canonical (CHN-4 governance lifecycle). /// Events emisión canonical (CHN-4 governance lifecycle).
#[pallet::event] #[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)] #[pallet::generate_deposit(pub(super) fn deposit_event)]
@@ -220,6 +273,22 @@ pub mod pallet {
/// Tally nay_weight canonical. /// Tally nay_weight canonical.
nay_weight: u128, 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,
},
} }
/// Pallet errors canonical. /// Pallet errors canonical.
@@ -241,6 +310,12 @@ pub mod pallet {
AdvisorMayNotVote, AdvisorMayNotVote,
/// CHN-4 canonical: proposer canonical ya existe con ese hash content (duplicate prevention). /// CHN-4 canonical: proposer canonical ya existe con ese hash content (duplicate prevention).
ProposalIdCollision, 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,
} }
/// Minimum stake canonical proposal submission per CHN-4 5.3. /// Minimum stake canonical proposal submission per CHN-4 5.3.
@@ -401,6 +476,80 @@ pub mod pallet {
Ok(()) 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(())
}
} }
/// Helper canonical para convert T::AccountId → 32-byte canonical. /// Helper canonical para convert T::AccountId → 32-byte canonical.
@@ -412,6 +561,24 @@ pub mod pallet {
bytes 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. /// Pure helpers canonical para Runtime API v0.0.31 consumers + tests.
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/// Query proposal canonical sin storage mutation (read-only canonical). /// Query proposal canonical sin storage mutation (read-only canonical).
@@ -240,6 +240,166 @@ fn scenario_9_vote_closed_period_rejected_canonical() {
}); });
} }
// ─── Scenario Council 1: add_council_member canonical (root only) ──
#[test]
fn council_scenario_1_add_member_canonical() {
new_test_ext().execute_with(|| {
use crate::pallet::CouncilRole;
use frame_support::dispatch::RawOrigin;
// Root canonical add canonical council member
assert_ok!(Governance::add_council_member(
RawOrigin::Root.into(),
alice_account(),
CouncilRole::Chair,
));
// Validate storage canonical
assert!(Governance::is_council_member(&alice_account()));
assert_eq!(Governance::current_council_size(), 1);
let member = crate::Council::<Test>::get(&alice_account()).unwrap();
assert!(matches!(member.role, CouncilRole::Chair));
});
}
// ─── Scenario Council 2: non-root add rejected canonical ────────────
#[test]
fn council_scenario_2_non_root_add_rejected_canonical() {
new_test_ext().execute_with(|| {
use crate::pallet::CouncilRole;
// Non-root origin → BadOrigin canonical (ensure_root rejects)
let result = Governance::add_council_member(
alice_origin(),
bob_account(),
CouncilRole::Member,
);
assert!(result.is_err());
});
}
// ─── Scenario Council 3: duplicate add rejected canonical ───────────
#[test]
fn council_scenario_3_duplicate_add_rejected_canonical() {
new_test_ext().execute_with(|| {
use crate::pallet::CouncilRole;
use frame_support::dispatch::RawOrigin;
// First add canonical
assert_ok!(Governance::add_council_member(
RawOrigin::Root.into(),
alice_account(),
CouncilRole::Member,
));
// Second add same account → AlreadyCouncilMember
assert_noop!(
Governance::add_council_member(
RawOrigin::Root.into(),
alice_account(),
CouncilRole::Member,
),
Error::<Test>::AlreadyCouncilMember
);
});
}
// ─── Scenario Council 4: remove_council_member canonical ────────────
#[test]
fn council_scenario_4_remove_member_canonical() {
new_test_ext().execute_with(|| {
use crate::pallet::CouncilRole;
use frame_support::dispatch::RawOrigin;
// Add + remove canonical
assert_ok!(Governance::add_council_member(
RawOrigin::Root.into(),
alice_account(),
CouncilRole::Member,
));
assert_eq!(Governance::current_council_size(), 1);
assert_ok!(Governance::remove_council_member(
RawOrigin::Root.into(),
alice_account(),
));
assert!(!Governance::is_council_member(&alice_account()));
assert_eq!(Governance::current_council_size(), 0);
});
}
// ─── Scenario Council 5: remove non-member rejected canonical ───────
#[test]
fn council_scenario_5_remove_non_member_rejected_canonical() {
new_test_ext().execute_with(|| {
use frame_support::dispatch::RawOrigin;
// Remove account never added → NotCouncilMember
assert_noop!(
Governance::remove_council_member(
RawOrigin::Root.into(),
alice_account(),
),
Error::<Test>::NotCouncilMember
);
});
}
// ─── Scenario Council 6: max size enforcement canonical ─────────────
#[test]
fn council_scenario_6_max_size_enforcement_canonical() {
new_test_ext().execute_with(|| {
use crate::pallet::{CouncilRole, MAX_COUNCIL_SIZE};
use frame_support::dispatch::RawOrigin;
// Add MAX_COUNCIL_SIZE members canonical
for i in 0..MAX_COUNCIL_SIZE {
let account = sp_runtime::AccountId32::new([i as u8 + 10; 32]);
assert_ok!(Governance::add_council_member(
RawOrigin::Root.into(),
account,
CouncilRole::Member,
));
}
assert_eq!(Governance::current_council_size(), MAX_COUNCIL_SIZE);
// Adding 8th member → CouncilSizeExceeded
let overflow = sp_runtime::AccountId32::new([99u8; 32]);
assert_noop!(
Governance::add_council_member(
RawOrigin::Root.into(),
overflow,
CouncilRole::Member,
),
Error::<Test>::CouncilSizeExceeded
);
});
}
// ─── Scenario Council 7: 2/3 threshold canonical compile-detect ─────
#[test]
fn council_scenario_7_veto_threshold_2_of_3_canonical() {
use crate::pallet::council_veto_threshold;
// 2/3 ceiling division canonical canonical:
assert_eq!(council_veto_threshold(0), 0);
assert_eq!(council_veto_threshold(1), 1); // ceiling(2/3) = 1
assert_eq!(council_veto_threshold(2), 2); // ceiling(4/3) = 2
assert_eq!(council_veto_threshold(3), 2); // ceiling(6/3) = 2
assert_eq!(council_veto_threshold(4), 3); // ceiling(8/3) = 3
assert_eq!(council_veto_threshold(5), 4); // ceiling(10/3) = 4
assert_eq!(council_veto_threshold(6), 4); // ceiling(12/3) = 4
assert_eq!(council_veto_threshold(7), 5); // ceiling(14/3) = 5
}
// ─── Scenario 10: Full lifecycle walkthrough canonical ────────────── // ─── Scenario 10: Full lifecycle walkthrough canonical ──────────────
#[test] #[test]