Skip to content

Commit 45514b9

Browse files
AIQnetLabclaude
andcommitted
consensus: remove dead graduated-reputation surface (binary {70|0} model)
Consensus reputation gates only admission (start 70) and equivocation slashing (->0, cryptographic proof). The graduated reward/penalty/jail engine was already dead: uniform-VRF does not rank by the value, and a mutable per-node score is a fork vector. Remove the dormant surface so it cannot be silently re-wired. - deterministic_reputation.rs: keep INITIAL_REPUTATION + MIN_CONSENSUS_REPUTATION; drop REWARD_*/PENALTY_*/JAIL_*/PASSIVE_RECOVERY_* + SlashingType/SlashingEvent/AutomaticJail - macro_consensus.rs: drop MacroBlockConsensusData + MissedBlockTracker (finality kept) - block.rs: drop dead ConsensusData fields slashing_events_data/automatic_jails_data/ reputation_snapshot + their data structs; banned_validators kept - node.rs: drop the empty compute_automatic_jails stub - rpc.rs: reputation API reads the live banned_validators anchor -> {0 | 70} - docs: rewrite REPUTATION_SYSTEM.md to the binary model ConsensusData wire format changes -> fresh genesis required. cargo check clean, 335 lib tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3210473 commit 45514b9

10 files changed

Lines changed: 154 additions & 2636 deletions

File tree

Lines changed: 12 additions & 364 deletions
Original file line numberDiff line numberDiff line change
@@ -1,368 +1,16 @@
1-
//! Deterministic Reputation — on-chain slashing/jail PRIMITIVES + parameters.
1+
//! Consensus reputation parameters.
22
//!
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.
2413
pub const INITIAL_REPUTATION: f64 = 70.0;
2514

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).
3016
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

Comments
 (0)