Skip to content

Commit 9b53d13

Browse files
CopilotSteake
andcommitted
Implement on-chain governance system for RC3-005
- Create bitcell-governance crate with full implementation - Implement proposal system with parameter changes, treasury spending, and protocol upgrades - Implement voting mechanism with token-weighted voting (linear and quadratic) - Add delegation support for voting power - Implement execution queue with timelock delays - Add multi-sig guardian controls for emergency cancellation and fast-tracking - Include comprehensive tests (20 tests, all passing) - Remove placeholder documentation file Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
1 parent 78e3552 commit 9b53d13

8 files changed

Lines changed: 1223 additions & 5 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ members = [
1616
"crates/bitcell-wallet-gui",
1717
"crates/bitcell-compiler",
1818
"crates/bitcell-light-client",
19+
"crates/bitcell-governance",
1920
]
2021
resolver = "2"
2122

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "bitcell-governance"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
rust-version.workspace = true
7+
license.workspace = true
8+
repository.workspace = true
9+
10+
[dependencies]
11+
bitcell-crypto = { path = "../bitcell-crypto" }
12+
serde.workspace = true
13+
thiserror.workspace = true
14+
bincode.workspace = true
15+
tracing.workspace = true
16+
hex.workspace = true
17+
18+
[dev-dependencies]
19+
proptest.workspace = true
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
//! Proposal execution system with timelock and guardian controls
2+
3+
use crate::{Error, Result, ProposalId, ProposalType};
4+
use serde::{Deserialize, Serialize};
5+
use std::collections::HashMap;
6+
7+
/// Timelock delay in blocks
8+
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
9+
pub struct TimelockDelay {
10+
/// Number of blocks to wait before execution
11+
pub blocks: u64,
12+
}
13+
14+
impl TimelockDelay {
15+
/// Standard timelock delay (e.g., 2 days assuming 12s blocks)
16+
pub fn standard() -> Self {
17+
Self { blocks: 14400 } // ~2 days
18+
}
19+
20+
/// Fast track delay (e.g., 6 hours)
21+
pub fn fast_track() -> Self {
22+
Self { blocks: 1800 } // ~6 hours
23+
}
24+
25+
/// Emergency delay (e.g., 1 hour)
26+
pub fn emergency() -> Self {
27+
Self { blocks: 300 } // ~1 hour
28+
}
29+
}
30+
31+
impl Default for TimelockDelay {
32+
fn default() -> Self {
33+
Self::standard()
34+
}
35+
}
36+
37+
/// Guardian action types
38+
#[derive(Debug, Clone, Serialize, Deserialize)]
39+
pub enum GuardianAction {
40+
/// Cancel a proposal
41+
Cancel(ProposalId),
42+
43+
/// Fast-track a proposal (reduce timelock)
44+
FastTrack(ProposalId),
45+
46+
/// Veto a proposal execution
47+
Veto(ProposalId),
48+
}
49+
50+
/// Queued proposal for execution
51+
#[derive(Debug, Clone, Serialize, Deserialize)]
52+
pub struct QueuedProposal {
53+
/// Proposal ID
54+
pub proposal_id: ProposalId,
55+
56+
/// Proposal type
57+
pub proposal_type: ProposalType,
58+
59+
/// Block when it was queued
60+
pub queued_block: u64,
61+
62+
/// Timelock delay
63+
pub timelock: TimelockDelay,
64+
65+
/// Block when it can be executed
66+
pub execution_block: u64,
67+
}
68+
69+
impl QueuedProposal {
70+
pub fn new(
71+
proposal_id: ProposalId,
72+
proposal_type: ProposalType,
73+
queued_block: u64,
74+
timelock: TimelockDelay,
75+
) -> Self {
76+
Self {
77+
proposal_id,
78+
proposal_type,
79+
queued_block,
80+
timelock,
81+
execution_block: queued_block + timelock.blocks,
82+
}
83+
}
84+
85+
/// Check if proposal is ready for execution
86+
pub fn is_executable(&self, current_block: u64) -> bool {
87+
current_block >= self.execution_block
88+
}
89+
}
90+
91+
/// Execution queue managing timelocked proposals
92+
#[derive(Debug, Clone, Serialize, Deserialize)]
93+
pub struct ExecutionQueue {
94+
/// Queued proposals awaiting execution
95+
queue: HashMap<ProposalId, QueuedProposal>,
96+
}
97+
98+
impl ExecutionQueue {
99+
pub fn new() -> Self {
100+
Self {
101+
queue: HashMap::new(),
102+
}
103+
}
104+
105+
/// Enqueue a proposal for execution after timelock
106+
pub fn enqueue(
107+
&mut self,
108+
proposal_id: ProposalId,
109+
current_block: u64,
110+
proposal_type: ProposalType,
111+
) {
112+
let timelock = match &proposal_type {
113+
ProposalType::ParameterChange { .. } => TimelockDelay::standard(),
114+
ProposalType::TreasurySpending { .. } => TimelockDelay::fast_track(),
115+
ProposalType::ProtocolUpgrade { .. } => TimelockDelay::standard(),
116+
};
117+
118+
let queued = QueuedProposal::new(
119+
proposal_id,
120+
proposal_type,
121+
current_block,
122+
timelock,
123+
);
124+
125+
let execution_block = queued.execution_block;
126+
self.queue.insert(proposal_id, queued);
127+
128+
tracing::info!(
129+
proposal_id = proposal_id.0,
130+
execution_block = execution_block,
131+
"Proposal queued for execution after timelock"
132+
);
133+
}
134+
135+
/// Execute a proposal (must be past timelock)
136+
pub fn execute(
137+
&mut self,
138+
proposal_id: ProposalId,
139+
current_block: u64,
140+
) -> Result<()> {
141+
let queued = self.queue.get(&proposal_id)
142+
.ok_or(Error::ProposalNotFound)?;
143+
144+
if !queued.is_executable(current_block) {
145+
return Err(Error::ExecutionLocked);
146+
}
147+
148+
// Remove from queue
149+
self.queue.remove(&proposal_id);
150+
151+
tracing::info!(
152+
proposal_id = proposal_id.0,
153+
"Proposal executed and removed from queue"
154+
);
155+
156+
Ok(())
157+
}
158+
159+
/// Cancel a proposal (guardian action)
160+
pub fn cancel(&mut self, proposal_id: ProposalId) -> Result<()> {
161+
self.queue.remove(&proposal_id)
162+
.ok_or(Error::ProposalNotFound)?;
163+
164+
tracing::warn!(
165+
proposal_id = proposal_id.0,
166+
"Proposal cancelled and removed from execution queue"
167+
);
168+
169+
Ok(())
170+
}
171+
172+
/// Fast-track a proposal (guardian action)
173+
pub fn fast_track(
174+
&mut self,
175+
proposal_id: ProposalId,
176+
current_block: u64,
177+
) -> Result<()> {
178+
let queued = self.queue.get_mut(&proposal_id)
179+
.ok_or(Error::ProposalNotFound)?;
180+
181+
queued.timelock = TimelockDelay::fast_track();
182+
queued.execution_block = current_block + queued.timelock.blocks;
183+
184+
tracing::info!(
185+
proposal_id = proposal_id.0,
186+
new_execution_block = queued.execution_block,
187+
"Proposal fast-tracked"
188+
);
189+
190+
Ok(())
191+
}
192+
193+
/// Get all executable proposals
194+
pub fn get_executable(&self, current_block: u64) -> Vec<ProposalId> {
195+
self.queue.values()
196+
.filter(|p| p.is_executable(current_block))
197+
.map(|p| p.proposal_id)
198+
.collect()
199+
}
200+
201+
/// Get proposal from queue
202+
pub fn get(&self, proposal_id: ProposalId) -> Option<&QueuedProposal> {
203+
self.queue.get(&proposal_id)
204+
}
205+
}
206+
207+
impl Default for ExecutionQueue {
208+
fn default() -> Self {
209+
Self::new()
210+
}
211+
}
212+
213+
#[cfg(test)]
214+
mod tests {
215+
use super::*;
216+
217+
#[test]
218+
fn test_timelock_delays() {
219+
let standard = TimelockDelay::standard();
220+
assert_eq!(standard.blocks, 14400);
221+
222+
let fast = TimelockDelay::fast_track();
223+
assert_eq!(fast.blocks, 1800);
224+
225+
let emergency = TimelockDelay::emergency();
226+
assert_eq!(emergency.blocks, 300);
227+
}
228+
229+
#[test]
230+
fn test_queued_proposal() {
231+
let proposal = QueuedProposal::new(
232+
ProposalId(1),
233+
ProposalType::ParameterChange {
234+
parameter: "test".to_string(),
235+
new_value: vec![1],
236+
},
237+
100,
238+
TimelockDelay::fast_track(),
239+
);
240+
241+
assert_eq!(proposal.execution_block, 1900); // 100 + 1800
242+
assert!(!proposal.is_executable(1000));
243+
assert!(proposal.is_executable(1900));
244+
assert!(proposal.is_executable(2000));
245+
}
246+
247+
#[test]
248+
fn test_execution_queue() {
249+
let mut queue = ExecutionQueue::new();
250+
251+
queue.enqueue(
252+
ProposalId(1),
253+
100,
254+
ProposalType::TreasurySpending {
255+
recipient: [1u8; 33],
256+
amount: 1000,
257+
reason: "Test".to_string(),
258+
},
259+
);
260+
261+
let queued = queue.get(ProposalId(1)).unwrap();
262+
assert_eq!(queued.execution_block, 1900); // Fast track for treasury
263+
264+
// Cannot execute before timelock
265+
let result = queue.execute(ProposalId(1), 1000);
266+
assert!(matches!(result, Err(Error::ExecutionLocked)));
267+
268+
// Can execute after timelock
269+
queue.execute(ProposalId(1), 2000).unwrap();
270+
assert!(queue.get(ProposalId(1)).is_none());
271+
}
272+
273+
#[test]
274+
fn test_cancel() {
275+
let mut queue = ExecutionQueue::new();
276+
277+
queue.enqueue(
278+
ProposalId(1),
279+
100,
280+
ProposalType::ParameterChange {
281+
parameter: "test".to_string(),
282+
new_value: vec![1],
283+
},
284+
);
285+
286+
queue.cancel(ProposalId(1)).unwrap();
287+
assert!(queue.get(ProposalId(1)).is_none());
288+
}
289+
290+
#[test]
291+
fn test_fast_track() {
292+
let mut queue = ExecutionQueue::new();
293+
294+
queue.enqueue(
295+
ProposalId(1),
296+
100,
297+
ProposalType::ParameterChange {
298+
parameter: "test".to_string(),
299+
new_value: vec![1],
300+
},
301+
);
302+
303+
// Original execution block
304+
let original = queue.get(ProposalId(1)).unwrap().execution_block;
305+
assert_eq!(original, 14500); // 100 + 14400 (standard)
306+
307+
// Fast track
308+
queue.fast_track(ProposalId(1), 200).unwrap();
309+
310+
let new_exec_block = queue.get(ProposalId(1)).unwrap().execution_block;
311+
assert_eq!(new_exec_block, 2000); // 200 + 1800 (fast track)
312+
}
313+
314+
#[test]
315+
fn test_get_executable() {
316+
let mut queue = ExecutionQueue::new();
317+
318+
queue.enqueue(
319+
ProposalId(1),
320+
100,
321+
ProposalType::TreasurySpending {
322+
recipient: [1u8; 33],
323+
amount: 1000,
324+
reason: "Test".to_string(),
325+
},
326+
);
327+
328+
queue.enqueue(
329+
ProposalId(2),
330+
100,
331+
ProposalType::ParameterChange {
332+
parameter: "test".to_string(),
333+
new_value: vec![1],
334+
},
335+
);
336+
337+
// At block 2000, only proposal 1 is executable (fast track)
338+
let executable = queue.get_executable(2000);
339+
assert_eq!(executable.len(), 1);
340+
assert_eq!(executable[0].0, 1);
341+
342+
// At block 15000, both are executable
343+
let executable = queue.get_executable(15000);
344+
assert_eq!(executable.len(), 2);
345+
}
346+
}

0 commit comments

Comments
 (0)