474 lines
14 KiB
Rust
474 lines
14 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, Serialize, Deserialize)]
|
|
pub enum ExitStatus {
|
|
/// 待审核
|
|
Pending,
|
|
/// 审核通过
|
|
Approved,
|
|
/// 审核拒绝
|
|
Rejected,
|
|
/// 已确认
|
|
Confirmed,
|
|
/// 已取消
|
|
Cancelled,
|
|
}
|
|
|
|
/// 退出原因
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum ExitReason {
|
|
/// 主动退出
|
|
Voluntary,
|
|
/// 硬件故障
|
|
HardwareFailure,
|
|
/// 违规被迫退出
|
|
Violation,
|
|
/// 质押不足
|
|
InsufficientStake,
|
|
/// 其他原因
|
|
Other(String),
|
|
}
|
|
|
|
/// 退出申请
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExitRequest {
|
|
/// CBP地址
|
|
pub address: Address,
|
|
/// 退出原因
|
|
pub reason: ExitReason,
|
|
/// 申请时间
|
|
pub requested_at: u64,
|
|
/// 退出状态
|
|
pub status: ExitStatus,
|
|
/// 审核时间
|
|
pub reviewed_at: Option<u64>,
|
|
/// 审核人
|
|
pub reviewer: Option<Address>,
|
|
/// 审核意见
|
|
pub review_comment: Option<String>,
|
|
/// 确认时间
|
|
pub confirmed_at: Option<u64>,
|
|
}
|
|
|
|
impl ExitRequest {
|
|
pub fn new(address: Address, reason: ExitReason, timestamp: u64) -> Self {
|
|
Self {
|
|
address,
|
|
reason,
|
|
requested_at: timestamp,
|
|
status: ExitStatus::Pending,
|
|
reviewed_at: None,
|
|
reviewer: None,
|
|
review_comment: None,
|
|
confirmed_at: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 退出记录
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExitRecord {
|
|
/// CBP地址
|
|
pub address: Address,
|
|
/// 退出原因
|
|
pub reason: ExitReason,
|
|
/// 申请时间
|
|
pub requested_at: u64,
|
|
/// 确认时间
|
|
pub confirmed_at: u64,
|
|
/// 质押金额
|
|
pub stake_amount: u64,
|
|
/// 赎回金额
|
|
pub redemption_amount: u64,
|
|
/// 处罚金额
|
|
pub penalty_amount: u64,
|
|
/// 声誉分数
|
|
pub final_reputation: f64,
|
|
/// 生产区块数
|
|
pub blocks_produced: u64,
|
|
}
|
|
|
|
/// 退出管理器
|
|
pub struct ExitManager {
|
|
/// 退出申请
|
|
requests: HashMap<Address, ExitRequest>,
|
|
/// 退出记录
|
|
records: Vec<ExitRecord>,
|
|
/// 审核等待期(秒)
|
|
review_period: u64,
|
|
/// 确认等待期(秒)
|
|
confirmation_period: u64,
|
|
}
|
|
|
|
impl ExitManager {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
requests: HashMap::new(),
|
|
records: Vec::new(),
|
|
review_period: 7 * 24 * 3600, // 7天
|
|
confirmation_period: 14 * 24 * 3600, // 14天
|
|
}
|
|
}
|
|
|
|
/// 提交退出申请
|
|
pub fn submit_exit_request(
|
|
&mut self,
|
|
node: &CbpNode,
|
|
reason: ExitReason,
|
|
timestamp: u64,
|
|
) -> Result<(), CbppL1Error> {
|
|
// 检查节点状态
|
|
if node.status == CbpStatus::Exited {
|
|
return Err(CbppL1Error::InvalidStatus("Node already exited".to_string()));
|
|
}
|
|
|
|
// 检查是否已有待处理的申请
|
|
if let Some(existing) = self.requests.get(&node.address) {
|
|
if existing.status == ExitStatus::Pending || existing.status == ExitStatus::Approved {
|
|
return Err(CbppL1Error::ExitRequestExists);
|
|
}
|
|
}
|
|
|
|
// 创建退出申请
|
|
let request = ExitRequest::new(node.address, reason, timestamp);
|
|
self.requests.insert(node.address, request);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 审核退出申请
|
|
pub fn review_exit_request(
|
|
&mut self,
|
|
address: &Address,
|
|
approved: bool,
|
|
reviewer: Address,
|
|
comment: Option<String>,
|
|
timestamp: u64,
|
|
) -> Result<(), CbppL1Error> {
|
|
let request = self.requests.get_mut(address)
|
|
.ok_or(CbppL1Error::ExitRequestNotFound)?;
|
|
|
|
// 检查申请状态
|
|
if request.status != ExitStatus::Pending {
|
|
return Err(CbppL1Error::InvalidStatus(
|
|
format!("Request status is {:?}, expected Pending", request.status)
|
|
));
|
|
}
|
|
|
|
// 检查审核等待期
|
|
if timestamp < request.requested_at + self.review_period {
|
|
return Err(CbppL1Error::ReviewPeriodNotMet);
|
|
}
|
|
|
|
// 更新申请状态
|
|
request.status = if approved {
|
|
ExitStatus::Approved
|
|
} else {
|
|
ExitStatus::Rejected
|
|
};
|
|
request.reviewed_at = Some(timestamp);
|
|
request.reviewer = Some(reviewer);
|
|
request.review_comment = comment;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 确认退出
|
|
pub fn confirm_exit(
|
|
&mut self,
|
|
address: &Address,
|
|
node: &CbpNode,
|
|
redemption_amount: u64,
|
|
penalty_amount: u64,
|
|
timestamp: u64,
|
|
) -> Result<(), CbppL1Error> {
|
|
let request = self.requests.get_mut(address)
|
|
.ok_or(CbppL1Error::ExitRequestNotFound)?;
|
|
|
|
// 检查申请状态
|
|
if request.status != ExitStatus::Approved {
|
|
return Err(CbppL1Error::InvalidStatus(
|
|
format!("Request status is {:?}, expected Approved", request.status)
|
|
));
|
|
}
|
|
|
|
// 检查确认等待期
|
|
let reviewed_at = request.reviewed_at.ok_or(CbppL1Error::InvalidStatus(
|
|
"Request not reviewed".to_string()
|
|
))?;
|
|
if timestamp < reviewed_at + self.confirmation_period {
|
|
return Err(CbppL1Error::ConfirmationPeriodNotMet);
|
|
}
|
|
|
|
// 更新申请状态
|
|
request.status = ExitStatus::Confirmed;
|
|
request.confirmed_at = Some(timestamp);
|
|
|
|
// 创建退出记录
|
|
let record = ExitRecord {
|
|
address: *address,
|
|
reason: request.reason.clone(),
|
|
requested_at: request.requested_at,
|
|
confirmed_at: timestamp,
|
|
stake_amount: node.stake_amount,
|
|
redemption_amount,
|
|
penalty_amount,
|
|
final_reputation: node.reputation,
|
|
blocks_produced: node.blocks_produced,
|
|
};
|
|
self.records.push(record);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 取消退出申请
|
|
pub fn cancel_exit_request(&mut self, address: &Address) -> Result<(), CbppL1Error> {
|
|
let request = self.requests.get_mut(address)
|
|
.ok_or(CbppL1Error::ExitRequestNotFound)?;
|
|
|
|
// 只能取消待审核的申请
|
|
if request.status != ExitStatus::Pending {
|
|
return Err(CbppL1Error::InvalidStatus(
|
|
format!("Cannot cancel request with status {:?}", request.status)
|
|
));
|
|
}
|
|
|
|
request.status = ExitStatus::Cancelled;
|
|
Ok(())
|
|
}
|
|
|
|
/// 获取退出申请
|
|
pub fn get_exit_request(&self, address: &Address) -> Option<&ExitRequest> {
|
|
self.requests.get(address)
|
|
}
|
|
|
|
/// 获取所有待审核的申请
|
|
pub fn get_pending_requests(&self) -> Vec<&ExitRequest> {
|
|
self.requests.values()
|
|
.filter(|r| r.status == ExitStatus::Pending)
|
|
.collect()
|
|
}
|
|
|
|
/// 获取退出记录
|
|
pub fn get_exit_records(&self, address: Option<&Address>) -> Vec<&ExitRecord> {
|
|
if let Some(addr) = address {
|
|
self.records.iter()
|
|
.filter(|r| &r.address == addr)
|
|
.collect()
|
|
} else {
|
|
self.records.iter().collect()
|
|
}
|
|
}
|
|
|
|
/// 获取退出统计
|
|
pub fn get_exit_statistics(&self) -> ExitStatistics {
|
|
let total_exits = self.records.len();
|
|
let voluntary_exits = self.records.iter()
|
|
.filter(|r| r.reason == ExitReason::Voluntary)
|
|
.count();
|
|
let violation_exits = self.records.iter()
|
|
.filter(|r| r.reason == ExitReason::Violation)
|
|
.count();
|
|
let total_penalties = self.records.iter()
|
|
.map(|r| r.penalty_amount)
|
|
.sum();
|
|
|
|
ExitStatistics {
|
|
total_exits,
|
|
voluntary_exits,
|
|
violation_exits,
|
|
total_penalties,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ExitManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// 退出统计
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExitStatistics {
|
|
/// 总退出数
|
|
pub total_exits: usize,
|
|
/// 主动退出数
|
|
pub voluntary_exits: usize,
|
|
/// 违规退出数
|
|
pub violation_exits: usize,
|
|
/// 总处罚金额
|
|
pub total_penalties: u64,
|
|
}
|
|
|
|
#[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_submit_exit_request() {
|
|
let mut manager = ExitManager::new();
|
|
let node = create_test_node();
|
|
|
|
let result = manager.submit_exit_request(
|
|
&node,
|
|
ExitReason::Voluntary,
|
|
3000,
|
|
);
|
|
assert!(result.is_ok());
|
|
|
|
let request = manager.get_exit_request(&node.address).expect("FIX-006: unexpected None/Err");
|
|
assert_eq!(request.status, ExitStatus::Pending);
|
|
assert_eq!(request.reason, ExitReason::Voluntary);
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_exit_request() {
|
|
let mut manager = ExitManager::new();
|
|
let node = create_test_node();
|
|
|
|
manager.submit_exit_request(&node, ExitReason::Voluntary, 3000).expect("FIX-006: unexpected None/Err");
|
|
let result = manager.submit_exit_request(&node, ExitReason::Voluntary, 3000);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_review_exit_request() {
|
|
let mut manager = ExitManager::new();
|
|
let node = create_test_node();
|
|
let reviewer = Address::new([2u8; 32]);
|
|
|
|
manager.submit_exit_request(&node, ExitReason::Voluntary, 1000).expect("FIX-006: unexpected None/Err");
|
|
|
|
// 审核(等待期满足)
|
|
let result = manager.review_exit_request(
|
|
&node.address,
|
|
true,
|
|
reviewer,
|
|
Some("Approved".to_string()),
|
|
1000 + 7 * 24 * 3600,
|
|
);
|
|
assert!(result.is_ok());
|
|
|
|
let request = manager.get_exit_request(&node.address).expect("FIX-006: unexpected None/Err");
|
|
assert_eq!(request.status, ExitStatus::Approved);
|
|
}
|
|
|
|
#[test]
|
|
fn test_review_period_not_met() {
|
|
let mut manager = ExitManager::new();
|
|
let node = create_test_node();
|
|
let reviewer = Address::new([2u8; 32]);
|
|
|
|
manager.submit_exit_request(&node, ExitReason::Voluntary, 1000).expect("FIX-006: unexpected None/Err");
|
|
|
|
// 审核(等待期不满足)
|
|
let result = manager.review_exit_request(
|
|
&node.address,
|
|
true,
|
|
reviewer,
|
|
None,
|
|
1000 + 3600, // 只过了1小时
|
|
);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_confirm_exit() {
|
|
let mut manager = ExitManager::new();
|
|
let node = create_test_node();
|
|
let reviewer = Address::new([2u8; 32]);
|
|
|
|
manager.submit_exit_request(&node, ExitReason::Voluntary, 1000).expect("FIX-006: unexpected None/Err");
|
|
manager.review_exit_request(
|
|
&node.address,
|
|
true,
|
|
reviewer,
|
|
None,
|
|
1000 + 7 * 24 * 3600,
|
|
).expect("FIX-006: unexpected None/Err");
|
|
|
|
// 确认退出
|
|
let result = manager.confirm_exit(
|
|
&node.address,
|
|
&node,
|
|
95_000_000_000,
|
|
5_000_000_000,
|
|
1000 + 21 * 24 * 3600,
|
|
);
|
|
assert!(result.is_ok());
|
|
|
|
let records = manager.get_exit_records(Some(&node.address));
|
|
assert_eq!(records.len(), 1);
|
|
assert_eq!(records[0].redemption_amount, 95_000_000_000);
|
|
}
|
|
|
|
#[test]
|
|
fn test_cancel_exit_request() {
|
|
let mut manager = ExitManager::new();
|
|
let node = create_test_node();
|
|
|
|
manager.submit_exit_request(&node, ExitReason::Voluntary, 1000).expect("FIX-006: unexpected None/Err");
|
|
|
|
let result = manager.cancel_exit_request(&node.address);
|
|
assert!(result.is_ok());
|
|
|
|
let request = manager.get_exit_request(&node.address).expect("FIX-006: unexpected None/Err");
|
|
assert_eq!(request.status, ExitStatus::Cancelled);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_pending_requests() {
|
|
let mut manager = ExitManager::new();
|
|
let node1 = create_test_node();
|
|
let mut node2 = create_test_node();
|
|
node2.address = Address::new([2u8; 32]);
|
|
|
|
manager.submit_exit_request(&node1, ExitReason::Voluntary, 1000).expect("FIX-006: unexpected None/Err");
|
|
manager.submit_exit_request(&node2, ExitReason::HardwareFailure, 1000).expect("FIX-006: unexpected None/Err");
|
|
|
|
let pending = manager.get_pending_requests();
|
|
assert_eq!(pending.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_exit_statistics() {
|
|
let mut manager = ExitManager::new();
|
|
let node = create_test_node();
|
|
let reviewer = Address::new([2u8; 32]);
|
|
|
|
manager.submit_exit_request(&node, ExitReason::Voluntary, 1000).expect("FIX-006: unexpected None/Err");
|
|
manager.review_exit_request(&node.address, true, reviewer, None, 1000 + 7 * 24 * 3600).expect("FIX-006: unexpected None/Err");
|
|
manager.confirm_exit(&node.address, &node, 95_000_000_000, 5_000_000_000, 1000 + 21 * 24 * 3600).expect("FIX-006: unexpected None/Err");
|
|
|
|
let stats = manager.get_exit_statistics();
|
|
assert_eq!(stats.total_exits, 1);
|
|
assert_eq!(stats.voluntary_exits, 1);
|
|
assert_eq!(stats.total_penalties, 5_000_000_000);
|
|
}
|
|
}
|