diff --git a/Cargo.toml b/Cargo.toml index 1efd369..488576a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/bitcell-wallet-gui", "crates/bitcell-compiler", "crates/bitcell-light-client", + "crates/bitcell-governance", ] resolver = "2" diff --git a/crates/bitcell-governance/Cargo.toml b/crates/bitcell-governance/Cargo.toml new file mode 100644 index 0000000..8159fca --- /dev/null +++ b/crates/bitcell-governance/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bitcell-governance" +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +serde.workspace = true +thiserror.workspace = true +bincode.workspace = true +tracing.workspace = true +hex.workspace = true + +[dev-dependencies] +proptest.workspace = true diff --git a/crates/bitcell-governance/README.md b/crates/bitcell-governance/README.md new file mode 100644 index 0000000..5d2065e --- /dev/null +++ b/crates/bitcell-governance/README.md @@ -0,0 +1,86 @@ +# bitcell-governance + +On-chain governance system for the BitCell blockchain, implementing RC3-005 requirements. + +## Features + +- **Proposal System**: Submit proposals for parameter changes, treasury spending, and protocol upgrades +- **Token-Weighted Voting**: Democratic voting with 1 CELL = 1 vote (linear) or quadratic voting option +- **Vote Delegation**: Delegate voting power to trusted representatives +- **Timelock Execution**: Mandatory waiting period before proposal execution +- **Guardian Controls**: Multi-sig emergency cancellation and fast-track capabilities +- **Comprehensive Testing**: 20 unit tests covering all functionality + +## Quick Start + +Add to your `Cargo.toml`: + +```toml +[dependencies] +bitcell-governance = { path = "crates/bitcell-governance" } +``` + +## Usage + +```rust +use bitcell_governance::*; + +// Create governance manager with guardians +let guardians = vec![guardian1, guardian2, guardian3]; +let mut governance = GovernanceManager::new(guardians); + +// Submit a proposal +let proposal_id = governance.submit_proposal( + proposer_pubkey, + ProposalType::ParameterChange { + parameter: "block_time".to_string(), + new_value: vec![10], + }, + "Reduce block time to 10s".to_string(), + 14400, // voting period in blocks + current_block, +)?; + +// Vote on the proposal +governance.vote( + proposal_id, + voter_pubkey, + VoteType::For, + token_balance, + current_block, + false, // quadratic voting +)?; + +// Finalize after voting period +governance.finalize_proposal(proposal_id, current_block + 15000)?; + +// Execute after timelock +governance.execute_proposal(proposal_id, current_block + 30000)?; +``` + +## Documentation + +See `docs/GOVERNANCE.md` for comprehensive documentation including: + +- Architecture overview +- Proposal types +- Voting process +- Guardian controls +- Security features +- Best practices +- Integration guide + +## Testing + +```bash +cargo test -p bitcell-governance +``` + +All tests pass: +- 20 unit tests +- Coverage of all major functionality +- Edge case testing + +## License + +MIT OR Apache-2.0 diff --git a/crates/bitcell-governance/src/execution.rs b/crates/bitcell-governance/src/execution.rs new file mode 100644 index 0000000..ead67aa --- /dev/null +++ b/crates/bitcell-governance/src/execution.rs @@ -0,0 +1,346 @@ +//! Proposal execution system with timelock and guardian controls + +use crate::{Error, Result, ProposalId, ProposalType}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Timelock delay in blocks +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct TimelockDelay { + /// Number of blocks to wait before execution + pub blocks: u64, +} + +impl TimelockDelay { + /// Standard timelock delay (e.g., 2 days assuming 12s blocks) + pub fn standard() -> Self { + Self { blocks: 14400 } // ~2 days + } + + /// Fast track delay (e.g., 6 hours) + pub fn fast_track() -> Self { + Self { blocks: 1800 } // ~6 hours + } + + /// Emergency delay (e.g., 1 hour) + pub fn emergency() -> Self { + Self { blocks: 300 } // ~1 hour + } +} + +impl Default for TimelockDelay { + fn default() -> Self { + Self::standard() + } +} + +/// Guardian action types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum GuardianAction { + /// Cancel a proposal + Cancel(ProposalId), + + /// Fast-track a proposal (reduce timelock) + FastTrack(ProposalId), + + /// Veto a proposal execution + Veto(ProposalId), +} + +/// Queued proposal for execution +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueuedProposal { + /// Proposal ID + pub proposal_id: ProposalId, + + /// Proposal type + pub proposal_type: ProposalType, + + /// Block when it was queued + pub queued_block: u64, + + /// Timelock delay + pub timelock: TimelockDelay, + + /// Block when it can be executed + pub execution_block: u64, +} + +impl QueuedProposal { + pub fn new( + proposal_id: ProposalId, + proposal_type: ProposalType, + queued_block: u64, + timelock: TimelockDelay, + ) -> Self { + Self { + proposal_id, + proposal_type, + queued_block, + timelock, + execution_block: queued_block + timelock.blocks, + } + } + + /// Check if proposal is ready for execution + pub fn is_executable(&self, current_block: u64) -> bool { + current_block >= self.execution_block + } +} + +/// Execution queue managing timelocked proposals +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExecutionQueue { + /// Queued proposals awaiting execution + queue: HashMap, +} + +impl ExecutionQueue { + pub fn new() -> Self { + Self { + queue: HashMap::new(), + } + } + + /// Enqueue a proposal for execution after timelock + pub fn enqueue( + &mut self, + proposal_id: ProposalId, + current_block: u64, + proposal_type: ProposalType, + ) { + let timelock = match &proposal_type { + ProposalType::ParameterChange { .. } => TimelockDelay::standard(), + ProposalType::TreasurySpending { .. } => TimelockDelay::fast_track(), + ProposalType::ProtocolUpgrade { .. } => TimelockDelay::standard(), + }; + + let queued = QueuedProposal::new( + proposal_id, + proposal_type, + current_block, + timelock, + ); + + let execution_block = queued.execution_block; + self.queue.insert(proposal_id, queued); + + tracing::info!( + proposal_id = proposal_id.0, + execution_block = execution_block, + "Proposal queued for execution after timelock" + ); + } + + /// Execute a proposal (must be past timelock) + pub fn execute( + &mut self, + proposal_id: ProposalId, + current_block: u64, + ) -> Result<()> { + let queued = self.queue.get(&proposal_id) + .ok_or(Error::ProposalNotFound)?; + + if !queued.is_executable(current_block) { + return Err(Error::ExecutionLocked); + } + + // Remove from queue + self.queue.remove(&proposal_id); + + tracing::info!( + proposal_id = proposal_id.0, + "Proposal executed and removed from queue" + ); + + Ok(()) + } + + /// Cancel a proposal (guardian action) + pub fn cancel(&mut self, proposal_id: ProposalId) -> Result<()> { + self.queue.remove(&proposal_id) + .ok_or(Error::ProposalNotFound)?; + + tracing::warn!( + proposal_id = proposal_id.0, + "Proposal cancelled and removed from execution queue" + ); + + Ok(()) + } + + /// Fast-track a proposal (guardian action) + pub fn fast_track( + &mut self, + proposal_id: ProposalId, + current_block: u64, + ) -> Result<()> { + let queued = self.queue.get_mut(&proposal_id) + .ok_or(Error::ProposalNotFound)?; + + queued.timelock = TimelockDelay::fast_track(); + queued.execution_block = current_block + queued.timelock.blocks; + + tracing::info!( + proposal_id = proposal_id.0, + new_execution_block = queued.execution_block, + "Proposal fast-tracked" + ); + + Ok(()) + } + + /// Get all executable proposals + pub fn get_executable(&self, current_block: u64) -> Vec { + self.queue.values() + .filter(|p| p.is_executable(current_block)) + .map(|p| p.proposal_id) + .collect() + } + + /// Get proposal from queue + pub fn get(&self, proposal_id: ProposalId) -> Option<&QueuedProposal> { + self.queue.get(&proposal_id) + } +} + +impl Default for ExecutionQueue { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timelock_delays() { + let standard = TimelockDelay::standard(); + assert_eq!(standard.blocks, 14400); + + let fast = TimelockDelay::fast_track(); + assert_eq!(fast.blocks, 1800); + + let emergency = TimelockDelay::emergency(); + assert_eq!(emergency.blocks, 300); + } + + #[test] + fn test_queued_proposal() { + let proposal = QueuedProposal::new( + ProposalId(1), + ProposalType::ParameterChange { + parameter: "test".to_string(), + new_value: vec![1], + }, + 100, + TimelockDelay::fast_track(), + ); + + assert_eq!(proposal.execution_block, 1900); // 100 + 1800 + assert!(!proposal.is_executable(1000)); + assert!(proposal.is_executable(1900)); + assert!(proposal.is_executable(2000)); + } + + #[test] + fn test_execution_queue() { + let mut queue = ExecutionQueue::new(); + + queue.enqueue( + ProposalId(1), + 100, + ProposalType::TreasurySpending { + recipient: [1u8; 33], + amount: 1000, + reason: "Test".to_string(), + }, + ); + + let queued = queue.get(ProposalId(1)).unwrap(); + assert_eq!(queued.execution_block, 1900); // Fast track for treasury + + // Cannot execute before timelock + let result = queue.execute(ProposalId(1), 1000); + assert!(matches!(result, Err(Error::ExecutionLocked))); + + // Can execute after timelock + queue.execute(ProposalId(1), 2000).unwrap(); + assert!(queue.get(ProposalId(1)).is_none()); + } + + #[test] + fn test_cancel() { + let mut queue = ExecutionQueue::new(); + + queue.enqueue( + ProposalId(1), + 100, + ProposalType::ParameterChange { + parameter: "test".to_string(), + new_value: vec![1], + }, + ); + + queue.cancel(ProposalId(1)).unwrap(); + assert!(queue.get(ProposalId(1)).is_none()); + } + + #[test] + fn test_fast_track() { + let mut queue = ExecutionQueue::new(); + + queue.enqueue( + ProposalId(1), + 100, + ProposalType::ParameterChange { + parameter: "test".to_string(), + new_value: vec![1], + }, + ); + + // Original execution block + let original = queue.get(ProposalId(1)).unwrap().execution_block; + assert_eq!(original, 14500); // 100 + 14400 (standard) + + // Fast track + queue.fast_track(ProposalId(1), 200).unwrap(); + + let new_exec_block = queue.get(ProposalId(1)).unwrap().execution_block; + assert_eq!(new_exec_block, 2000); // 200 + 1800 (fast track) + } + + #[test] + fn test_get_executable() { + let mut queue = ExecutionQueue::new(); + + queue.enqueue( + ProposalId(1), + 100, + ProposalType::TreasurySpending { + recipient: [1u8; 33], + amount: 1000, + reason: "Test".to_string(), + }, + ); + + queue.enqueue( + ProposalId(2), + 100, + ProposalType::ParameterChange { + parameter: "test".to_string(), + new_value: vec![1], + }, + ); + + // At block 2000, only proposal 1 is executable (fast track) + let executable = queue.get_executable(2000); + assert_eq!(executable.len(), 1); + assert_eq!(executable[0].0, 1); + + // At block 15000, both are executable + let executable = queue.get_executable(15000); + assert_eq!(executable.len(), 2); + } +} diff --git a/crates/bitcell-governance/src/lib.rs b/crates/bitcell-governance/src/lib.rs new file mode 100644 index 0000000..206cbb9 --- /dev/null +++ b/crates/bitcell-governance/src/lib.rs @@ -0,0 +1,475 @@ +//! On-Chain Governance System for BitCell +//! +//! Implements RC3-005 requirements: +//! - Proposal System for parameter changes, treasury spending, and protocol upgrades +//! - Voting Mechanism with token-weighted voting, delegation, and optional quadratic voting +//! - Execution with timelock delay, emergency cancel, and multi-sig guardian + +mod serde_pubkey; +pub mod proposal; +pub mod voting; +pub mod execution; + +pub use proposal::{Proposal, ProposalType, ProposalStatus, ProposalId}; +pub use voting::{Vote, VoteType, VotingPower, Delegation}; +pub use execution::{ExecutionQueue, TimelockDelay, GuardianAction}; + +use std::collections::HashMap; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Proposal not found")] + ProposalNotFound, + + #[error("Invalid proposal")] + InvalidProposal, + + #[error("Voting period ended")] + VotingPeriodEnded, + + #[error("Voting period not ended")] + VotingPeriodNotEnded, + + #[error("Already voted")] + AlreadyVoted, + + #[error("Insufficient voting power")] + InsufficientVotingPower, + + #[error("Execution locked")] + ExecutionLocked, + + #[error("Not authorized")] + NotAuthorized, + + #[error("Invalid timelock")] + InvalidTimelock, +} + +/// Governance system manager +pub struct GovernanceManager { + /// Active proposals + pub proposals: HashMap, + + /// Vote records + pub votes: HashMap>, + + /// Delegations + pub delegations: HashMap<[u8; 33], Delegation>, + + /// Execution queue + pub execution_queue: ExecutionQueue, + + /// Multi-sig guardians + pub guardians: Vec<[u8; 33]>, + + /// Next proposal ID + next_proposal_id: u64, +} + +impl GovernanceManager { + pub fn new(guardians: Vec<[u8; 33]>) -> Self { + Self { + proposals: HashMap::new(), + votes: HashMap::new(), + delegations: HashMap::new(), + execution_queue: ExecutionQueue::new(), + guardians, + next_proposal_id: 1, + } + } + + /// Submit a new proposal + pub fn submit_proposal( + &mut self, + proposer: [u8; 33], + proposal_type: ProposalType, + description: String, + voting_period_blocks: u64, + current_block: u64, + ) -> Result { + let proposal_id = ProposalId(self.next_proposal_id); + self.next_proposal_id += 1; + + let proposal = Proposal::new( + proposal_id, + proposer, + proposal_type, + description, + current_block, + voting_period_blocks, + ); + + self.proposals.insert(proposal_id, proposal); + self.votes.insert(proposal_id, HashMap::new()); + + tracing::info!( + proposal_id = proposal_id.0, + proposer = %hex::encode(&proposer), + "New proposal submitted" + ); + + Ok(proposal_id) + } + + /// Cast a vote on a proposal + pub fn vote( + &mut self, + proposal_id: ProposalId, + voter: [u8; 33], + vote_type: VoteType, + voting_power: u64, + current_block: u64, + quadratic: bool, + ) -> Result<()> { + let proposal = self.proposals.get_mut(&proposal_id) + .ok_or(Error::ProposalNotFound)?; + + // Check if voting period is still active + if current_block > proposal.voting_end_block { + return Err(Error::VotingPeriodEnded); + } + + // Check if already voted + let votes = self.votes.get_mut(&proposal_id).unwrap(); + if votes.contains_key(&voter) { + return Err(Error::AlreadyVoted); + } + + // Calculate effective voting power (quadratic if enabled) + let effective_power = if quadratic { + VotingPower::quadratic(voting_power) + } else { + VotingPower::linear(voting_power) + }; + + // Record vote + let vote = Vote { + voter, + vote_type: vote_type.clone(), + voting_power: effective_power, + block_number: current_block, + }; + + // Update proposal tallies (use saturating add to prevent overflow) + match vote_type { + VoteType::For => proposal.votes_for = proposal.votes_for.saturating_add(effective_power.value), + VoteType::Against => proposal.votes_against = proposal.votes_against.saturating_add(effective_power.value), + VoteType::Abstain => proposal.votes_abstain = proposal.votes_abstain.saturating_add(effective_power.value), + } + + votes.insert(voter, vote); + + tracing::info!( + proposal_id = proposal_id.0, + voter = %hex::encode(&voter), + vote_type = ?vote_type, + power = effective_power.value, + "Vote cast" + ); + + Ok(()) + } + + /// Finalize a proposal after voting period ends + pub fn finalize_proposal( + &mut self, + proposal_id: ProposalId, + current_block: u64, + ) -> Result<()> { + let proposal = self.proposals.get_mut(&proposal_id) + .ok_or(Error::ProposalNotFound)?; + + // Check if voting period has ended + if current_block <= proposal.voting_end_block { + return Err(Error::VotingPeriodNotEnded); + } + + // Determine outcome + let total_votes = proposal.votes_for + proposal.votes_against + proposal.votes_abstain; + let quorum_met = total_votes >= proposal.quorum_threshold; + let majority_met = proposal.votes_for > proposal.votes_against; + + if quorum_met && majority_met { + proposal.status = ProposalStatus::Passed; + + // Queue for execution with timelock + self.execution_queue.enqueue( + proposal_id, + current_block, + proposal.proposal_type.clone(), + ); + + tracing::info!( + proposal_id = proposal_id.0, + votes_for = proposal.votes_for, + votes_against = proposal.votes_against, + "Proposal passed and queued for execution" + ); + } else { + proposal.status = ProposalStatus::Rejected; + + tracing::info!( + proposal_id = proposal_id.0, + votes_for = proposal.votes_for, + votes_against = proposal.votes_against, + quorum_met = quorum_met, + majority_met = majority_met, + "Proposal rejected" + ); + } + + Ok(()) + } + + /// Execute a proposal after timelock expires + pub fn execute_proposal( + &mut self, + proposal_id: ProposalId, + current_block: u64, + ) -> Result<()> { + let proposal = self.proposals.get(&proposal_id) + .ok_or(Error::ProposalNotFound)?; + + if proposal.status != ProposalStatus::Passed { + return Err(Error::InvalidProposal); + } + + self.execution_queue.execute(proposal_id, current_block)?; + + // Mark as executed + if let Some(p) = self.proposals.get_mut(&proposal_id) { + p.status = ProposalStatus::Executed; + } + + tracing::info!( + proposal_id = proposal_id.0, + "Proposal executed" + ); + + Ok(()) + } + + /// Emergency cancel by guardians + pub fn emergency_cancel( + &mut self, + proposal_id: ProposalId, + guardian_signatures: Vec<[u8; 33]>, + ) -> Result<()> { + // Verify sufficient guardian signatures (require 2/3 majority) + // Use checked arithmetic to prevent overflow + let required = self.guardians.len() + .checked_mul(2) + .and_then(|v| v.checked_add(2)) + .map(|v| v / 3) + .unwrap_or(0); + + let valid_signatures = guardian_signatures.iter() + .filter(|sig| self.guardians.contains(sig)) + .count(); + + if valid_signatures < required { + return Err(Error::NotAuthorized); + } + + let proposal = self.proposals.get_mut(&proposal_id) + .ok_or(Error::ProposalNotFound)?; + + proposal.status = ProposalStatus::Cancelled; + + // Try to cancel from execution queue (may not be queued yet) + let _ = self.execution_queue.cancel(proposal_id); + + tracing::warn!( + proposal_id = proposal_id.0, + guardian_signatures = valid_signatures, + "Proposal emergency cancelled by guardians" + ); + + Ok(()) + } + + /// Delegate voting power + pub fn delegate( + &mut self, + delegator: [u8; 33], + delegatee: [u8; 33], + amount: u64, + ) -> Result<()> { + let delegation = Delegation { + delegator, + delegatee, + amount, + }; + + self.delegations.insert(delegator, delegation); + + tracing::info!( + delegator = %hex::encode(&delegator), + delegatee = %hex::encode(&delegatee), + amount = amount, + "Voting power delegated" + ); + + Ok(()) + } + + /// Get effective voting power (including delegations) + /// Uses saturating arithmetic to prevent overflow when accumulating power + pub fn get_voting_power(&self, voter: &[u8; 33], token_balance: u64) -> u64 { + let mut power = token_balance; + + // Add delegated power (using saturating add to prevent overflow) + for delegation in self.delegations.values() { + if delegation.delegatee == *voter { + power = power.saturating_add(delegation.amount); + } + } + + power + } +} + +impl Default for GovernanceManager { + fn default() -> Self { + Self::new(Vec::new()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_submit_proposal() { + let mut gov = GovernanceManager::new(vec![]); + let proposer = [1u8; 33]; + + let proposal_id = gov.submit_proposal( + proposer, + ProposalType::ParameterChange { + parameter: "block_time".to_string(), + new_value: vec![10], + }, + "Reduce block time to 10s".to_string(), + 1000, + 100, + ).unwrap(); + + assert_eq!(proposal_id.0, 1); + assert!(gov.proposals.contains_key(&proposal_id)); + } + + #[test] + fn test_voting() { + let mut gov = GovernanceManager::new(vec![]); + let proposer = [1u8; 33]; + let voter = [2u8; 33]; + + let proposal_id = gov.submit_proposal( + proposer, + ProposalType::ParameterChange { + parameter: "block_time".to_string(), + new_value: vec![10], + }, + "Reduce block time".to_string(), + 1000, + 100, + ).unwrap(); + + // Cast vote + gov.vote(proposal_id, voter, VoteType::For, 1000, 150, false).unwrap(); + + let proposal = gov.proposals.get(&proposal_id).unwrap(); + assert_eq!(proposal.votes_for, 1000); + } + + #[test] + fn test_double_vote_rejected() { + let mut gov = GovernanceManager::new(vec![]); + let proposer = [1u8; 33]; + let voter = [2u8; 33]; + + let proposal_id = gov.submit_proposal( + proposer, + ProposalType::ParameterChange { + parameter: "block_time".to_string(), + new_value: vec![10], + }, + "Test".to_string(), + 1000, + 100, + ).unwrap(); + + gov.vote(proposal_id, voter, VoteType::For, 1000, 150, false).unwrap(); + + // Try to vote again + let result = gov.vote(proposal_id, voter, VoteType::Against, 500, 200, false); + assert!(matches!(result, Err(Error::AlreadyVoted))); + } + + #[test] + fn test_finalize_proposal() { + let mut gov = GovernanceManager::new(vec![]); + let proposer = [1u8; 33]; + + let proposal_id = gov.submit_proposal( + proposer, + ProposalType::TreasurySpending { + recipient: [5u8; 33], + amount: 10000, + reason: "Development grant".to_string(), + }, + "Fund development".to_string(), + 1000, + 100, + ).unwrap(); + + // Cast votes (total must meet quorum of 10000) + gov.vote(proposal_id, [2u8; 33], VoteType::For, 7000, 150, false).unwrap(); + gov.vote(proposal_id, [3u8; 33], VoteType::Against, 3000, 160, false).unwrap(); + + // Finalize after voting period + gov.finalize_proposal(proposal_id, 1200).unwrap(); + + let proposal = gov.proposals.get(&proposal_id).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Passed); + } + + #[test] + fn test_emergency_cancel() { + let guardians = vec![[10u8; 33], [11u8; 33], [12u8; 33]]; + let mut gov = GovernanceManager::new(guardians.clone()); + + let proposal_id = gov.submit_proposal( + [1u8; 33], + ProposalType::ParameterChange { + parameter: "test".to_string(), + new_value: vec![1], + }, + "Test".to_string(), + 1000, + 100, + ).unwrap(); + + // Emergency cancel with 2 of 3 guardians + gov.emergency_cancel(proposal_id, vec![guardians[0], guardians[1]]).unwrap(); + + let proposal = gov.proposals.get(&proposal_id).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Cancelled); + } + + #[test] + fn test_delegation() { + let mut gov = GovernanceManager::new(vec![]); + let delegator = [1u8; 33]; + let delegatee = [2u8; 33]; + + gov.delegate(delegator, delegatee, 5000).unwrap(); + + let power = gov.get_voting_power(&delegatee, 1000); + assert_eq!(power, 6000); // 1000 own + 5000 delegated + } +} diff --git a/crates/bitcell-governance/src/proposal.rs b/crates/bitcell-governance/src/proposal.rs new file mode 100644 index 0000000..fe4156f --- /dev/null +++ b/crates/bitcell-governance/src/proposal.rs @@ -0,0 +1,201 @@ +//! Proposal types and management + +use serde::{Deserialize, Serialize}; + +/// Unique proposal identifier +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ProposalId(pub u64); + +/// Proposal types +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ProposalType { + /// Change a protocol parameter + ParameterChange { + parameter: String, + new_value: Vec, + }, + + /// Spend from treasury + TreasurySpending { + #[serde(with = "crate::serde_pubkey")] + recipient: [u8; 33], + amount: u64, + reason: String, + }, + + /// Protocol upgrade + ProtocolUpgrade { + version: String, + code_hash: [u8; 32], + description: String, + }, +} + +/// Proposal status +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ProposalStatus { + /// Proposal is active and accepting votes + Active, + + /// Voting period ended, proposal passed + Passed, + + /// Voting period ended, proposal rejected + Rejected, + + /// Proposal executed successfully + Executed, + + /// Proposal cancelled by guardians + Cancelled, +} + +/// On-chain governance proposal +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Proposal { + /// Unique identifier + pub id: ProposalId, + + /// Proposer's public key + #[serde(with = "crate::serde_pubkey")] + pub proposer: [u8; 33], + + /// Type of proposal + pub proposal_type: ProposalType, + + /// Human-readable description + pub description: String, + + /// Current status + pub status: ProposalStatus, + + /// Block number when proposal was created + pub creation_block: u64, + + /// Block number when voting ends + pub voting_end_block: u64, + + /// Votes in favor + pub votes_for: u64, + + /// Votes against + pub votes_against: u64, + + /// Abstain votes + pub votes_abstain: u64, + + /// Minimum votes required for validity (quorum) + pub quorum_threshold: u64, +} + +impl Proposal { + pub fn new( + id: ProposalId, + proposer: [u8; 33], + proposal_type: ProposalType, + description: String, + creation_block: u64, + voting_period_blocks: u64, + ) -> Self { + Self { + id, + proposer, + proposal_type, + description, + status: ProposalStatus::Active, + creation_block, + voting_end_block: creation_block + voting_period_blocks, + votes_for: 0, + votes_against: 0, + votes_abstain: 0, + quorum_threshold: 10000, // Default: 10000 CELL minimum participation + } + } + + /// Check if proposal is still in voting period + pub fn is_active(&self, current_block: u64) -> bool { + self.status == ProposalStatus::Active && current_block <= self.voting_end_block + } + + /// Get total votes cast + pub fn total_votes(&self) -> u64 { + self.votes_for + self.votes_against + self.votes_abstain + } + + /// Check if quorum is met + pub fn quorum_met(&self) -> bool { + self.total_votes() >= self.quorum_threshold + } + + /// Check if proposal has majority support + pub fn has_majority(&self) -> bool { + self.votes_for > self.votes_against + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_proposal_creation() { + let proposal = Proposal::new( + ProposalId(1), + [1u8; 33], + ProposalType::ParameterChange { + parameter: "block_time".to_string(), + new_value: vec![10], + }, + "Reduce block time".to_string(), + 100, + 1000, + ); + + assert_eq!(proposal.id.0, 1); + assert_eq!(proposal.voting_end_block, 1100); + assert_eq!(proposal.status, ProposalStatus::Active); + } + + #[test] + fn test_proposal_active() { + let proposal = Proposal::new( + ProposalId(1), + [1u8; 33], + ProposalType::ParameterChange { + parameter: "test".to_string(), + new_value: vec![1], + }, + "Test".to_string(), + 100, + 1000, + ); + + assert!(proposal.is_active(500)); + assert!(proposal.is_active(1100)); + assert!(!proposal.is_active(1101)); + } + + #[test] + fn test_quorum_and_majority() { + let mut proposal = Proposal::new( + ProposalId(1), + [1u8; 33], + ProposalType::TreasurySpending { + recipient: [2u8; 33], + amount: 5000, + reason: "Test".to_string(), + }, + "Test".to_string(), + 100, + 1000, + ); + + proposal.quorum_threshold = 10000; + proposal.votes_for = 6000; + proposal.votes_against = 4000; + + assert_eq!(proposal.total_votes(), 10000); + assert!(proposal.quorum_met()); + assert!(proposal.has_majority()); + } +} diff --git a/crates/bitcell-governance/src/serde_pubkey.rs b/crates/bitcell-governance/src/serde_pubkey.rs new file mode 100644 index 0000000..56144ca --- /dev/null +++ b/crates/bitcell-governance/src/serde_pubkey.rs @@ -0,0 +1,40 @@ +//! Custom serialization for public keys + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// Serialize a 33-byte public key +pub fn serialize(pubkey: &[u8; 33], serializer: S) -> Result +where + S: Serializer, +{ + if serializer.is_human_readable() { + hex::encode(pubkey).serialize(serializer) + } else { + serializer.serialize_bytes(pubkey) + } +} + +/// Deserialize a 33-byte public key +pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 33], D::Error> +where + D: Deserializer<'de>, +{ + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?; + if bytes.len() != 33 { + return Err(serde::de::Error::custom("invalid public key length")); + } + let mut array = [0u8; 33]; + array.copy_from_slice(&bytes); + Ok(array) + } else { + let bytes: Vec = Vec::deserialize(deserializer)?; + if bytes.len() != 33 { + return Err(serde::de::Error::custom("invalid public key length")); + } + let mut array = [0u8; 33]; + array.copy_from_slice(&bytes); + Ok(array) + } +} diff --git a/crates/bitcell-governance/src/voting.rs b/crates/bitcell-governance/src/voting.rs new file mode 100644 index 0000000..a2ccf3a --- /dev/null +++ b/crates/bitcell-governance/src/voting.rs @@ -0,0 +1,157 @@ +//! Voting mechanisms and delegation + +use serde::{Deserialize, Serialize}; + +/// Vote type +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VoteType { + /// Vote in favor + For, + + /// Vote against + Against, + + /// Abstain from voting (counted for quorum but not for/against) + Abstain, +} + +/// Voting power calculation +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct VotingPower { + /// Raw token amount + pub tokens: u64, + + /// Effective voting power (after quadratic calculation if applicable) + pub value: u64, +} + +impl VotingPower { + /// Linear voting: 1 CELL = 1 vote + pub fn linear(tokens: u64) -> Self { + Self { + tokens, + value: tokens, + } + } + + /// Quadratic voting: voting power = sqrt(tokens) + /// This helps prevent plutocracy by reducing the power of large token holders + pub fn quadratic(tokens: u64) -> Self { + let value = integer_sqrt(tokens); + Self { + tokens, + value, + } + } +} + +/// Integer square root using Newton's method +/// Uses checked arithmetic to prevent overflow +fn integer_sqrt(n: u64) -> u64 { + if n == 0 { + return 0; + } + if n == 1 { + return 1; + } + + let mut x = n; + let mut y = (x + 1) / 2; + + while y < x { + x = y; + // Use checked division to prevent overflow + if let Some(div) = n.checked_div(x) { + y = (x + div) / 2; + } else { + break; + } + } + + x +} + +/// A vote cast on a proposal +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Vote { + /// Voter's public key + #[serde(with = "crate::serde_pubkey")] + pub voter: [u8; 33], + + /// Type of vote + pub vote_type: VoteType, + + /// Voting power used + pub voting_power: VotingPower, + + /// Block number when vote was cast + pub block_number: u64, +} + +/// Delegation of voting power +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Delegation { + /// Delegator's public key + #[serde(with = "crate::serde_pubkey")] + pub delegator: [u8; 33], + + /// Delegatee's public key (who receives the voting power) + #[serde(with = "crate::serde_pubkey")] + pub delegatee: [u8; 33], + + /// Amount of voting power delegated + pub amount: u64, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_linear_voting_power() { + let power = VotingPower::linear(1000); + assert_eq!(power.tokens, 1000); + assert_eq!(power.value, 1000); + } + + #[test] + fn test_quadratic_voting_power() { + let power = VotingPower::quadratic(10000); + assert_eq!(power.tokens, 10000); + assert_eq!(power.value, 100); // sqrt(10000) = 100 + } + + #[test] + fn test_integer_sqrt() { + assert_eq!(integer_sqrt(0), 0); + assert_eq!(integer_sqrt(1), 1); + assert_eq!(integer_sqrt(4), 2); + assert_eq!(integer_sqrt(9), 3); + assert_eq!(integer_sqrt(16), 4); + assert_eq!(integer_sqrt(100), 10); + assert_eq!(integer_sqrt(10000), 100); + } + + #[test] + fn test_vote_creation() { + let vote = Vote { + voter: [1u8; 33], + vote_type: VoteType::For, + voting_power: VotingPower::linear(5000), + block_number: 100, + }; + + assert_eq!(vote.voting_power.value, 5000); + } + + #[test] + fn test_delegation() { + let delegation = Delegation { + delegator: [1u8; 33], + delegatee: [2u8; 33], + amount: 1000, + }; + + assert_eq!(delegation.amount, 1000); + } +} diff --git a/docs/GOVERNANCE.md b/docs/GOVERNANCE.md new file mode 100644 index 0000000..78db27e --- /dev/null +++ b/docs/GOVERNANCE.md @@ -0,0 +1,332 @@ +# On-Chain Governance System + +## Overview + +The BitCell On-Chain Governance System (RC3-005) provides a decentralized mechanism for protocol evolution and treasury management. It implements a three-phase governance process: proposal submission, token-weighted voting, and timelocked execution with guardian oversight. + +## Architecture + +### Core Components + +1. **Proposal System** (`proposal.rs`) + - Parameter changes for protocol configuration + - Treasury spending proposals + - Protocol upgrade proposals + +2. **Voting Mechanism** (`voting.rs`) + - Token-weighted voting (1 CELL = 1 vote by default) + - Optional quadratic voting (power = √tokens) + - Vote delegation support + +3. **Execution System** (`execution.rs`) + - Timelock delays for proposal execution + - Multi-sig guardian controls + - Emergency cancellation capabilities + +## Proposal Types + +### Parameter Change +```rust +ProposalType::ParameterChange { + parameter: String, + new_value: Vec, +} +``` +Used for modifying protocol parameters such as block time, gas limits, or economic parameters. + +### Treasury Spending +```rust +ProposalType::TreasurySpending { + recipient: [u8; 33], + amount: u64, + reason: String, +} +``` +Used for allocating funds from the protocol treasury to development, grants, or other initiatives. + +### Protocol Upgrade +```rust +ProposalType::ProtocolUpgrade { + version: String, + code_hash: [u8; 32], + description: String, +} +``` +Used for coordinating protocol upgrades and hard forks. + +## Voting Process + +### 1. Proposal Submission + +```rust +let proposal_id = governance_manager.submit_proposal( + proposer_pubkey, + proposal_type, + "Proposal description", + voting_period_blocks, // e.g., 14400 blocks (~2 days) + current_block, +)?; +``` + +### 2. Voting + +Voters can cast their votes during the voting period: + +```rust +governance_manager.vote( + proposal_id, + voter_pubkey, + VoteType::For, // or Against, or Abstain + token_balance, + current_block, + quadratic: false, // use quadratic voting if true +)?; +``` + +#### Voting Options + +- **For**: Support the proposal +- **Against**: Oppose the proposal +- **Abstain**: Count toward quorum without affecting the outcome + +#### Voting Power Calculation + +**Linear Voting (default)**: +``` +voting_power = token_balance +``` + +**Quadratic Voting**: +``` +voting_power = sqrt(token_balance) +``` + +Quadratic voting reduces the influence of large token holders, promoting more egalitarian decision-making. + +### 3. Vote Delegation + +Token holders can delegate their voting power to trusted representatives: + +```rust +governance_manager.delegate( + delegator_pubkey, + delegatee_pubkey, + amount, +)?; +``` + +The delegatee's effective voting power includes both their own tokens and delegated tokens. + +### 4. Proposal Finalization + +After the voting period ends, proposals must be finalized: + +```rust +governance_manager.finalize_proposal( + proposal_id, + current_block, +)?; +``` + +A proposal passes if: +1. **Quorum is met**: Total votes ≥ quorum threshold (default: 10,000 CELL) +2. **Majority support**: Votes for > Votes against + +### 5. Timelocked Execution + +Passed proposals enter a timelock period before execution: + +| Proposal Type | Timelock Period | +|---------------|-----------------| +| Parameter Change | 14,400 blocks (~2 days) | +| Treasury Spending | 1,800 blocks (~6 hours) | +| Protocol Upgrade | 14,400 blocks (~2 days) | + +```rust +governance_manager.execute_proposal( + proposal_id, + current_block, +)?; +``` + +## Guardian Controls + +Multi-sig guardians provide emergency oversight with the following capabilities: + +### Emergency Cancellation + +Guardians can cancel malicious or erroneous proposals: + +```rust +governance_manager.emergency_cancel( + proposal_id, + guardian_signatures, // Requires 2/3 majority +)?; +``` + +### Fast-Track Execution + +Guardians can reduce timelock delays for urgent proposals: + +```rust +governance_manager.execution_queue.fast_track( + proposal_id, + current_block, +)?; +``` + +## Security Features + +### Timelock Protection + +All passed proposals must wait through a timelock period, providing: +- Time for community review +- Opportunity for guardian intervention if needed +- Protection against hasty or malicious changes + +### Guardian Multi-Sig + +Guardian actions require a 2/3 supermajority: +```rust +required_signatures = (total_guardians * 2 + 2) / 3 +``` + +This prevents individual guardians from unilaterally controlling the system. + +### Double-Vote Prevention + +Each address can only vote once per proposal. Attempted double-voting returns an error: +```rust +Error::AlreadyVoted +``` + +## Usage Example + +### Complete Governance Flow + +```rust +use bitcell_governance::*; + +// Initialize governance with guardians +let guardians = vec![guardian1_pubkey, guardian2_pubkey, guardian3_pubkey]; +let mut governance = GovernanceManager::new(guardians); + +// Submit a proposal to adjust block reward +let proposal_id = governance.submit_proposal( + proposer_pubkey, + ProposalType::ParameterChange { + parameter: "block_reward".to_string(), + new_value: 40u64.to_le_bytes().to_vec(), // Reduce from 50 to 40 + }, + "Reduce block reward to extend emission schedule".to_string(), + 14400, // 2-day voting period + current_block, +)?; + +// Community members vote +governance.vote(proposal_id, voter1_pubkey, VoteType::For, 10000, current_block, false)?; +governance.vote(proposal_id, voter2_pubkey, VoteType::For, 8000, current_block, false)?; +governance.vote(proposal_id, voter3_pubkey, VoteType::Against, 3000, current_block, false)?; + +// After voting period, finalize the proposal +governance.finalize_proposal(proposal_id, current_block + 14500)?; + +// After timelock expires, execute the proposal +governance.execute_proposal(proposal_id, current_block + 14500 + 14400)?; +``` + +## Proposal Status States + +```rust +pub enum ProposalStatus { + Active, // Accepting votes + Passed, // Passed, waiting for execution + Rejected, // Failed to meet quorum or majority + Executed, // Successfully executed + Cancelled, // Cancelled by guardians +} +``` + +## Error Handling + +The governance system defines comprehensive error types: + +```rust +pub enum Error { + ProposalNotFound, // Invalid proposal ID + InvalidProposal, // Proposal in wrong state + VotingPeriodEnded, // Attempted to vote after deadline + VotingPeriodNotEnded, // Attempted to finalize too early + AlreadyVoted, // Double-vote attempt + InsufficientVotingPower, // Not enough tokens + ExecutionLocked, // Timelock not expired + NotAuthorized, // Insufficient guardian signatures + InvalidTimelock, // Invalid timelock configuration +} +``` + +## Best Practices + +### For Proposers + +1. **Clear Description**: Provide detailed rationale and expected impact +2. **Appropriate Timelock**: Consider using longer timelocks for major changes +3. **Community Engagement**: Discuss proposals before submission +4. **Parameter Validation**: Ensure proposed values are technically sound + +### For Voters + +1. **Research**: Review proposal details and community discussion +2. **Consider Delegation**: Delegate to experts if uncertain +3. **Vote Early**: Don't wait until the last minute +4. **Use Quadratic Voting**: Consider using quadratic voting for fairer representation + +### For Guardians + +1. **Minimal Intervention**: Only use emergency powers for actual emergencies +2. **Transparency**: Document reasons for guardian actions +3. **Community Alignment**: Ensure actions align with community values +4. **Coordination**: Require multiple guardians to agree before acting + +## Integration with BitCell + +The governance system integrates with other BitCell components: + +- **State Management**: Proposals can modify state parameters +- **Economics**: Treasury spending affects token distribution +- **Consensus**: Protocol upgrades coordinate network changes + +## Future Enhancements + +Potential future improvements include: + +1. **On-chain execution**: Automatic parameter changes upon execution +2. **Proposal templates**: Standardized formats for common proposal types +3. **Reputation weighting**: Factor trust scores into voting power +4. **Time-weighted voting**: Give more weight to long-term holders +5. **Conviction voting**: Lock tokens to increase voting power +6. **Liquid democracy**: Transitive delegation chains + +## Testing + +The governance system includes comprehensive tests covering: + +- Proposal submission and lifecycle +- Voting mechanics (linear and quadratic) +- Delegation functionality +- Timelock enforcement +- Guardian controls +- Error conditions + +Run tests with: +```bash +cargo test -p bitcell-governance +``` + +All 20 tests pass successfully, ensuring robust functionality. + +## References + +- RC3-005: Governance System Requirements +- `docs/RELEASE_REQUIREMENTS.md`: Detailed RC3-005 specification +- Academic references on quadratic voting and liquid democracy diff --git a/docs/issue-63.md b/docs/issue-63.md deleted file mode 100644 index dd9cac9..0000000 --- a/docs/issue-63.md +++ /dev/null @@ -1,5 +0,0 @@ -# Issue 63 - -Work in progress by Emulated Coder. - -Ref: #63 \ No newline at end of file