762 lines
22 KiB
Rust
762 lines
22 KiB
Rust
//! CBP处罚机制
|
||
//!
|
||
//! 实现CBP节点的违规检测、处罚执行、处罚记录和申诉机制
|
||
|
||
use crate::{CbpNode, CbpStatus, CbppL1Error};
|
||
use nac_udm::primitives::Address;
|
||
use serde::{Deserialize, Serialize};
|
||
use std::collections::HashMap;
|
||
|
||
/// 违规类型
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||
pub enum ViolationType {
|
||
/// 双签
|
||
DoubleSign,
|
||
/// 连续错过区块
|
||
ConsecutiveMissedBlocks,
|
||
/// 恶意行为
|
||
MaliciousBehavior,
|
||
/// 硬件不达标
|
||
HardwareFailure,
|
||
/// KYC过期
|
||
KycExpired,
|
||
/// 质押不足
|
||
InsufficientStake,
|
||
/// 宪法违规
|
||
ConstitutionViolation,
|
||
}
|
||
|
||
/// 处罚类型
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
pub enum PenaltyType {
|
||
/// 警告
|
||
Warning,
|
||
/// 罚款
|
||
Fine,
|
||
/// 暂停
|
||
Suspension,
|
||
/// 强制退出
|
||
ForceExit,
|
||
/// 削减质押
|
||
Slashing,
|
||
}
|
||
|
||
/// 违规检测结果
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct ViolationDetection {
|
||
/// CBP地址
|
||
pub address: Address,
|
||
/// 违规类型
|
||
pub violation_type: ViolationType,
|
||
/// 检测时间
|
||
pub detected_at: u64,
|
||
/// 证据
|
||
pub evidence: String,
|
||
/// 严重程度(1-10)
|
||
pub severity: u8,
|
||
}
|
||
|
||
/// 处罚记录
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct PenaltyRecord {
|
||
/// 记录ID
|
||
pub id: u64,
|
||
/// CBP地址
|
||
pub address: Address,
|
||
/// 违规类型
|
||
pub violation_type: ViolationType,
|
||
/// 处罚类型
|
||
pub penalty_type: PenaltyType,
|
||
/// 罚款金额
|
||
pub fine_amount: u64,
|
||
/// 暂停时长(秒)
|
||
pub suspension_duration: Option<u64>,
|
||
/// 削减金额
|
||
pub slashed_amount: u64,
|
||
/// 处罚时间
|
||
pub penalized_at: u64,
|
||
/// 处罚原因
|
||
pub reason: String,
|
||
/// 证据
|
||
pub evidence: String,
|
||
/// 是否已申诉
|
||
pub appealed: bool,
|
||
}
|
||
|
||
/// 申诉状态
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||
pub enum AppealStatus {
|
||
/// 待审核
|
||
Pending,
|
||
/// 审核通过
|
||
Approved,
|
||
/// 审核拒绝
|
||
Rejected,
|
||
}
|
||
|
||
/// 申诉请求
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct AppealRequest {
|
||
/// 申诉ID
|
||
pub id: u64,
|
||
/// 处罚记录ID
|
||
pub penalty_id: u64,
|
||
/// CBP地址
|
||
pub address: Address,
|
||
/// 申诉理由
|
||
pub reason: String,
|
||
/// 申诉证据
|
||
pub evidence: String,
|
||
/// 申诉时间
|
||
pub appealed_at: u64,
|
||
/// 申诉状态
|
||
pub status: AppealStatus,
|
||
/// 审核时间
|
||
pub reviewed_at: Option<u64>,
|
||
/// 审核人
|
||
pub reviewer: Option<Address>,
|
||
/// 审核意见
|
||
pub review_comment: Option<String>,
|
||
}
|
||
|
||
/// 处罚配置
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct PenaltyConfig {
|
||
/// 违规处罚映射
|
||
pub violation_penalties: HashMap<ViolationType, (PenaltyType, u64)>,
|
||
/// 警告阈值
|
||
pub warning_threshold: u8,
|
||
/// 暂停阈值
|
||
pub suspension_threshold: u8,
|
||
/// 强制退出阈值
|
||
pub force_exit_threshold: u8,
|
||
/// 申诉期限(秒)
|
||
pub appeal_period: u64,
|
||
}
|
||
|
||
impl Default for PenaltyConfig {
|
||
fn default() -> Self {
|
||
let mut violation_penalties = HashMap::new();
|
||
violation_penalties.insert(ViolationType::DoubleSign, (PenaltyType::Slashing, 50_000_000_000));
|
||
violation_penalties.insert(ViolationType::ConsecutiveMissedBlocks, (PenaltyType::Fine, 1_000_000_000));
|
||
violation_penalties.insert(ViolationType::MaliciousBehavior, (PenaltyType::ForceExit, 100_000_000_000));
|
||
violation_penalties.insert(ViolationType::HardwareFailure, (PenaltyType::Warning, 0));
|
||
violation_penalties.insert(ViolationType::KycExpired, (PenaltyType::Suspension, 0));
|
||
violation_penalties.insert(ViolationType::InsufficientStake, (PenaltyType::Suspension, 0));
|
||
violation_penalties.insert(ViolationType::ConstitutionViolation, (PenaltyType::Fine, 5_000_000_000));
|
||
|
||
Self {
|
||
violation_penalties,
|
||
warning_threshold: 3,
|
||
suspension_threshold: 5,
|
||
force_exit_threshold: 10,
|
||
appeal_period: 7 * 24 * 3600, // 7天
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 处罚管理器
|
||
pub struct PenaltyManager {
|
||
/// 处罚配置
|
||
config: PenaltyConfig,
|
||
/// 处罚记录
|
||
records: Vec<PenaltyRecord>,
|
||
/// 申诉请求
|
||
appeals: Vec<AppealRequest>,
|
||
/// 下一个记录ID
|
||
next_record_id: u64,
|
||
/// 下一个申诉ID
|
||
next_appeal_id: u64,
|
||
/// 节点警告计数
|
||
warning_counts: HashMap<Address, u8>,
|
||
}
|
||
|
||
impl PenaltyManager {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
config: PenaltyConfig::default(),
|
||
records: Vec::new(),
|
||
appeals: Vec::new(),
|
||
next_record_id: 1,
|
||
next_appeal_id: 1,
|
||
warning_counts: HashMap::new(),
|
||
}
|
||
}
|
||
|
||
pub fn with_config(config: PenaltyConfig) -> Self {
|
||
Self {
|
||
config,
|
||
records: Vec::new(),
|
||
appeals: Vec::new(),
|
||
next_record_id: 1,
|
||
next_appeal_id: 1,
|
||
warning_counts: HashMap::new(),
|
||
}
|
||
}
|
||
|
||
/// 检测违规
|
||
pub fn detect_violation(
|
||
&self,
|
||
node: &CbpNode,
|
||
violation_type: ViolationType,
|
||
evidence: String,
|
||
severity: u8,
|
||
timestamp: u64,
|
||
) -> ViolationDetection {
|
||
ViolationDetection {
|
||
address: node.address,
|
||
violation_type,
|
||
detected_at: timestamp,
|
||
evidence,
|
||
severity,
|
||
}
|
||
}
|
||
|
||
/// 执行处罚
|
||
pub fn execute_penalty(
|
||
&mut self,
|
||
node: &mut CbpNode,
|
||
detection: ViolationDetection,
|
||
timestamp: u64,
|
||
) -> Result<u64, CbppL1Error> {
|
||
// 获取处罚配置
|
||
let (penalty_type, base_amount) = self.config.violation_penalties
|
||
.get(&detection.violation_type)
|
||
.copied()
|
||
.ok_or(CbppL1Error::UnknownViolationType)?;
|
||
|
||
// 根据严重程度调整金额
|
||
let adjusted_amount = (base_amount as f64 * detection.severity as f64 / 5.0) as u64;
|
||
|
||
let mut fine_amount = 0u64;
|
||
let mut suspension_duration = None;
|
||
let mut slashed_amount = 0u64;
|
||
|
||
// 执行处罚
|
||
match penalty_type {
|
||
PenaltyType::Warning => {
|
||
let count = self.warning_counts.entry(node.address).or_insert(0);
|
||
*count += 1;
|
||
|
||
// 警告次数过多自动升级为暂停
|
||
if *count >= self.config.warning_threshold {
|
||
node.status = CbpStatus::Suspended;
|
||
suspension_duration = Some(7 * 24 * 3600); // 7天
|
||
}
|
||
}
|
||
PenaltyType::Fine => {
|
||
fine_amount = adjusted_amount;
|
||
// 从质押中扣除
|
||
if node.stake_amount >= fine_amount {
|
||
node.stake_amount -= fine_amount;
|
||
} else {
|
||
return Err(CbppL1Error::InsufficientStakeForPenalty);
|
||
}
|
||
}
|
||
PenaltyType::Suspension => {
|
||
node.status = CbpStatus::Suspended;
|
||
suspension_duration = Some(30 * 24 * 3600); // 30天
|
||
}
|
||
PenaltyType::ForceExit => {
|
||
node.status = CbpStatus::Exited;
|
||
slashed_amount = adjusted_amount.min(node.stake_amount);
|
||
node.stake_amount -= slashed_amount;
|
||
}
|
||
PenaltyType::Slashing => {
|
||
slashed_amount = adjusted_amount.min(node.stake_amount);
|
||
node.stake_amount -= slashed_amount;
|
||
// 严重违规同时暂停
|
||
if detection.severity >= 8 {
|
||
node.status = CbpStatus::Suspended;
|
||
suspension_duration = Some(90 * 24 * 3600); // 90天
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建处罚记录
|
||
let record = PenaltyRecord {
|
||
id: self.next_record_id,
|
||
address: node.address,
|
||
violation_type: detection.violation_type,
|
||
penalty_type,
|
||
fine_amount,
|
||
suspension_duration,
|
||
slashed_amount,
|
||
penalized_at: timestamp,
|
||
reason: format!("Violation detected: {:?}", detection.violation_type),
|
||
evidence: detection.evidence,
|
||
appealed: false,
|
||
};
|
||
|
||
let record_id = record.id;
|
||
self.records.push(record);
|
||
self.next_record_id += 1;
|
||
|
||
Ok(record_id)
|
||
}
|
||
|
||
/// 提交申诉
|
||
pub fn submit_appeal(
|
||
&mut self,
|
||
penalty_id: u64,
|
||
address: Address,
|
||
reason: String,
|
||
evidence: String,
|
||
timestamp: u64,
|
||
) -> Result<u64, CbppL1Error> {
|
||
// 查找处罚记录
|
||
let record = self.records.iter_mut()
|
||
.find(|r| r.id == penalty_id)
|
||
.ok_or(CbppL1Error::PenaltyRecordNotFound)?;
|
||
|
||
// 检查地址
|
||
if record.address != address {
|
||
return Err(CbppL1Error::InvalidAddress);
|
||
}
|
||
|
||
// 检查是否已申诉
|
||
if record.appealed {
|
||
return Err(CbppL1Error::AlreadyAppealed);
|
||
}
|
||
|
||
// 检查申诉期限
|
||
if timestamp > record.penalized_at + self.config.appeal_period {
|
||
return Err(CbppL1Error::AppealPeriodExpired);
|
||
}
|
||
|
||
// 标记已申诉
|
||
record.appealed = true;
|
||
|
||
// 创建申诉请求
|
||
let appeal = AppealRequest {
|
||
id: self.next_appeal_id,
|
||
penalty_id,
|
||
address,
|
||
reason,
|
||
evidence,
|
||
appealed_at: timestamp,
|
||
status: AppealStatus::Pending,
|
||
reviewed_at: None,
|
||
reviewer: None,
|
||
review_comment: None,
|
||
};
|
||
|
||
let appeal_id = appeal.id;
|
||
self.appeals.push(appeal);
|
||
self.next_appeal_id += 1;
|
||
|
||
Ok(appeal_id)
|
||
}
|
||
|
||
/// 审核申诉
|
||
pub fn review_appeal(
|
||
&mut self,
|
||
appeal_id: u64,
|
||
approved: bool,
|
||
reviewer: Address,
|
||
comment: Option<String>,
|
||
timestamp: u64,
|
||
) -> Result<(), CbppL1Error> {
|
||
let appeal = self.appeals.iter_mut()
|
||
.find(|a| a.id == appeal_id)
|
||
.ok_or(CbppL1Error::AppealNotFound)?;
|
||
|
||
// 检查申诉状态
|
||
if appeal.status != AppealStatus::Pending {
|
||
return Err(CbppL1Error::InvalidStatus(
|
||
format!("Appeal status is {:?}, expected Pending", appeal.status)
|
||
));
|
||
}
|
||
|
||
// 更新申诉状态
|
||
appeal.status = if approved {
|
||
AppealStatus::Approved
|
||
} else {
|
||
AppealStatus::Rejected
|
||
};
|
||
appeal.reviewed_at = Some(timestamp);
|
||
appeal.reviewer = Some(reviewer);
|
||
appeal.review_comment = comment;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 撤销处罚(申诉成功后)
|
||
pub fn revoke_penalty(
|
||
&mut self,
|
||
penalty_id: u64,
|
||
node: &mut CbpNode,
|
||
) -> Result<(), CbppL1Error> {
|
||
let record = self.records.iter()
|
||
.find(|r| r.id == penalty_id)
|
||
.ok_or(CbppL1Error::PenaltyRecordNotFound)?;
|
||
|
||
// 检查地址
|
||
if record.address != node.address {
|
||
return Err(CbppL1Error::InvalidAddress);
|
||
}
|
||
|
||
// 恢复质押
|
||
node.stake_amount += record.fine_amount + record.slashed_amount;
|
||
|
||
// 恢复状态(如果是暂停或退出)
|
||
if node.status == CbpStatus::Suspended || node.status == CbpStatus::Exited {
|
||
node.status = CbpStatus::Active;
|
||
}
|
||
|
||
// 清除警告计数
|
||
if record.penalty_type == PenaltyType::Warning {
|
||
if let Some(count) = self.warning_counts.get_mut(&node.address) {
|
||
*count = count.saturating_sub(1);
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 获取处罚记录
|
||
pub fn get_penalty_records(&self, address: Option<&Address>) -> Vec<&PenaltyRecord> {
|
||
if let Some(addr) = address {
|
||
self.records.iter()
|
||
.filter(|r| &r.address == addr)
|
||
.collect()
|
||
} else {
|
||
self.records.iter().collect()
|
||
}
|
||
}
|
||
|
||
/// 获取申诉请求
|
||
pub fn get_appeals(&self, status: Option<AppealStatus>) -> Vec<&AppealRequest> {
|
||
if let Some(s) = status {
|
||
self.appeals.iter()
|
||
.filter(|a| a.status == s)
|
||
.collect()
|
||
} else {
|
||
self.appeals.iter().collect()
|
||
}
|
||
}
|
||
|
||
/// 获取处罚统计
|
||
pub fn get_penalty_statistics(&self) -> PenaltyStatistics {
|
||
let total_penalties = self.records.len();
|
||
let warnings = self.records.iter().filter(|r| r.penalty_type == PenaltyType::Warning).count();
|
||
let fines = self.records.iter().filter(|r| r.penalty_type == PenaltyType::Fine).count();
|
||
let suspensions = self.records.iter().filter(|r| r.penalty_type == PenaltyType::Suspension).count();
|
||
let force_exits = self.records.iter().filter(|r| r.penalty_type == PenaltyType::ForceExit).count();
|
||
let slashings = self.records.iter().filter(|r| r.penalty_type == PenaltyType::Slashing).count();
|
||
let total_fines = self.records.iter().map(|r| r.fine_amount).sum();
|
||
let total_slashed = self.records.iter().map(|r| r.slashed_amount).sum();
|
||
let total_appeals = self.appeals.len();
|
||
let approved_appeals = self.appeals.iter().filter(|a| a.status == AppealStatus::Approved).count();
|
||
|
||
PenaltyStatistics {
|
||
total_penalties,
|
||
warnings,
|
||
fines,
|
||
suspensions,
|
||
force_exits,
|
||
slashings,
|
||
total_fines,
|
||
total_slashed,
|
||
total_appeals,
|
||
approved_appeals,
|
||
}
|
||
}
|
||
|
||
/// 更新配置
|
||
pub fn update_config(&mut self, config: PenaltyConfig) {
|
||
self.config = config;
|
||
}
|
||
|
||
/// 获取配置
|
||
pub fn get_config(&self) -> &PenaltyConfig {
|
||
&self.config
|
||
}
|
||
}
|
||
|
||
impl Default for PenaltyManager {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
/// 处罚统计
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct PenaltyStatistics {
|
||
/// 总处罚数
|
||
pub total_penalties: usize,
|
||
/// 警告数
|
||
pub warnings: usize,
|
||
/// 罚款数
|
||
pub fines: usize,
|
||
/// 暂停数
|
||
pub suspensions: usize,
|
||
/// 强制退出数
|
||
pub force_exits: usize,
|
||
/// 削减数
|
||
pub slashings: usize,
|
||
/// 总罚款金额
|
||
pub total_fines: u64,
|
||
/// 总削减金额
|
||
pub total_slashed: u64,
|
||
/// 总申诉数
|
||
pub total_appeals: usize,
|
||
/// 通过申诉数
|
||
pub approved_appeals: usize,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn create_test_node() -> CbpNode {
|
||
CbpNode {
|
||
address: Address::new([1u8; 32]),
|
||
did: "did:nac:test".to_string(),
|
||
stake_amount: 100_000_000_000,
|
||
kyc_level: 2,
|
||
hardware_score: 8000,
|
||
constitution_score: 80,
|
||
status: CbpStatus::Active,
|
||
registered_at: 1000,
|
||
last_active_at: 2000,
|
||
blocks_produced: 100,
|
||
reputation: 0.8,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_detect_violation() {
|
||
let manager = PenaltyManager::new();
|
||
let node = create_test_node();
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::DoubleSign,
|
||
"Evidence of double signing".to_string(),
|
||
8,
|
||
3000,
|
||
);
|
||
|
||
assert_eq!(detection.violation_type, ViolationType::DoubleSign);
|
||
assert_eq!(detection.severity, 8);
|
||
}
|
||
|
||
#[test]
|
||
fn test_execute_penalty_warning() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::HardwareFailure,
|
||
"Hardware benchmark failed".to_string(),
|
||
3,
|
||
3000,
|
||
);
|
||
|
||
let result = manager.execute_penalty(&mut node, detection, 3000);
|
||
assert!(result.is_ok());
|
||
|
||
let warning_count = manager.warning_counts.get(&node.address).copied().unwrap_or(0);
|
||
assert_eq!(warning_count, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_execute_penalty_fine() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
let original_stake = node.stake_amount;
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::ConsecutiveMissedBlocks,
|
||
"Missed 10 consecutive blocks".to_string(),
|
||
5,
|
||
3000,
|
||
);
|
||
|
||
let result = manager.execute_penalty(&mut node, detection, 3000);
|
||
assert!(result.is_ok());
|
||
assert!(node.stake_amount < original_stake);
|
||
}
|
||
|
||
#[test]
|
||
fn test_execute_penalty_suspension() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::KycExpired,
|
||
"KYC expired".to_string(),
|
||
5,
|
||
3000,
|
||
);
|
||
|
||
let result = manager.execute_penalty(&mut node, detection, 3000);
|
||
assert!(result.is_ok());
|
||
assert_eq!(node.status, CbpStatus::Suspended);
|
||
}
|
||
|
||
#[test]
|
||
fn test_execute_penalty_slashing() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
let original_stake = node.stake_amount;
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::DoubleSign,
|
||
"Double signing detected".to_string(),
|
||
10,
|
||
3000,
|
||
);
|
||
|
||
let result = manager.execute_penalty(&mut node, detection, 3000);
|
||
assert!(result.is_ok());
|
||
assert!(node.stake_amount < original_stake);
|
||
}
|
||
|
||
#[test]
|
||
fn test_submit_appeal() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::ConsecutiveMissedBlocks,
|
||
"Evidence".to_string(),
|
||
5,
|
||
1000,
|
||
);
|
||
|
||
let penalty_id = manager.execute_penalty(&mut node, detection, 1000).expect("FIX-006: unexpected None/Err");
|
||
|
||
let result = manager.submit_appeal(
|
||
penalty_id,
|
||
node.address,
|
||
"False positive".to_string(),
|
||
"Counter evidence".to_string(),
|
||
1000 + 3600,
|
||
);
|
||
|
||
assert!(result.is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn test_appeal_period_expired() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::ConsecutiveMissedBlocks,
|
||
"Evidence".to_string(),
|
||
5,
|
||
1000,
|
||
);
|
||
|
||
let penalty_id = manager.execute_penalty(&mut node, detection, 1000).expect("FIX-006: unexpected None/Err");
|
||
|
||
// 超过申诉期限
|
||
let result = manager.submit_appeal(
|
||
penalty_id,
|
||
node.address,
|
||
"False positive".to_string(),
|
||
"Counter evidence".to_string(),
|
||
1000 + 8 * 24 * 3600,
|
||
);
|
||
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_review_appeal() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
let reviewer = Address::new([2u8; 32]);
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::ConsecutiveMissedBlocks,
|
||
"Evidence".to_string(),
|
||
5,
|
||
1000,
|
||
);
|
||
|
||
let penalty_id = manager.execute_penalty(&mut node, detection, 1000).expect("FIX-006: unexpected None/Err");
|
||
let appeal_id = manager.submit_appeal(
|
||
penalty_id,
|
||
node.address,
|
||
"False positive".to_string(),
|
||
"Counter evidence".to_string(),
|
||
1000 + 3600,
|
||
).expect("FIX-006: unexpected None/Err");
|
||
|
||
let result = manager.review_appeal(
|
||
appeal_id,
|
||
true,
|
||
reviewer,
|
||
Some("Approved".to_string()),
|
||
1000 + 7200,
|
||
);
|
||
|
||
assert!(result.is_ok());
|
||
|
||
let appeals = manager.get_appeals(Some(AppealStatus::Approved));
|
||
assert_eq!(appeals.len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn test_revoke_penalty() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
let original_stake = node.stake_amount;
|
||
|
||
let detection = manager.detect_violation(
|
||
&node,
|
||
ViolationType::ConsecutiveMissedBlocks,
|
||
"Evidence".to_string(),
|
||
5,
|
||
1000,
|
||
);
|
||
|
||
let penalty_id = manager.execute_penalty(&mut node, detection, 1000).expect("FIX-006: unexpected None/Err");
|
||
let stake_after_penalty = node.stake_amount;
|
||
|
||
let result = manager.revoke_penalty(penalty_id, &mut node);
|
||
assert!(result.is_ok());
|
||
assert!(node.stake_amount > stake_after_penalty);
|
||
}
|
||
|
||
#[test]
|
||
fn test_penalty_statistics() {
|
||
let mut manager = PenaltyManager::new();
|
||
let mut node = create_test_node();
|
||
|
||
let detection1 = manager.detect_violation(
|
||
&node,
|
||
ViolationType::HardwareFailure,
|
||
"Evidence".to_string(),
|
||
3,
|
||
1000,
|
||
);
|
||
manager.execute_penalty(&mut node, detection1, 1000).expect("FIX-006: unexpected None/Err");
|
||
|
||
let detection2 = manager.detect_violation(
|
||
&node,
|
||
ViolationType::ConsecutiveMissedBlocks,
|
||
"Evidence".to_string(),
|
||
5,
|
||
2000,
|
||
);
|
||
manager.execute_penalty(&mut node, detection2, 2000).expect("FIX-006: unexpected None/Err");
|
||
|
||
let stats = manager.get_penalty_statistics();
|
||
assert_eq!(stats.total_penalties, 2);
|
||
assert_eq!(stats.warnings, 1);
|
||
assert_eq!(stats.fines, 1);
|
||
}
|
||
}
|