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:
1 parent
e6a7feecae
commit
afefcb2c04
3 files changed
+371
No files matched your search
@@ -124,6 +124,43 @@ pub mod pallet {
|
||||
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,
|
||||
@@ -185,6 +222,22 @@ pub mod pallet {
|
||||
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).
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
@@ -220,6 +273,22 @@ pub mod pallet {
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
|
||||
/// Pallet errors canonical.
|
||||
@@ -241,6 +310,12 @@ pub mod pallet {
|
||||
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,
|
||||
}
|
||||
|
||||
/// Minimum stake canonical proposal submission per CHN-4 5.3.
|
||||
@@ -401,6 +476,80 @@ pub mod pallet {
|
||||
|
||||
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.
|
||||
@@ -412,6 +561,24 @@ pub mod pallet {
|
||||
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).
|
||||
|
||||
@@ -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 ──────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user