use chrono::{DateTime, Duration, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use uuid::Uuid;
use crate::error::{CoreError, Result};
use crate::trading::order_book::{LimitOrder, OrderSide};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitRevealConfig {
pub commit_duration: Duration,
pub reveal_duration: Duration,
pub non_reveal_penalty_pct: Decimal,
pub allow_early_reveal: bool,
}
impl Default for CommitRevealConfig {
fn default() -> Self {
Self {
commit_duration: Duration::seconds(30),
reveal_duration: Duration::seconds(60),
non_reveal_penalty_pct: dec!(1.0), allow_early_reveal: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CommitRevealPhase {
Commit,
Reveal,
Processing,
Completed,
Expired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderCommitment {
pub id: Uuid,
pub user_id: Uuid,
pub token_id: Uuid,
pub commitment_hash: String,
pub committed_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub is_revealed: bool,
pub revealed_order: Option<RevealedOrder>,
}
impl OrderCommitment {
pub fn new(
user_id: Uuid,
token_id: Uuid,
commitment_hash: String,
expires_at: DateTime<Utc>,
) -> Self {
Self {
id: Uuid::new_v4(),
user_id,
token_id,
commitment_hash,
committed_at: Utc::now(),
expires_at,
is_revealed: false,
revealed_order: None,
}
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub fn can_reveal(&self, allow_early: bool, reveal_start: DateTime<Utc>) -> bool {
if self.is_revealed || self.is_expired() {
return false;
}
allow_early || Utc::now() >= reveal_start
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevealedOrder {
pub order: OrderParams,
pub secret: String,
pub revealed_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderParams {
pub side: OrderSide,
pub price: Decimal,
pub amount: Decimal,
}
impl OrderParams {
pub fn compute_commitment(&self, secret: &str) -> String {
let data = format!("{:?}|{}|{}|{}", self.side, self.price, self.amount, secret);
let hash = Sha256::digest(data.as_bytes());
hex::encode(hash)
}
pub fn verify_commitment(&self, secret: &str, expected_hash: &str) -> bool {
let computed_hash = self.compute_commitment(secret);
computed_hash == expected_hash
}
pub fn to_limit_order(&self, user_id: Uuid, token_id: Uuid) -> LimitOrder {
LimitOrder {
order_id: Uuid::new_v4(),
user_id,
token_id,
side: self.side,
price: self.price,
amount: self.amount,
filled_amount: dec!(0),
timestamp: Utc::now().timestamp_millis(),
}
}
}
pub struct CommitRevealCycle {
pub id: Uuid,
pub token_id: Uuid,
pub config: CommitRevealConfig,
pub phase: CommitRevealPhase,
pub commit_start: DateTime<Utc>,
pub reveal_start: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub commitments: HashMap<Uuid, OrderCommitment>,
pub revealed_orders: Vec<LimitOrder>,
}
impl CommitRevealCycle {
pub fn new(token_id: Uuid, config: CommitRevealConfig) -> Self {
let commit_start = Utc::now();
let reveal_start = commit_start + config.commit_duration;
let end_time = reveal_start + config.reveal_duration;
Self {
id: Uuid::new_v4(),
token_id,
config,
phase: CommitRevealPhase::Commit,
commit_start,
reveal_start,
end_time,
commitments: HashMap::new(),
revealed_orders: Vec::new(),
}
}
pub fn update_phase(&mut self) {
let now = Utc::now();
if now > self.end_time {
self.phase = CommitRevealPhase::Expired;
} else if now >= self.reveal_start && self.phase == CommitRevealPhase::Commit {
self.phase = CommitRevealPhase::Reveal;
}
}
pub fn add_commitment(
&mut self,
user_id: Uuid,
token_id: Uuid,
commitment_hash: String,
) -> Result<Uuid> {
self.update_phase();
if self.phase != CommitRevealPhase::Commit {
return Err(CoreError::InvalidState(format!(
"Not in commit phase (current: {:?})",
self.phase
)));
}
if token_id != self.token_id {
return Err(CoreError::Validation(format!(
"Token {} does not match cycle token {}",
token_id, self.token_id
)));
}
let commitment = OrderCommitment::new(user_id, token_id, commitment_hash, self.end_time);
let commitment_id = commitment.id;
self.commitments.insert(commitment_id, commitment);
Ok(commitment_id)
}
pub fn reveal_order(
&mut self,
commitment_id: Uuid,
order_params: OrderParams,
secret: String,
) -> Result<LimitOrder> {
self.update_phase();
let commitment = self.commitments.get_mut(&commitment_id).ok_or_else(|| {
CoreError::NotFound(format!("Commitment {} not found", commitment_id))
})?;
if !commitment.can_reveal(self.config.allow_early_reveal, self.reveal_start) {
return Err(CoreError::InvalidState(
"Cannot reveal at this time".to_string(),
));
}
if !order_params.verify_commitment(&secret, &commitment.commitment_hash) {
return Err(CoreError::Validation(
"Commitment verification failed".to_string(),
));
}
commitment.is_revealed = true;
commitment.revealed_order = Some(RevealedOrder {
order: order_params.clone(),
secret: secret.clone(),
revealed_at: Utc::now(),
});
let limit_order = order_params.to_limit_order(commitment.user_id, commitment.token_id);
self.revealed_orders.push(limit_order.clone());
Ok(limit_order)
}
pub fn stats(&self) -> CommitRevealStats {
let total_commitments = self.commitments.len();
let revealed_count = self.commitments.values().filter(|c| c.is_revealed).count();
let unrevealed_count = total_commitments - revealed_count;
CommitRevealStats {
cycle_id: self.id,
token_id: self.token_id,
phase: self.phase,
total_commitments,
revealed_count,
unrevealed_count,
reveal_rate_pct: if total_commitments > 0 {
(Decimal::from(revealed_count) / Decimal::from(total_commitments)) * dec!(100)
} else {
dec!(0)
},
commit_start: self.commit_start,
reveal_start: self.reveal_start,
end_time: self.end_time,
}
}
pub fn process(&mut self) -> Result<Vec<LimitOrder>> {
self.update_phase();
if self.phase != CommitRevealPhase::Reveal && self.phase != CommitRevealPhase::Expired {
return Err(CoreError::InvalidState(
"Cycle not ready for processing".to_string(),
));
}
self.phase = CommitRevealPhase::Processing;
let orders = self.revealed_orders.clone();
self.phase = CommitRevealPhase::Completed;
Ok(orders)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommitRevealStats {
pub cycle_id: Uuid,
pub token_id: Uuid,
pub phase: CommitRevealPhase,
pub total_commitments: usize,
pub revealed_count: usize,
pub unrevealed_count: usize,
pub reveal_rate_pct: Decimal,
pub commit_start: DateTime<Utc>,
pub reveal_start: DateTime<Utc>,
pub end_time: DateTime<Utc>,
}
pub struct CommitRevealManager {
active_cycles: HashMap<Uuid, CommitRevealCycle>,
completed_cycles: Vec<CommitRevealCycle>,
default_config: CommitRevealConfig,
}
impl CommitRevealManager {
pub fn new(config: CommitRevealConfig) -> Self {
Self {
active_cycles: HashMap::new(),
completed_cycles: Vec::new(),
default_config: config,
}
}
pub fn start_cycle(&mut self, token_id: Uuid) -> Result<Uuid> {
if self.active_cycles.contains_key(&token_id) {
return Err(CoreError::AlreadyExists(format!(
"Active cycle already exists for token {}",
token_id
)));
}
let cycle = CommitRevealCycle::new(token_id, self.default_config.clone());
let cycle_id = cycle.id;
self.active_cycles.insert(token_id, cycle);
Ok(cycle_id)
}
pub fn add_commitment(
&mut self,
token_id: Uuid,
user_id: Uuid,
commitment_hash: String,
) -> Result<Uuid> {
let cycle = self.active_cycles.get_mut(&token_id).ok_or_else(|| {
CoreError::NotFound(format!("No active cycle for token {}", token_id))
})?;
cycle.add_commitment(user_id, token_id, commitment_hash)
}
pub fn reveal_order(
&mut self,
token_id: Uuid,
commitment_id: Uuid,
order_params: OrderParams,
secret: String,
) -> Result<LimitOrder> {
let cycle = self.active_cycles.get_mut(&token_id).ok_or_else(|| {
CoreError::NotFound(format!("No active cycle for token {}", token_id))
})?;
cycle.reveal_order(commitment_id, order_params, secret)
}
pub fn process_cycle(&mut self, token_id: Uuid) -> Result<Vec<LimitOrder>> {
let mut cycle = self.active_cycles.remove(&token_id).ok_or_else(|| {
CoreError::NotFound(format!("No active cycle for token {}", token_id))
})?;
let orders = cycle.process()?;
self.completed_cycles.push(cycle);
Ok(orders)
}
pub fn get_active_cycle(&self, token_id: &Uuid) -> Option<&CommitRevealCycle> {
self.active_cycles.get(token_id)
}
pub fn get_completed_stats(&self) -> Vec<CommitRevealStats> {
self.completed_cycles.iter().map(|c| c.stats()).collect()
}
pub fn cleanup_old_cycles(&mut self, keep_last: usize) {
if self.completed_cycles.len() > keep_last {
self.completed_cycles
.drain(0..self.completed_cycles.len() - keep_last);
}
}
}
impl Default for CommitRevealManager {
fn default() -> Self {
Self::new(CommitRevealConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_commitment_hash() {
let params = OrderParams {
side: OrderSide::Buy,
price: dec!(100),
amount: dec!(10),
};
let secret = "my_secret_123";
let hash1 = params.compute_commitment(secret);
let hash2 = params.compute_commitment(secret);
assert_eq!(hash1, hash2);
let hash3 = params.compute_commitment("different_secret");
assert_ne!(hash1, hash3);
}
#[test]
fn test_commitment_verification() {
let params = OrderParams {
side: OrderSide::Buy,
price: dec!(100),
amount: dec!(10),
};
let secret = "my_secret";
let hash = params.compute_commitment(secret);
assert!(params.verify_commitment(secret, &hash));
assert!(!params.verify_commitment("wrong_secret", &hash));
assert!(!params.verify_commitment(secret, "wrong_hash"));
}
#[test]
fn test_commit_reveal_cycle() {
let token_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let config = CommitRevealConfig {
allow_early_reveal: true, ..Default::default()
};
let mut cycle = CommitRevealCycle::new(token_id, config);
let params = OrderParams {
side: OrderSide::Buy,
price: dec!(100),
amount: dec!(10),
};
let secret = "test_secret";
let commitment_hash = params.compute_commitment(secret);
let commitment_id = cycle
.add_commitment(user_id, token_id, commitment_hash)
.unwrap();
let order = cycle
.reveal_order(commitment_id, params.clone(), secret.to_string())
.unwrap();
assert_eq!(order.price, dec!(100));
assert_eq!(order.amount, dec!(10));
assert_eq!(order.side, OrderSide::Buy);
}
#[test]
fn test_invalid_reveal() {
let token_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let config = CommitRevealConfig {
allow_early_reveal: true,
..Default::default()
};
let mut cycle = CommitRevealCycle::new(token_id, config);
let params = OrderParams {
side: OrderSide::Buy,
price: dec!(100),
amount: dec!(10),
};
let secret = "test_secret";
let commitment_hash = params.compute_commitment(secret);
let commitment_id = cycle
.add_commitment(user_id, token_id, commitment_hash)
.unwrap();
let wrong_params = OrderParams {
side: OrderSide::Buy,
price: dec!(100),
amount: dec!(10),
};
let result = cycle.reveal_order(commitment_id, wrong_params, "wrong_secret".to_string());
assert!(result.is_err());
}
#[test]
fn test_commit_reveal_manager() {
let token_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let mut manager = CommitRevealManager::default();
let cycle_id = manager.start_cycle(token_id).unwrap();
assert!(cycle_id != Uuid::nil());
let params = OrderParams {
side: OrderSide::Buy,
price: dec!(100),
amount: dec!(10),
};
let secret = "secret";
let hash = params.compute_commitment(secret);
let commitment_id = manager.add_commitment(token_id, user_id, hash).unwrap();
assert!(commitment_id != Uuid::nil());
}
#[test]
fn test_cycle_stats() {
let token_id = Uuid::new_v4();
let config = CommitRevealConfig {
allow_early_reveal: true,
..Default::default()
};
let mut cycle = CommitRevealCycle::new(token_id, config);
for i in 0..5 {
let params = OrderParams {
side: OrderSide::Buy,
price: dec!(100),
amount: Decimal::from(i),
};
let hash = params.compute_commitment(&format!("secret_{}", i));
cycle
.add_commitment(Uuid::new_v4(), token_id, hash)
.unwrap();
}
let stats = cycle.stats();
assert_eq!(stats.total_commitments, 5);
assert_eq!(stats.revealed_count, 0);
assert_eq!(stats.unrevealed_count, 5);
}
}