|
1 | | -//! Deterministic Reputation — on-chain slashing/jail PRIMITIVES + parameters. |
| 1 | +//! Consensus reputation parameters. |
2 | 2 | //! |
3 | | -//! HISTORY: this module previously also hosted `DeterministicReputationState`, a |
4 | | -//! per-node RAM telemetry engine (rotation rewards, passive recovery, snapshot |
5 | | -//! sync). That engine was display/telemetry-only and has been REMOVED — it never |
6 | | -//! gated consensus. Consensus eligibility is decided solely by the deterministic |
7 | | -//! chain fold (`compute_consensus_reputation_map` + the macroblock |
8 | | -//! `eligible_producers` snapshot + uniform-VRF sortition) in qnet-integration. |
9 | | -//! |
10 | | -//! WHAT REMAINS (still used on-chain): |
11 | | -//! - All `pub const` reputation parameters (thresholds, rewards, penalties, |
12 | | -//! jail durations, passive-recovery bounds, scalability limits). |
13 | | -//! - `SlashingType`, `SlashingEvent`, `AutomaticJail` — these are part of the |
14 | | -//! on-chain `MacroBlockConsensusData` type in `macro_consensus.rs`. |
15 | | -
|
16 | | -use serde::{Deserialize, Serialize}; |
17 | | -use sha3::{Sha3_256, Digest}; |
18 | | - |
19 | | -// ============================================================================ |
20 | | -// CONSTANTS - Blockchain-standard approach |
21 | | -// ============================================================================ |
22 | | - |
23 | | -/// Starting reputation for all nodes (consensus threshold) |
| 3 | +//! Consensus reputation is binary {INITIAL_REPUTATION | 0}: every node starts at the |
| 4 | +//! floor and is dropped to 0 only by a cryptographically-proven equivocation (the |
| 5 | +//! ban-set is anchored per-macroblock and re-verified each epoch). Eligibility is the |
| 6 | +//! deterministic chain fold (`compute_consensus_reputation_map` + the `eligible_producers` |
| 7 | +//! snapshot + uniform-VRF sortition) in qnet-integration. No graduated score, jail, or |
| 8 | +//! decay gates consensus: a per-node mutable score is timing-dependent across nodes and |
| 9 | +//! therefore a fork vector. Off-consensus telemetry may still display a richer score, but |
| 10 | +//! it must never feed eligibility or any QC-bound field. |
| 11 | +
|
| 12 | +/// Reputation floor: every node starts here; the `>=` gate admits it to consensus. |
24 | 13 | pub const INITIAL_REPUTATION: f64 = 70.0; |
25 | 14 |
|
26 | | -/// Maximum reputation cap |
27 | | -pub const MAX_REPUTATION: f64 = 100.0; |
28 | | - |
29 | | -/// Minimum reputation (below = excluded from consensus) |
| 15 | +/// Minimum reputation to participate in consensus (below = excluded). |
30 | 16 | pub const MIN_CONSENSUS_REPUTATION: f64 = 70.0; |
31 | | - |
32 | | -/// Reputation for automatic jail trigger |
33 | | -pub const JAIL_THRESHOLD: f64 = 10.0; |
34 | | - |
35 | | -/// Blocks per rotation (30 blocks = one producer cycle) |
36 | | -pub const BLOCKS_PER_ROTATION: u64 = 30; |
37 | | - |
38 | | -/// Blocks per macroblock (consensus checkpoint) |
39 | | -pub const BLOCKS_PER_MACROBLOCK: u64 = 90; |
40 | | - |
41 | | -/// Consecutive missed blocks for automatic jail |
42 | | -pub const AUTO_JAIL_MISSED_BLOCKS: u64 = 5; |
43 | | - |
44 | | -// ============================================================================ |
45 | | -// SCALABILITY CONSTANTS - For 10,000+ node networks |
46 | | -// ============================================================================ |
47 | | - |
48 | | -/// Chunk size for batch processing (prevents blocking) |
49 | | -pub const PROCESSING_CHUNK_SIZE: usize = 1000; |
50 | | - |
51 | | -/// Max slashing events per macroblock (prevents DoS) |
52 | | -pub const MAX_SLASHING_EVENTS_PER_MACROBLOCK: usize = 100; |
53 | | - |
54 | | -/// Max automatic jails per macroblock |
55 | | -pub const MAX_AUTO_JAILS_PER_MACROBLOCK: usize = 50; |
56 | | - |
57 | | -// ============================================================================ |
58 | | -// REWARD/PENALTY CONSTANTS (documented and verifiable) |
59 | | -// ============================================================================ |
60 | | - |
61 | | -/// Reward for completing full 30-block rotation as producer |
62 | | -pub const REWARD_FULL_ROTATION: f64 = 2.0; |
63 | | - |
64 | | -/// Reward for participating in macroblock consensus (commit + reveal) |
65 | | -pub const REWARD_CONSENSUS_PARTICIPATION: f64 = 1.0; |
66 | | - |
67 | | -/// Penalty for producing invalid block (Byzantine attack) |
68 | | -pub const PENALTY_INVALID_BLOCK: f64 = 20.0; |
69 | | - |
70 | | -/// Penalty for double signing (signing two blocks at same height) |
71 | | -pub const PENALTY_DOUBLE_SIGN: f64 = 50.0; |
72 | | - |
73 | | -/// Penalty for missing assigned block production |
74 | | -pub const PENALTY_MISSED_BLOCK: f64 = 2.0; |
75 | | - |
76 | | -/// Penalty for missing consensus participation |
77 | | -pub const PENALTY_MISSED_CONSENSUS: f64 = 1.0; |
78 | | - |
79 | | -// ============================================================================ |
80 | | -// JAIL DURATIONS (progressive, same for ALL nodes including Genesis) |
81 | | -// ============================================================================ |
82 | | - |
83 | | -/// Jail duration for first offense (1 hour) |
84 | | -pub const JAIL_DURATION_1: u64 = 3600; |
85 | | - |
86 | | -/// Jail duration for second offense (24 hours) |
87 | | -pub const JAIL_DURATION_2: u64 = 86400; |
88 | | - |
89 | | -/// Jail duration for third offense (7 days) |
90 | | -pub const JAIL_DURATION_3: u64 = 604800; |
91 | | - |
92 | | -/// Jail duration for fourth offense (30 days) |
93 | | -pub const JAIL_DURATION_4: u64 = 2592000; |
94 | | - |
95 | | -/// Jail duration for fifth offense (90 days) |
96 | | -pub const JAIL_DURATION_5: u64 = 7776000; |
97 | | - |
98 | | -/// Jail duration for 6+ offenses (1 year) |
99 | | -pub const JAIL_DURATION_MAX: u64 = 31536000; |
100 | | - |
101 | | -/// Permanent ban marker |
102 | | -pub const PERMANENT_BAN: u64 = u64::MAX; |
103 | | - |
104 | | -// ============================================================================ |
105 | | -// PASSIVE RECOVERY - For nodes with reputation 10-69% |
106 | | -// ============================================================================ |
107 | | - |
108 | | -/// Passive recovery interval (every 4 hours = 14400 seconds) |
109 | | -pub const PASSIVE_RECOVERY_INTERVAL: u64 = 14400; |
110 | | - |
111 | | -/// Passive recovery amount per interval (+1%) |
112 | | -pub const PASSIVE_RECOVERY_AMOUNT: f64 = 1.0; |
113 | | - |
114 | | -/// Minimum reputation for passive recovery (below = cannot recover) |
115 | | -pub const PASSIVE_RECOVERY_MIN: f64 = 10.0; |
116 | | - |
117 | | -/// Maximum reputation for passive recovery (above = no passive recovery needed) |
118 | | -pub const PASSIVE_RECOVERY_MAX: f64 = 70.0; |
119 | | - |
120 | | -// ============================================================================ |
121 | | -// SLASHING EVENT - Recorded in MacroBlock with cryptographic proof |
122 | | -// ============================================================================ |
123 | | - |
124 | | -/// Types of slashable offenses (must have cryptographic proof) |
125 | | -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] |
126 | | -pub enum SlashingType { |
127 | | - /// Signed two different blocks at same height |
128 | | - /// Proof: Both signed blocks with same height but different hashes |
129 | | - DoubleSign { |
130 | | - height: u64, |
131 | | - hash_a: [u8; 32], |
132 | | - hash_b: [u8; 32], |
133 | | - signature_a: Vec<u8>, |
134 | | - signature_b: Vec<u8>, |
135 | | - }, |
136 | | - |
137 | | - /// Produced cryptographically invalid block |
138 | | - /// Proof: The invalid block itself (fails verification) |
139 | | - InvalidBlock { |
140 | | - height: u64, |
141 | | - block_hash: [u8; 32], |
142 | | - reason: String, |
143 | | - }, |
144 | | - |
145 | | - /// Attempted chain fork (critical - permanent ban) |
146 | | - /// Proof: Conflicting blocks signed by same node |
147 | | - ChainFork { |
148 | | - fork_height: u64, |
149 | | - main_chain_hash: [u8; 32], |
150 | | - fork_chain_hash: [u8; 32], |
151 | | - }, |
152 | | - |
153 | | - /// Missed N consecutive assigned blocks |
154 | | - /// Proof: Block heights where node was assigned but didn't produce |
155 | | - ConsecutiveMissedBlocks { |
156 | | - missed_heights: Vec<u64>, |
157 | | - }, |
158 | | -} |
159 | | - |
160 | | -/// Slashing event recorded in macroblock |
161 | | -#[derive(Debug, Clone, Serialize, Deserialize)] |
162 | | -pub struct SlashingEvent { |
163 | | - /// Node being slashed |
164 | | - pub offender: String, |
165 | | - |
166 | | - /// Type of offense with proof |
167 | | - pub offense: SlashingType, |
168 | | - |
169 | | - /// Penalty amount (reputation points) |
170 | | - pub penalty: f64, |
171 | | - |
172 | | - /// Block height when detected |
173 | | - pub detected_at_height: u64, |
174 | | - |
175 | | - /// Reporter node (gets small reward for reporting) |
176 | | - pub reporter: String, |
177 | | - |
178 | | - /// SHA3 hash of evidence for verification |
179 | | - pub evidence_hash: [u8; 32], |
180 | | -} |
181 | | - |
182 | | -impl SlashingEvent { |
183 | | - /// Calculate penalty based on offense type |
184 | | - pub fn calculate_penalty(offense: &SlashingType) -> f64 { |
185 | | - match offense { |
186 | | - SlashingType::DoubleSign { .. } => PENALTY_DOUBLE_SIGN, |
187 | | - SlashingType::InvalidBlock { .. } => PENALTY_INVALID_BLOCK, |
188 | | - SlashingType::ChainFork { .. } => 100.0, // Full reputation loss |
189 | | - SlashingType::ConsecutiveMissedBlocks { missed_heights } => { |
190 | | - PENALTY_MISSED_BLOCK * missed_heights.len() as f64 |
191 | | - } |
192 | | - } |
193 | | - } |
194 | | - |
195 | | - /// v3.33: ALL slashing offenses = permanent ban, no recovery. |
196 | | - /// QNet has no staking — the only deterrent is irrevocable network exclusion. |
197 | | - /// Cryptographic proof is required for all offenses (no false positives). |
198 | | - pub fn is_permanent_ban(offense: &SlashingType) -> bool { |
199 | | - match offense { |
200 | | - SlashingType::DoubleSign { .. } => true, |
201 | | - SlashingType::InvalidBlock { .. } => true, |
202 | | - SlashingType::ChainFork { .. } => true, |
203 | | - SlashingType::ConsecutiveMissedBlocks { .. } => false, |
204 | | - } |
205 | | - } |
206 | | - |
207 | | - /// Compute evidence hash for verification |
208 | | - pub fn compute_evidence_hash(offense: &SlashingType) -> [u8; 32] { |
209 | | - let mut hasher = Sha3_256::new(); |
210 | | - |
211 | | - match offense { |
212 | | - SlashingType::DoubleSign { height, hash_a, hash_b, signature_a, signature_b } => { |
213 | | - hasher.update(b"DOUBLE_SIGN:"); |
214 | | - hasher.update(&height.to_le_bytes()); |
215 | | - hasher.update(hash_a); |
216 | | - hasher.update(hash_b); |
217 | | - hasher.update(signature_a); |
218 | | - hasher.update(signature_b); |
219 | | - } |
220 | | - SlashingType::InvalidBlock { height, block_hash, reason } => { |
221 | | - hasher.update(b"INVALID_BLOCK:"); |
222 | | - hasher.update(&height.to_le_bytes()); |
223 | | - hasher.update(block_hash); |
224 | | - hasher.update(reason.as_bytes()); |
225 | | - } |
226 | | - SlashingType::ChainFork { fork_height, main_chain_hash, fork_chain_hash } => { |
227 | | - hasher.update(b"CHAIN_FORK:"); |
228 | | - hasher.update(&fork_height.to_le_bytes()); |
229 | | - hasher.update(main_chain_hash); |
230 | | - hasher.update(fork_chain_hash); |
231 | | - } |
232 | | - SlashingType::ConsecutiveMissedBlocks { missed_heights } => { |
233 | | - hasher.update(b"MISSED_BLOCKS:"); |
234 | | - for h in missed_heights { |
235 | | - hasher.update(&h.to_le_bytes()); |
236 | | - } |
237 | | - } |
238 | | - } |
239 | | - |
240 | | - let result = hasher.finalize(); |
241 | | - let mut hash = [0u8; 32]; |
242 | | - hash.copy_from_slice(&result); |
243 | | - hash |
244 | | - } |
245 | | - |
246 | | - /// Verify evidence is valid and matches hash |
247 | | - pub fn verify_evidence(&self) -> bool { |
248 | | - let computed_hash = Self::compute_evidence_hash(&self.offense); |
249 | | - computed_hash == self.evidence_hash |
250 | | - } |
251 | | -} |
252 | | - |
253 | | -// ============================================================================ |
254 | | -// AUTOMATIC JAIL - Recorded in MacroBlock (deterministic trigger) |
255 | | -// ============================================================================ |
256 | | - |
257 | | -/// Automatic jail event (computed deterministically from blocks) |
258 | | -#[derive(Debug, Clone, Serialize, Deserialize)] |
259 | | -pub struct AutomaticJail { |
260 | | - /// Node being jailed |
261 | | - pub node_id: String, |
262 | | - |
263 | | - /// Offense count (progressive jail duration) |
264 | | - pub offense_count: u32, |
265 | | - |
266 | | - /// Block height when jail starts |
267 | | - pub jail_start_height: u64, |
268 | | - |
269 | | - /// Duration in seconds |
270 | | - pub jail_duration: u64, |
271 | | - |
272 | | - /// Reason |
273 | | - pub reason: String, |
274 | | - |
275 | | - /// Hash of evidence (missed block heights, etc.) |
276 | | - pub evidence_hash: [u8; 32], |
277 | | -} |
278 | | - |
279 | | -impl AutomaticJail { |
280 | | - /// Calculate jail duration based on offense count |
281 | | - pub fn calculate_duration(offense_count: u32) -> u64 { |
282 | | - match offense_count { |
283 | | - 1 => JAIL_DURATION_1, |
284 | | - 2 => JAIL_DURATION_2, |
285 | | - 3 => JAIL_DURATION_3, |
286 | | - 4 => JAIL_DURATION_4, |
287 | | - 5 => JAIL_DURATION_5, |
288 | | - _ => JAIL_DURATION_MAX, |
289 | | - } |
290 | | - } |
291 | | - |
292 | | - /// Check if jail has expired at given timestamp |
293 | | - pub fn is_expired(&self, current_timestamp: u64, block_start_timestamp: u64) -> bool { |
294 | | - if self.jail_duration == PERMANENT_BAN { |
295 | | - return false; // Never expires |
296 | | - } |
297 | | - |
298 | | - let jail_end = block_start_timestamp.saturating_add(self.jail_duration); |
299 | | - current_timestamp >= jail_end |
300 | | - } |
301 | | -} |
302 | | - |
303 | | -// ============================================================================ |
304 | | -// REMOVED: DeterministicReputationState (display/telemetry engine). |
305 | | -// |
306 | | -// The engine struct + impl, its `Default`, and the private feed/snapshot types |
307 | | -// (BlockData, MacroBlockData, MacroBlockConsensus, ReputationStats, |
308 | | -// FullReputationSnapshot, DeltaReputationSnapshot, ReputationSnapshotPayload) |
309 | | -// were RAM-only telemetry and have been deleted — they never gated consensus. |
310 | | -// The constants and slashing/jail primitives above are KEPT (still used on-chain |
311 | | -// and re-exported from lib.rs). |
312 | | -// ============================================================================ |
313 | | - |
314 | | -// ============================================================================ |
315 | | -// TESTS |
316 | | -// ============================================================================ |
317 | | - |
318 | | -#[cfg(test)] |
319 | | -mod tests { |
320 | | - use super::*; |
321 | | - |
322 | | - #[test] |
323 | | - fn test_slashing_verification() { |
324 | | - let offense = SlashingType::DoubleSign { |
325 | | - height: 100, |
326 | | - hash_a: [1u8; 32], |
327 | | - hash_b: [2u8; 32], |
328 | | - signature_a: vec![1, 2, 3], |
329 | | - signature_b: vec![4, 5, 6], |
330 | | - }; |
331 | | - |
332 | | - let event = SlashingEvent { |
333 | | - offender: "bad_node".to_string(), |
334 | | - offense: offense.clone(), |
335 | | - penalty: PENALTY_DOUBLE_SIGN, |
336 | | - detected_at_height: 101, |
337 | | - reporter: "good_node".to_string(), |
338 | | - evidence_hash: SlashingEvent::compute_evidence_hash(&offense), |
339 | | - }; |
340 | | - |
341 | | - assert!(event.verify_evidence()); |
342 | | - } |
343 | | - |
344 | | - #[test] |
345 | | - fn test_slashing_penalty_and_ban_classification() { |
346 | | - // Penalty + permanent-ban classification are pure functions of the |
347 | | - // offense type and remain the on-chain source of truth after the |
348 | | - // telemetry engine removal. |
349 | | - let fork = SlashingType::ChainFork { |
350 | | - fork_height: 100, |
351 | | - main_chain_hash: [1u8; 32], |
352 | | - fork_chain_hash: [2u8; 32], |
353 | | - }; |
354 | | - assert_eq!(SlashingEvent::calculate_penalty(&fork), 100.0); |
355 | | - assert!(SlashingEvent::is_permanent_ban(&fork)); |
356 | | - |
357 | | - let missed = SlashingType::ConsecutiveMissedBlocks { missed_heights: vec![1, 2, 3] }; |
358 | | - assert_eq!(SlashingEvent::calculate_penalty(&missed), PENALTY_MISSED_BLOCK * 3.0); |
359 | | - assert!(!SlashingEvent::is_permanent_ban(&missed)); |
360 | | - } |
361 | | - |
362 | | - #[test] |
363 | | - fn test_automatic_jail_duration_progression() { |
364 | | - assert_eq!(AutomaticJail::calculate_duration(1), JAIL_DURATION_1); |
365 | | - assert_eq!(AutomaticJail::calculate_duration(5), JAIL_DURATION_5); |
366 | | - assert_eq!(AutomaticJail::calculate_duration(99), JAIL_DURATION_MAX); |
367 | | - } |
368 | | -} |
0 commit comments