Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
147
vendor/ruvector/crates/ruvector-dag/src/qudag/client.rs
vendored
Normal file
147
vendor/ruvector/crates/ruvector-dag/src/qudag/client.rs
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
//! QuDAG Network Client
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QuDagConfig {
|
||||
pub endpoint: String,
|
||||
pub timeout_ms: u64,
|
||||
pub max_retries: usize,
|
||||
pub stake_amount: f64,
|
||||
}
|
||||
|
||||
impl Default for QuDagConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
endpoint: "https://qudag.network:8443".to_string(),
|
||||
timeout_ms: 5000,
|
||||
max_retries: 3,
|
||||
stake_amount: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuDagClient {
|
||||
#[allow(dead_code)]
|
||||
config: QuDagConfig,
|
||||
node_id: String,
|
||||
connected: Arc<RwLock<bool>>,
|
||||
// In real implementation, would have ML-DSA keypair
|
||||
#[allow(dead_code)]
|
||||
identity_key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl QuDagClient {
|
||||
pub fn new(config: QuDagConfig) -> Self {
|
||||
// Generate random node ID for now
|
||||
let node_id = format!("node_{}", rand::random::<u64>());
|
||||
|
||||
Self {
|
||||
config,
|
||||
node_id,
|
||||
connected: Arc::new(RwLock::new(false)),
|
||||
identity_key: vec![0u8; 32], // Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(&self) -> Result<(), QuDagError> {
|
||||
// Simulate connection
|
||||
*self.connected.write().await = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn disconnect(&self) {
|
||||
*self.connected.write().await = false;
|
||||
}
|
||||
|
||||
pub async fn is_connected(&self) -> bool {
|
||||
*self.connected.read().await
|
||||
}
|
||||
|
||||
pub fn node_id(&self) -> &str {
|
||||
&self.node_id
|
||||
}
|
||||
|
||||
pub async fn propose_pattern(
|
||||
&self,
|
||||
_pattern: super::proposal::PatternProposal,
|
||||
) -> Result<String, QuDagError> {
|
||||
if !self.is_connected().await {
|
||||
return Err(QuDagError::NotConnected);
|
||||
}
|
||||
|
||||
// Generate proposal ID
|
||||
let proposal_id = format!("prop_{}", rand::random::<u64>());
|
||||
|
||||
// In real implementation, would:
|
||||
// 1. Sign with ML-DSA
|
||||
// 2. Add differential privacy noise
|
||||
// 3. Submit to network
|
||||
|
||||
Ok(proposal_id)
|
||||
}
|
||||
|
||||
pub async fn get_proposal_status(
|
||||
&self,
|
||||
_proposal_id: &str,
|
||||
) -> Result<super::proposal::ProposalStatus, QuDagError> {
|
||||
if !self.is_connected().await {
|
||||
return Err(QuDagError::NotConnected);
|
||||
}
|
||||
|
||||
// Simulate status check
|
||||
Ok(super::proposal::ProposalStatus::Pending)
|
||||
}
|
||||
|
||||
pub async fn sync_patterns(
|
||||
&self,
|
||||
_since_round: u64,
|
||||
) -> Result<Vec<super::sync::SyncedPattern>, QuDagError> {
|
||||
if !self.is_connected().await {
|
||||
return Err(QuDagError::NotConnected);
|
||||
}
|
||||
|
||||
// Return empty for now
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub async fn get_balance(&self) -> Result<f64, QuDagError> {
|
||||
if !self.is_connected().await {
|
||||
return Err(QuDagError::NotConnected);
|
||||
}
|
||||
|
||||
Ok(0.0)
|
||||
}
|
||||
|
||||
pub async fn stake(&self, amount: f64) -> Result<String, QuDagError> {
|
||||
if !self.is_connected().await {
|
||||
return Err(QuDagError::NotConnected);
|
||||
}
|
||||
|
||||
if amount <= 0.0 {
|
||||
return Err(QuDagError::InvalidAmount);
|
||||
}
|
||||
|
||||
// Return transaction hash
|
||||
Ok(format!("tx_{}", rand::random::<u64>()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum QuDagError {
|
||||
#[error("Not connected to QuDAG network")]
|
||||
NotConnected,
|
||||
#[error("Connection failed: {0}")]
|
||||
ConnectionFailed(String),
|
||||
#[error("Authentication failed")]
|
||||
AuthFailed,
|
||||
#[error("Invalid amount")]
|
||||
InvalidAmount,
|
||||
#[error("Proposal rejected: {0}")]
|
||||
ProposalRejected(String),
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(String),
|
||||
#[error("Timeout")]
|
||||
Timeout,
|
||||
}
|
||||
85
vendor/ruvector/crates/ruvector-dag/src/qudag/consensus.rs
vendored
Normal file
85
vendor/ruvector/crates/ruvector-dag/src/qudag/consensus.rs
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
//! Consensus Validation
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConsensusResult {
|
||||
pub round: u64,
|
||||
pub proposal_id: String,
|
||||
pub accepted: bool,
|
||||
pub stake_weight: f64,
|
||||
pub validator_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Vote {
|
||||
pub voter_id: String,
|
||||
pub proposal_id: String,
|
||||
pub approve: bool,
|
||||
pub stake_weight: f64,
|
||||
pub signature: Vec<u8>, // ML-DSA signature
|
||||
}
|
||||
|
||||
impl Vote {
|
||||
pub fn new(voter_id: String, proposal_id: String, approve: bool, stake_weight: f64) -> Self {
|
||||
Self {
|
||||
voter_id,
|
||||
proposal_id,
|
||||
approve,
|
||||
stake_weight,
|
||||
signature: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign(&mut self, _private_key: &[u8]) {
|
||||
// Would use ML-DSA to sign
|
||||
self.signature = vec![0u8; 64];
|
||||
}
|
||||
|
||||
pub fn verify(&self, _public_key: &[u8]) -> bool {
|
||||
// Would verify ML-DSA signature
|
||||
!self.signature.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ConsensusTracker {
|
||||
proposals: std::collections::HashMap<String, Vec<Vote>>,
|
||||
threshold: f64, // Stake threshold for acceptance (e.g., 0.67)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl ConsensusTracker {
|
||||
pub fn new(threshold: f64) -> Self {
|
||||
Self {
|
||||
proposals: std::collections::HashMap::new(),
|
||||
threshold,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_vote(&mut self, vote: Vote) {
|
||||
self.proposals
|
||||
.entry(vote.proposal_id.clone())
|
||||
.or_default()
|
||||
.push(vote);
|
||||
}
|
||||
|
||||
pub fn check_consensus(&self, proposal_id: &str) -> Option<ConsensusResult> {
|
||||
let votes = self.proposals.get(proposal_id)?;
|
||||
|
||||
let total_stake: f64 = votes.iter().map(|v| v.stake_weight).sum();
|
||||
let approve_stake: f64 = votes
|
||||
.iter()
|
||||
.filter(|v| v.approve)
|
||||
.map(|v| v.stake_weight)
|
||||
.sum();
|
||||
|
||||
let accepted = approve_stake / total_stake > self.threshold;
|
||||
|
||||
Some(ConsensusResult {
|
||||
round: 0,
|
||||
proposal_id: proposal_id.to_string(),
|
||||
accepted,
|
||||
stake_weight: total_stake,
|
||||
validator_count: votes.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
90
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/differential_privacy.rs
vendored
Normal file
90
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/differential_privacy.rs
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
//! Differential Privacy for Pattern Sharing
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DpConfig {
|
||||
pub epsilon: f64, // Privacy budget
|
||||
pub delta: f64, // Failure probability
|
||||
pub sensitivity: f64, // Query sensitivity
|
||||
}
|
||||
|
||||
impl Default for DpConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
epsilon: 1.0,
|
||||
delta: 1e-5,
|
||||
sensitivity: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DifferentialPrivacy {
|
||||
config: DpConfig,
|
||||
}
|
||||
|
||||
impl DifferentialPrivacy {
|
||||
pub fn new(config: DpConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
/// Add Laplace noise for (epsilon, 0)-differential privacy
|
||||
pub fn laplace_noise(&self, value: f64) -> f64 {
|
||||
let scale = self.config.sensitivity / self.config.epsilon;
|
||||
let noise = self.sample_laplace(scale);
|
||||
value + noise
|
||||
}
|
||||
|
||||
/// Add Laplace noise to a vector
|
||||
pub fn add_noise_to_vector(&self, vector: &mut [f32]) {
|
||||
let scale = self.config.sensitivity / self.config.epsilon;
|
||||
for v in vector.iter_mut() {
|
||||
let noise = self.sample_laplace(scale);
|
||||
*v += noise as f32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add Gaussian noise for (epsilon, delta)-differential privacy
|
||||
pub fn gaussian_noise(&self, value: f64) -> f64 {
|
||||
let sigma = self.gaussian_sigma();
|
||||
let noise = self.sample_gaussian(sigma);
|
||||
value + noise
|
||||
}
|
||||
|
||||
fn gaussian_sigma(&self) -> f64 {
|
||||
// Compute sigma for (epsilon, delta)-DP
|
||||
let c = (2.0 * (1.25 / self.config.delta).ln()).sqrt();
|
||||
c * self.config.sensitivity / self.config.epsilon
|
||||
}
|
||||
|
||||
fn sample_laplace(&self, scale: f64) -> f64 {
|
||||
let mut rng = rand::thread_rng();
|
||||
// Clamp to avoid ln(0) - use small epsilon for numerical stability
|
||||
let u: f64 = rng.gen::<f64>() - 0.5;
|
||||
let clamped = (1.0 - 2.0 * u.abs()).clamp(f64::EPSILON, 1.0);
|
||||
-scale * u.signum() * clamped.ln()
|
||||
}
|
||||
|
||||
fn sample_gaussian(&self, sigma: f64) -> f64 {
|
||||
let mut rng = rand::thread_rng();
|
||||
// Box-Muller transform with numerical stability
|
||||
// Clamp u1 to avoid ln(0)
|
||||
let u1: f64 = rng.gen::<f64>().clamp(f64::EPSILON, 1.0 - f64::EPSILON);
|
||||
let u2: f64 = rng.gen();
|
||||
sigma * (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
|
||||
}
|
||||
|
||||
/// Compute privacy loss for a composition of queries
|
||||
pub fn privacy_loss(&self, num_queries: usize) -> f64 {
|
||||
// Basic composition theorem
|
||||
self.config.epsilon * (num_queries as f64)
|
||||
}
|
||||
|
||||
/// Compute privacy loss with advanced composition
|
||||
pub fn advanced_privacy_loss(&self, num_queries: usize) -> f64 {
|
||||
let k = num_queries as f64;
|
||||
// Advanced composition theorem
|
||||
(2.0 * k * (1.0 / self.config.delta).ln()).sqrt() * self.config.epsilon
|
||||
+ k * self.config.epsilon * (self.config.epsilon.exp() - 1.0)
|
||||
}
|
||||
}
|
||||
129
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/identity.rs
vendored
Normal file
129
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/identity.rs
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
//! QuDAG Identity Management
|
||||
|
||||
use super::{
|
||||
MlDsa65, MlDsa65PublicKey, MlDsa65SecretKey, MlKem768, MlKem768PublicKey, MlKem768SecretKey,
|
||||
};
|
||||
|
||||
pub struct QuDagIdentity {
|
||||
pub node_id: String,
|
||||
pub kem_public: MlKem768PublicKey,
|
||||
pub kem_secret: MlKem768SecretKey,
|
||||
pub dsa_public: MlDsa65PublicKey,
|
||||
pub dsa_secret: MlDsa65SecretKey,
|
||||
}
|
||||
|
||||
impl QuDagIdentity {
|
||||
pub fn generate() -> Result<Self, IdentityError> {
|
||||
let (kem_public, kem_secret) =
|
||||
MlKem768::generate_keypair().map_err(|_| IdentityError::KeyGenerationFailed)?;
|
||||
|
||||
let (dsa_public, dsa_secret) =
|
||||
MlDsa65::generate_keypair().map_err(|_| IdentityError::KeyGenerationFailed)?;
|
||||
|
||||
// Generate node ID from public key hash
|
||||
let node_id = Self::hash_to_id(&kem_public.0[..32]);
|
||||
|
||||
Ok(Self {
|
||||
node_id,
|
||||
kem_public,
|
||||
kem_secret,
|
||||
dsa_public,
|
||||
dsa_secret,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, IdentityError> {
|
||||
let sig =
|
||||
MlDsa65::sign(&self.dsa_secret, message).map_err(|_| IdentityError::SigningFailed)?;
|
||||
Ok(sig.0.to_vec())
|
||||
}
|
||||
|
||||
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, IdentityError> {
|
||||
if signature.len() != super::ml_dsa::ML_DSA_65_SIGNATURE_SIZE {
|
||||
return Err(IdentityError::InvalidSignature);
|
||||
}
|
||||
|
||||
let mut sig_array = [0u8; super::ml_dsa::ML_DSA_65_SIGNATURE_SIZE];
|
||||
sig_array.copy_from_slice(signature);
|
||||
|
||||
MlDsa65::verify(
|
||||
&self.dsa_public,
|
||||
message,
|
||||
&super::ml_dsa::Signature(sig_array),
|
||||
)
|
||||
.map_err(|_| IdentityError::VerificationFailed)
|
||||
}
|
||||
|
||||
pub fn encrypt_for(
|
||||
&self,
|
||||
recipient_pk: &[u8],
|
||||
plaintext: &[u8],
|
||||
) -> Result<Vec<u8>, IdentityError> {
|
||||
if recipient_pk.len() != super::ml_kem::ML_KEM_768_PUBLIC_KEY_SIZE {
|
||||
return Err(IdentityError::InvalidPublicKey);
|
||||
}
|
||||
|
||||
let mut pk_array = [0u8; super::ml_kem::ML_KEM_768_PUBLIC_KEY_SIZE];
|
||||
pk_array.copy_from_slice(recipient_pk);
|
||||
|
||||
let encap = MlKem768::encapsulate(&MlKem768PublicKey(pk_array))
|
||||
.map_err(|_| IdentityError::EncryptionFailed)?;
|
||||
|
||||
// Simple XOR encryption with shared secret
|
||||
let mut ciphertext = encap.ciphertext.to_vec();
|
||||
for (i, byte) in plaintext.iter().enumerate() {
|
||||
ciphertext.push(*byte ^ encap.shared_secret[i % 32]);
|
||||
}
|
||||
|
||||
Ok(ciphertext)
|
||||
}
|
||||
|
||||
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, IdentityError> {
|
||||
if ciphertext.len() < super::ml_kem::ML_KEM_768_CIPHERTEXT_SIZE {
|
||||
return Err(IdentityError::InvalidCiphertext);
|
||||
}
|
||||
|
||||
let mut ct_array = [0u8; super::ml_kem::ML_KEM_768_CIPHERTEXT_SIZE];
|
||||
ct_array.copy_from_slice(&ciphertext[..super::ml_kem::ML_KEM_768_CIPHERTEXT_SIZE]);
|
||||
|
||||
let shared_secret = MlKem768::decapsulate(&self.kem_secret, &ct_array)
|
||||
.map_err(|_| IdentityError::DecryptionFailed)?;
|
||||
|
||||
// Decrypt with XOR
|
||||
let encrypted_data = &ciphertext[super::ml_kem::ML_KEM_768_CIPHERTEXT_SIZE..];
|
||||
let plaintext: Vec<u8> = encrypted_data
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &b)| b ^ shared_secret[i % 32])
|
||||
.collect();
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
fn hash_to_id(data: &[u8]) -> String {
|
||||
let hash: u64 = data
|
||||
.iter()
|
||||
.fold(0u64, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as u64));
|
||||
format!("qudag_{:016x}", hash)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IdentityError {
|
||||
#[error("Key generation failed")]
|
||||
KeyGenerationFailed,
|
||||
#[error("Signing failed")]
|
||||
SigningFailed,
|
||||
#[error("Verification failed")]
|
||||
VerificationFailed,
|
||||
#[error("Invalid signature")]
|
||||
InvalidSignature,
|
||||
#[error("Invalid public key")]
|
||||
InvalidPublicKey,
|
||||
#[error("Encryption failed")]
|
||||
EncryptionFailed,
|
||||
#[error("Decryption failed")]
|
||||
DecryptionFailed,
|
||||
#[error("Invalid ciphertext")]
|
||||
InvalidCiphertext,
|
||||
}
|
||||
73
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/keystore.rs
vendored
Normal file
73
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/keystore.rs
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Secure Keystore with Zeroization
|
||||
|
||||
use super::identity::QuDagIdentity;
|
||||
use std::collections::HashMap;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub struct SecureKeystore {
|
||||
identities: HashMap<String, QuDagIdentity>,
|
||||
master_key: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
impl SecureKeystore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
identities: HashMap::new(),
|
||||
master_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_master_key(key: [u8; 32]) -> Self {
|
||||
Self {
|
||||
identities: HashMap::new(),
|
||||
master_key: Some(key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_identity(&mut self, identity: QuDagIdentity) {
|
||||
let id = identity.node_id.clone();
|
||||
self.identities.insert(id, identity);
|
||||
}
|
||||
|
||||
pub fn get_identity(&self, node_id: &str) -> Option<&QuDagIdentity> {
|
||||
self.identities.get(node_id)
|
||||
}
|
||||
|
||||
pub fn remove_identity(&mut self, node_id: &str) -> Option<QuDagIdentity> {
|
||||
self.identities.remove(node_id)
|
||||
}
|
||||
|
||||
pub fn list_identities(&self) -> Vec<&str> {
|
||||
self.identities.keys().map(|s| s.as_str()).collect()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.identities.clear();
|
||||
if let Some(ref mut key) = self.master_key {
|
||||
key.zeroize();
|
||||
}
|
||||
self.master_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SecureKeystore {
|
||||
fn drop(&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SecureKeystore {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum KeystoreError {
|
||||
#[error("Identity not found")]
|
||||
IdentityNotFound,
|
||||
#[error("Keystore locked")]
|
||||
Locked,
|
||||
#[error("Storage error: {0}")]
|
||||
StorageError(String),
|
||||
}
|
||||
239
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/ml_dsa.rs
vendored
Normal file
239
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/ml_dsa.rs
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
//! ML-DSA-65 Digital Signatures
|
||||
//!
|
||||
//! # Security Status
|
||||
//!
|
||||
//! With `production-crypto` feature: Uses `pqcrypto-dilithium` (Dilithium3 ≈ ML-DSA-65)
|
||||
//! Without feature: Uses HMAC-SHA256 placeholder (NOT quantum-resistant)
|
||||
//!
|
||||
//! ## Production Use
|
||||
//!
|
||||
//! Enable the `production-crypto` feature in Cargo.toml:
|
||||
//! ```toml
|
||||
//! ruvector-dag = { version = "0.1", features = ["production-crypto"] }
|
||||
//! ```
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// ML-DSA-65 sizes (FIPS 204)
|
||||
// Note: Dilithium3 is the closest match to ML-DSA-65 security level
|
||||
pub const ML_DSA_65_PUBLIC_KEY_SIZE: usize = 1952;
|
||||
pub const ML_DSA_65_SECRET_KEY_SIZE: usize = 4032;
|
||||
pub const ML_DSA_65_SIGNATURE_SIZE: usize = 3309;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MlDsa65PublicKey(pub [u8; ML_DSA_65_PUBLIC_KEY_SIZE]);
|
||||
|
||||
#[derive(Clone, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct MlDsa65SecretKey(pub [u8; ML_DSA_65_SECRET_KEY_SIZE]);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Signature(pub [u8; ML_DSA_65_SIGNATURE_SIZE]);
|
||||
|
||||
pub struct MlDsa65;
|
||||
|
||||
// ============================================================================
|
||||
// Production Implementation (using pqcrypto-dilithium)
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(feature = "production-crypto")]
|
||||
mod production {
|
||||
use super::*;
|
||||
use pqcrypto_dilithium::dilithium3;
|
||||
use pqcrypto_traits::sign::{DetachedSignature, PublicKey, SecretKey};
|
||||
|
||||
impl MlDsa65 {
|
||||
/// Generate a new signing keypair using real Dilithium3
|
||||
pub fn generate_keypair() -> Result<(MlDsa65PublicKey, MlDsa65SecretKey), DsaError> {
|
||||
let (pk, sk) = dilithium3::keypair();
|
||||
|
||||
let pk_bytes = pk.as_bytes();
|
||||
let sk_bytes = sk.as_bytes();
|
||||
|
||||
// Dilithium3 sizes: pk=1952, sk=4032 (matches ML-DSA-65)
|
||||
let mut pk_arr = [0u8; ML_DSA_65_PUBLIC_KEY_SIZE];
|
||||
let mut sk_arr = [0u8; ML_DSA_65_SECRET_KEY_SIZE];
|
||||
|
||||
if pk_bytes.len() != ML_DSA_65_PUBLIC_KEY_SIZE {
|
||||
return Err(DsaError::InvalidPublicKey);
|
||||
}
|
||||
if sk_bytes.len() != ML_DSA_65_SECRET_KEY_SIZE {
|
||||
return Err(DsaError::SigningFailed);
|
||||
}
|
||||
|
||||
pk_arr.copy_from_slice(pk_bytes);
|
||||
sk_arr.copy_from_slice(sk_bytes);
|
||||
|
||||
Ok((MlDsa65PublicKey(pk_arr), MlDsa65SecretKey(sk_arr)))
|
||||
}
|
||||
|
||||
/// Sign a message using real Dilithium3
|
||||
pub fn sign(sk: &MlDsa65SecretKey, message: &[u8]) -> Result<Signature, DsaError> {
|
||||
let secret_key =
|
||||
dilithium3::SecretKey::from_bytes(&sk.0).map_err(|_| DsaError::InvalidSignature)?;
|
||||
|
||||
let sig = dilithium3::detached_sign(message, &secret_key);
|
||||
let sig_bytes = sig.as_bytes();
|
||||
|
||||
let mut sig_arr = [0u8; ML_DSA_65_SIGNATURE_SIZE];
|
||||
|
||||
// Dilithium3 signature size is 3293, we pad to match ML-DSA-65's 3309
|
||||
let copy_len = sig_bytes.len().min(ML_DSA_65_SIGNATURE_SIZE);
|
||||
sig_arr[..copy_len].copy_from_slice(&sig_bytes[..copy_len]);
|
||||
|
||||
Ok(Signature(sig_arr))
|
||||
}
|
||||
|
||||
/// Verify a signature using real Dilithium3
|
||||
pub fn verify(
|
||||
pk: &MlDsa65PublicKey,
|
||||
message: &[u8],
|
||||
signature: &Signature,
|
||||
) -> Result<bool, DsaError> {
|
||||
let public_key =
|
||||
dilithium3::PublicKey::from_bytes(&pk.0).map_err(|_| DsaError::InvalidPublicKey)?;
|
||||
|
||||
// Dilithium3 signature is 3293 bytes
|
||||
let sig = dilithium3::DetachedSignature::from_bytes(&signature.0[..3293])
|
||||
.map_err(|_| DsaError::InvalidSignature)?;
|
||||
|
||||
match dilithium3::verify_detached_signature(&sig, message, &public_key) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Placeholder Implementation (HMAC-SHA256 - NOT quantum-resistant)
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(not(feature = "production-crypto"))]
|
||||
mod placeholder {
|
||||
use super::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
impl MlDsa65 {
|
||||
/// Generate a new signing keypair (PLACEHOLDER)
|
||||
///
|
||||
/// # Security Warning
|
||||
/// This is a placeholder using random bytes, NOT real ML-DSA.
|
||||
pub fn generate_keypair() -> Result<(MlDsa65PublicKey, MlDsa65SecretKey), DsaError> {
|
||||
let mut pk = [0u8; ML_DSA_65_PUBLIC_KEY_SIZE];
|
||||
let mut sk = [0u8; ML_DSA_65_SECRET_KEY_SIZE];
|
||||
|
||||
getrandom::getrandom(&mut pk).map_err(|_| DsaError::RngFailed)?;
|
||||
getrandom::getrandom(&mut sk).map_err(|_| DsaError::RngFailed)?;
|
||||
|
||||
Ok((MlDsa65PublicKey(pk), MlDsa65SecretKey(sk)))
|
||||
}
|
||||
|
||||
/// Sign a message (PLACEHOLDER)
|
||||
///
|
||||
/// # Security Warning
|
||||
/// This is a placeholder using HMAC-SHA256, NOT real ML-DSA.
|
||||
/// Provides basic integrity but NO quantum resistance.
|
||||
pub fn sign(sk: &MlDsa65SecretKey, message: &[u8]) -> Result<Signature, DsaError> {
|
||||
let mut sig = [0u8; ML_DSA_65_SIGNATURE_SIZE];
|
||||
|
||||
let hmac = Self::hmac_sha256(&sk.0[..32], message);
|
||||
|
||||
for i in 0..ML_DSA_65_SIGNATURE_SIZE {
|
||||
sig[i] = hmac[i % 32];
|
||||
}
|
||||
|
||||
let key_hash = Self::sha256(&sk.0[32..64]);
|
||||
for i in 0..32 {
|
||||
sig[i + 32] = key_hash[i];
|
||||
}
|
||||
|
||||
Ok(Signature(sig))
|
||||
}
|
||||
|
||||
/// Verify a signature (PLACEHOLDER)
|
||||
///
|
||||
/// # Security Warning
|
||||
/// This is a placeholder using HMAC-SHA256, NOT real ML-DSA.
|
||||
pub fn verify(
|
||||
pk: &MlDsa65PublicKey,
|
||||
message: &[u8],
|
||||
signature: &Signature,
|
||||
) -> Result<bool, DsaError> {
|
||||
let expected_key_hash = Self::sha256(&pk.0[..32]);
|
||||
let sig_key_hash = &signature.0[32..64];
|
||||
|
||||
if sig_key_hash != expected_key_hash.as_slice() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let msg_hash = Self::sha256(message);
|
||||
let sig_structure_valid = signature.0[..32]
|
||||
.iter()
|
||||
.zip(msg_hash.iter().cycle())
|
||||
.all(|(s, h)| *s != 0 || *h == 0);
|
||||
|
||||
Ok(sig_structure_valid)
|
||||
}
|
||||
|
||||
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
|
||||
const BLOCK_SIZE: usize = 64;
|
||||
|
||||
let mut key_block = [0u8; BLOCK_SIZE];
|
||||
if key.len() > BLOCK_SIZE {
|
||||
let hash = Self::sha256(key);
|
||||
key_block[..32].copy_from_slice(&hash);
|
||||
} else {
|
||||
key_block[..key.len()].copy_from_slice(key);
|
||||
}
|
||||
|
||||
let mut ipad = [0x36u8; BLOCK_SIZE];
|
||||
for (i, k) in key_block.iter().enumerate() {
|
||||
ipad[i] ^= k;
|
||||
}
|
||||
|
||||
let mut opad = [0x5cu8; BLOCK_SIZE];
|
||||
for (i, k) in key_block.iter().enumerate() {
|
||||
opad[i] ^= k;
|
||||
}
|
||||
|
||||
let mut inner = Vec::with_capacity(BLOCK_SIZE + message.len());
|
||||
inner.extend_from_slice(&ipad);
|
||||
inner.extend_from_slice(message);
|
||||
let inner_hash = Self::sha256(&inner);
|
||||
|
||||
let mut outer = Vec::with_capacity(BLOCK_SIZE + 32);
|
||||
outer.extend_from_slice(&opad);
|
||||
outer.extend_from_slice(&inner_hash);
|
||||
Self::sha256(&outer)
|
||||
}
|
||||
|
||||
fn sha256(data: &[u8]) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
let result = hasher.finalize();
|
||||
let mut output = [0u8; 32];
|
||||
output.copy_from_slice(&result);
|
||||
output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DsaError {
|
||||
#[error("Random number generation failed")]
|
||||
RngFailed,
|
||||
#[error("Invalid public key")]
|
||||
InvalidPublicKey,
|
||||
#[error("Invalid signature")]
|
||||
InvalidSignature,
|
||||
#[error("Signing failed")]
|
||||
SigningFailed,
|
||||
#[error("Verification failed")]
|
||||
VerificationFailed,
|
||||
}
|
||||
|
||||
/// Check if using production cryptography
|
||||
pub fn is_production() -> bool {
|
||||
cfg!(feature = "production-crypto")
|
||||
}
|
||||
268
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/ml_kem.rs
vendored
Normal file
268
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/ml_kem.rs
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
//! ML-KEM-768 Key Encapsulation Mechanism
|
||||
//!
|
||||
//! # Security Status
|
||||
//!
|
||||
//! With `production-crypto` feature: Uses `pqcrypto-kyber` (Kyber768 ≈ ML-KEM-768)
|
||||
//! Without feature: Uses HKDF-SHA256 placeholder (NOT quantum-resistant)
|
||||
//!
|
||||
//! ## Production Use
|
||||
//!
|
||||
//! Enable the `production-crypto` feature in Cargo.toml:
|
||||
//! ```toml
|
||||
//! ruvector-dag = { version = "0.1", features = ["production-crypto"] }
|
||||
//! ```
|
||||
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// ML-KEM-768 sizes (FIPS 203)
|
||||
// Note: Kyber768 is the closest match to ML-KEM-768 security level
|
||||
pub const ML_KEM_768_PUBLIC_KEY_SIZE: usize = 1184;
|
||||
pub const ML_KEM_768_SECRET_KEY_SIZE: usize = 2400;
|
||||
pub const ML_KEM_768_CIPHERTEXT_SIZE: usize = 1088;
|
||||
pub const SHARED_SECRET_SIZE: usize = 32;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MlKem768PublicKey(pub [u8; ML_KEM_768_PUBLIC_KEY_SIZE]);
|
||||
|
||||
#[derive(Clone, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct MlKem768SecretKey(pub [u8; ML_KEM_768_SECRET_KEY_SIZE]);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EncapsulatedKey {
|
||||
pub ciphertext: [u8; ML_KEM_768_CIPHERTEXT_SIZE],
|
||||
pub shared_secret: [u8; SHARED_SECRET_SIZE],
|
||||
}
|
||||
|
||||
pub struct MlKem768;
|
||||
|
||||
// ============================================================================
|
||||
// Production Implementation (using pqcrypto-kyber)
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(feature = "production-crypto")]
|
||||
mod production {
|
||||
use super::*;
|
||||
use pqcrypto_kyber::kyber768;
|
||||
use pqcrypto_traits::kem::{Ciphertext, PublicKey, SecretKey, SharedSecret};
|
||||
|
||||
impl MlKem768 {
|
||||
/// Generate a new keypair using real Kyber768
|
||||
pub fn generate_keypair() -> Result<(MlKem768PublicKey, MlKem768SecretKey), KemError> {
|
||||
let (pk, sk) = kyber768::keypair();
|
||||
|
||||
let pk_bytes = pk.as_bytes();
|
||||
let sk_bytes = sk.as_bytes();
|
||||
|
||||
// Kyber768 sizes: pk=1184, sk=2400 (matches ML-KEM-768)
|
||||
let mut pk_arr = [0u8; ML_KEM_768_PUBLIC_KEY_SIZE];
|
||||
let mut sk_arr = [0u8; ML_KEM_768_SECRET_KEY_SIZE];
|
||||
|
||||
if pk_bytes.len() != ML_KEM_768_PUBLIC_KEY_SIZE {
|
||||
return Err(KemError::InvalidPublicKey);
|
||||
}
|
||||
if sk_bytes.len() != ML_KEM_768_SECRET_KEY_SIZE {
|
||||
return Err(KemError::DecapsulationFailed);
|
||||
}
|
||||
|
||||
pk_arr.copy_from_slice(pk_bytes);
|
||||
sk_arr.copy_from_slice(sk_bytes);
|
||||
|
||||
Ok((MlKem768PublicKey(pk_arr), MlKem768SecretKey(sk_arr)))
|
||||
}
|
||||
|
||||
/// Encapsulate a shared secret using real Kyber768
|
||||
pub fn encapsulate(pk: &MlKem768PublicKey) -> Result<EncapsulatedKey, KemError> {
|
||||
let public_key =
|
||||
kyber768::PublicKey::from_bytes(&pk.0).map_err(|_| KemError::InvalidPublicKey)?;
|
||||
|
||||
let (ss, ct) = kyber768::encapsulate(&public_key);
|
||||
|
||||
let ss_bytes = ss.as_bytes();
|
||||
let ct_bytes = ct.as_bytes();
|
||||
|
||||
let mut shared_secret = [0u8; SHARED_SECRET_SIZE];
|
||||
let mut ciphertext = [0u8; ML_KEM_768_CIPHERTEXT_SIZE];
|
||||
|
||||
if ss_bytes.len() != SHARED_SECRET_SIZE {
|
||||
return Err(KemError::DecapsulationFailed);
|
||||
}
|
||||
if ct_bytes.len() != ML_KEM_768_CIPHERTEXT_SIZE {
|
||||
return Err(KemError::InvalidCiphertext);
|
||||
}
|
||||
|
||||
shared_secret.copy_from_slice(ss_bytes);
|
||||
ciphertext.copy_from_slice(ct_bytes);
|
||||
|
||||
Ok(EncapsulatedKey {
|
||||
ciphertext,
|
||||
shared_secret,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decapsulate to recover the shared secret using real Kyber768
|
||||
pub fn decapsulate(
|
||||
sk: &MlKem768SecretKey,
|
||||
ciphertext: &[u8; ML_KEM_768_CIPHERTEXT_SIZE],
|
||||
) -> Result<[u8; SHARED_SECRET_SIZE], KemError> {
|
||||
let secret_key = kyber768::SecretKey::from_bytes(&sk.0)
|
||||
.map_err(|_| KemError::DecapsulationFailed)?;
|
||||
|
||||
let ct = kyber768::Ciphertext::from_bytes(ciphertext)
|
||||
.map_err(|_| KemError::InvalidCiphertext)?;
|
||||
|
||||
let ss = kyber768::decapsulate(&ct, &secret_key);
|
||||
let ss_bytes = ss.as_bytes();
|
||||
|
||||
let mut shared_secret = [0u8; SHARED_SECRET_SIZE];
|
||||
if ss_bytes.len() != SHARED_SECRET_SIZE {
|
||||
return Err(KemError::DecapsulationFailed);
|
||||
}
|
||||
|
||||
shared_secret.copy_from_slice(ss_bytes);
|
||||
Ok(shared_secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Placeholder Implementation (HKDF-SHA256 - NOT quantum-resistant)
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(not(feature = "production-crypto"))]
|
||||
mod placeholder {
|
||||
use super::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
impl MlKem768 {
|
||||
/// Generate a new keypair (PLACEHOLDER)
|
||||
///
|
||||
/// # Security Warning
|
||||
/// This is a placeholder using random bytes, NOT real ML-KEM.
|
||||
pub fn generate_keypair() -> Result<(MlKem768PublicKey, MlKem768SecretKey), KemError> {
|
||||
let mut pk = [0u8; ML_KEM_768_PUBLIC_KEY_SIZE];
|
||||
let mut sk = [0u8; ML_KEM_768_SECRET_KEY_SIZE];
|
||||
|
||||
getrandom::getrandom(&mut pk).map_err(|_| KemError::RngFailed)?;
|
||||
getrandom::getrandom(&mut sk).map_err(|_| KemError::RngFailed)?;
|
||||
|
||||
Ok((MlKem768PublicKey(pk), MlKem768SecretKey(sk)))
|
||||
}
|
||||
|
||||
/// Encapsulate a shared secret (PLACEHOLDER)
|
||||
///
|
||||
/// # Security Warning
|
||||
/// This is a placeholder using HKDF-SHA256, NOT real ML-KEM.
|
||||
pub fn encapsulate(pk: &MlKem768PublicKey) -> Result<EncapsulatedKey, KemError> {
|
||||
let mut ephemeral = [0u8; 32];
|
||||
getrandom::getrandom(&mut ephemeral).map_err(|_| KemError::RngFailed)?;
|
||||
|
||||
let mut ciphertext = [0u8; ML_KEM_768_CIPHERTEXT_SIZE];
|
||||
|
||||
let pk_hash = Self::sha256(&pk.0[..64]);
|
||||
for i in 0..32 {
|
||||
ciphertext[i] = ephemeral[i] ^ pk_hash[i];
|
||||
}
|
||||
|
||||
let padding = Self::sha256(&ephemeral);
|
||||
for i in 32..ML_KEM_768_CIPHERTEXT_SIZE {
|
||||
ciphertext[i] = padding[i % 32];
|
||||
}
|
||||
|
||||
let shared_secret = Self::hkdf_sha256(&ephemeral, &pk.0[..32], b"ml-kem-768-shared");
|
||||
|
||||
Ok(EncapsulatedKey {
|
||||
ciphertext,
|
||||
shared_secret,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decapsulate to recover the shared secret (PLACEHOLDER)
|
||||
///
|
||||
/// # Security Warning
|
||||
/// This is a placeholder using HKDF-SHA256, NOT real ML-KEM.
|
||||
pub fn decapsulate(
|
||||
sk: &MlKem768SecretKey,
|
||||
ciphertext: &[u8; ML_KEM_768_CIPHERTEXT_SIZE],
|
||||
) -> Result<[u8; SHARED_SECRET_SIZE], KemError> {
|
||||
let sk_hash = Self::sha256(&sk.0[..64]);
|
||||
let mut ephemeral = [0u8; 32];
|
||||
for i in 0..32 {
|
||||
ephemeral[i] = ciphertext[i] ^ sk_hash[i];
|
||||
}
|
||||
|
||||
let expected_padding = Self::sha256(&ephemeral);
|
||||
for i in 32..64.min(ML_KEM_768_CIPHERTEXT_SIZE) {
|
||||
if ciphertext[i] != expected_padding[i % 32] {
|
||||
return Err(KemError::InvalidCiphertext);
|
||||
}
|
||||
}
|
||||
|
||||
let shared_secret = Self::hkdf_sha256(&ephemeral, &sk.0[..32], b"ml-kem-768-shared");
|
||||
Ok(shared_secret)
|
||||
}
|
||||
|
||||
fn hkdf_sha256(ikm: &[u8], salt: &[u8], info: &[u8]) -> [u8; SHARED_SECRET_SIZE] {
|
||||
let prk = Self::hmac_sha256(salt, ikm);
|
||||
let mut okm_input = Vec::with_capacity(info.len() + 1);
|
||||
okm_input.extend_from_slice(info);
|
||||
okm_input.push(1);
|
||||
Self::hmac_sha256(&prk, &okm_input)
|
||||
}
|
||||
|
||||
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
|
||||
const BLOCK_SIZE: usize = 64;
|
||||
|
||||
let mut key_block = [0u8; BLOCK_SIZE];
|
||||
if key.len() > BLOCK_SIZE {
|
||||
let hash = Self::sha256(key);
|
||||
key_block[..32].copy_from_slice(&hash);
|
||||
} else {
|
||||
key_block[..key.len()].copy_from_slice(key);
|
||||
}
|
||||
|
||||
let mut ipad = [0x36u8; BLOCK_SIZE];
|
||||
let mut opad = [0x5cu8; BLOCK_SIZE];
|
||||
for i in 0..BLOCK_SIZE {
|
||||
ipad[i] ^= key_block[i];
|
||||
opad[i] ^= key_block[i];
|
||||
}
|
||||
|
||||
let mut inner = Vec::with_capacity(BLOCK_SIZE + message.len());
|
||||
inner.extend_from_slice(&ipad);
|
||||
inner.extend_from_slice(message);
|
||||
let inner_hash = Self::sha256(&inner);
|
||||
|
||||
let mut outer = Vec::with_capacity(BLOCK_SIZE + 32);
|
||||
outer.extend_from_slice(&opad);
|
||||
outer.extend_from_slice(&inner_hash);
|
||||
Self::sha256(&outer)
|
||||
}
|
||||
|
||||
fn sha256(data: &[u8]) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
let result = hasher.finalize();
|
||||
let mut output = [0u8; 32];
|
||||
output.copy_from_slice(&result);
|
||||
output
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum KemError {
|
||||
#[error("Random number generation failed")]
|
||||
RngFailed,
|
||||
#[error("Invalid public key")]
|
||||
InvalidPublicKey,
|
||||
#[error("Invalid ciphertext")]
|
||||
InvalidCiphertext,
|
||||
#[error("Decapsulation failed")]
|
||||
DecapsulationFailed,
|
||||
}
|
||||
|
||||
/// Check if using production cryptography
|
||||
pub fn is_production() -> bool {
|
||||
cfg!(feature = "production-crypto")
|
||||
}
|
||||
43
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/mod.rs
vendored
Normal file
43
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/mod.rs
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
//! Quantum-Resistant Cryptography for QuDAG
|
||||
//!
|
||||
//! # Security Status
|
||||
//!
|
||||
//! | Component | With `production-crypto` | Without Feature |
|
||||
//! |-----------|-------------------------|-----------------|
|
||||
//! | ML-DSA-65 | ✓ Dilithium3 | ✗ HMAC-SHA256 placeholder |
|
||||
//! | ML-KEM-768 | ✓ Kyber768 | ✗ HKDF-SHA256 placeholder |
|
||||
//! | Differential Privacy | ✓ Production | ✓ Production |
|
||||
//! | Keystore | ✓ Uses zeroize | ✓ Uses zeroize |
|
||||
//!
|
||||
//! ## Enabling Production Cryptography
|
||||
//!
|
||||
//! ```toml
|
||||
//! ruvector-dag = { version = "0.1", features = ["production-crypto"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Startup Check
|
||||
//!
|
||||
//! Call [`check_crypto_security()`] at application startup to log security status.
|
||||
|
||||
mod differential_privacy;
|
||||
mod identity;
|
||||
mod keystore;
|
||||
mod ml_dsa;
|
||||
mod ml_kem;
|
||||
mod security_notice;
|
||||
|
||||
pub use differential_privacy::{DifferentialPrivacy, DpConfig};
|
||||
pub use identity::{IdentityError, QuDagIdentity};
|
||||
pub use keystore::{KeystoreError, SecureKeystore};
|
||||
pub use ml_dsa::{
|
||||
is_production as is_ml_dsa_production, DsaError, MlDsa65, MlDsa65PublicKey, MlDsa65SecretKey,
|
||||
Signature, ML_DSA_65_PUBLIC_KEY_SIZE, ML_DSA_65_SECRET_KEY_SIZE, ML_DSA_65_SIGNATURE_SIZE,
|
||||
};
|
||||
pub use ml_kem::{
|
||||
is_production as is_ml_kem_production, EncapsulatedKey, KemError, MlKem768, MlKem768PublicKey,
|
||||
MlKem768SecretKey, ML_KEM_768_CIPHERTEXT_SIZE, ML_KEM_768_PUBLIC_KEY_SIZE,
|
||||
ML_KEM_768_SECRET_KEY_SIZE, SHARED_SECRET_SIZE,
|
||||
};
|
||||
pub use security_notice::{
|
||||
check_crypto_security, is_production_ready, security_status, SecurityStatus,
|
||||
};
|
||||
204
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/security_notice.rs
vendored
Normal file
204
vendor/ruvector/crates/ruvector-dag/src/qudag/crypto/security_notice.rs
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
//! # Security Notice for QuDAG Cryptography
|
||||
//!
|
||||
//! ## Security Status
|
||||
//!
|
||||
//! | Component | With `production-crypto` | Without Feature |
|
||||
//! |-----------|-------------------------|-----------------|
|
||||
//! | ML-DSA-65 | ✓ Dilithium3 (NIST PQC) | ✗ HMAC-SHA256 placeholder |
|
||||
//! | ML-KEM-768 | ✓ Kyber768 (NIST PQC) | ✗ HKDF-SHA256 placeholder |
|
||||
//! | Differential Privacy | ✓ Production-ready | ✓ Production-ready |
|
||||
//! | Keystore | ✓ Uses zeroize | ✓ Uses zeroize |
|
||||
//!
|
||||
//! ## Enabling Production Cryptography
|
||||
//!
|
||||
//! Add to your Cargo.toml:
|
||||
//! ```toml
|
||||
//! ruvector-dag = { version = "0.1", features = ["production-crypto"] }
|
||||
//! ```
|
||||
//!
|
||||
//! ## NIST Post-Quantum Cryptography Standards
|
||||
//!
|
||||
//! - **FIPS 203**: ML-KEM (Module-Lattice Key Encapsulation Mechanism)
|
||||
//! - **FIPS 204**: ML-DSA (Module-Lattice Digital Signature Algorithm)
|
||||
//!
|
||||
//! The `production-crypto` feature uses:
|
||||
//! - `pqcrypto-dilithium` (Dilithium3 ≈ ML-DSA-65 security level)
|
||||
//! - `pqcrypto-kyber` (Kyber768 ≈ ML-KEM-768 security level)
|
||||
//!
|
||||
//! ## Security Contact
|
||||
//!
|
||||
//! Report security issues to: security@ruvector.io
|
||||
|
||||
use super::{ml_dsa, ml_kem};
|
||||
|
||||
/// Check cryptographic security at startup
|
||||
///
|
||||
/// Call this function during application initialization to log
|
||||
/// warnings about placeholder crypto usage.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn main() {
|
||||
/// ruvector_dag::qudag::crypto::check_crypto_security();
|
||||
/// // ... rest of application
|
||||
/// }
|
||||
/// ```
|
||||
#[cold]
|
||||
pub fn check_crypto_security() {
|
||||
let status = security_status();
|
||||
|
||||
if status.production_ready {
|
||||
tracing::info!("✓ QuDAG cryptography: Production mode enabled (Dilithium3 + Kyber768)");
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"⚠️ SECURITY WARNING: Using placeholder cryptography. \
|
||||
NOT suitable for production. Enable 'production-crypto' feature."
|
||||
);
|
||||
tracing::warn!(
|
||||
" ML-DSA: {} | ML-KEM: {}",
|
||||
if status.ml_dsa_ready {
|
||||
"Ready"
|
||||
} else {
|
||||
"PLACEHOLDER"
|
||||
},
|
||||
if status.ml_kem_ready {
|
||||
"Ready"
|
||||
} else {
|
||||
"PLACEHOLDER"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime check for production readiness
|
||||
pub fn is_production_ready() -> bool {
|
||||
ml_dsa::is_production() && ml_kem::is_production()
|
||||
}
|
||||
|
||||
/// Get detailed security status report
|
||||
pub fn security_status() -> SecurityStatus {
|
||||
let ml_dsa_ready = ml_dsa::is_production();
|
||||
let ml_kem_ready = ml_kem::is_production();
|
||||
|
||||
SecurityStatus {
|
||||
ml_dsa_ready,
|
||||
ml_kem_ready,
|
||||
dp_ready: true,
|
||||
keystore_ready: true,
|
||||
production_ready: ml_dsa_ready && ml_kem_ready,
|
||||
}
|
||||
}
|
||||
|
||||
/// Security status of cryptographic components
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SecurityStatus {
|
||||
/// ML-DSA-65 uses real implementation (Dilithium3)
|
||||
pub ml_dsa_ready: bool,
|
||||
/// ML-KEM-768 uses real implementation (Kyber768)
|
||||
pub ml_kem_ready: bool,
|
||||
/// Differential privacy is properly implemented
|
||||
pub dp_ready: bool,
|
||||
/// Keystore uses proper zeroization
|
||||
pub keystore_ready: bool,
|
||||
/// Overall production readiness
|
||||
pub production_ready: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SecurityStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "QuDAG Cryptography Security Status:")?;
|
||||
writeln!(
|
||||
f,
|
||||
" ML-DSA-65: {} ({})",
|
||||
if self.ml_dsa_ready { "✓" } else { "✗" },
|
||||
if self.ml_dsa_ready {
|
||||
"Dilithium3"
|
||||
} else {
|
||||
"PLACEHOLDER"
|
||||
}
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
" ML-KEM-768: {} ({})",
|
||||
if self.ml_kem_ready { "✓" } else { "✗" },
|
||||
if self.ml_kem_ready {
|
||||
"Kyber768"
|
||||
} else {
|
||||
"PLACEHOLDER"
|
||||
}
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
" DP: {} ({})",
|
||||
if self.dp_ready { "✓" } else { "✗" },
|
||||
if self.dp_ready { "Ready" } else { "Not Ready" }
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
" Keystore: {} ({})",
|
||||
if self.keystore_ready { "✓" } else { "✗" },
|
||||
if self.keystore_ready {
|
||||
"Ready"
|
||||
} else {
|
||||
"Not Ready"
|
||||
}
|
||||
)?;
|
||||
writeln!(f)?;
|
||||
writeln!(
|
||||
f,
|
||||
" OVERALL: {}",
|
||||
if self.production_ready {
|
||||
"✓ PRODUCTION READY (Post-Quantum Secure)"
|
||||
} else {
|
||||
"✗ NOT PRODUCTION READY - Enable 'production-crypto' feature"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_security_status() {
|
||||
let status = security_status();
|
||||
// These should always be ready
|
||||
assert!(status.dp_ready);
|
||||
assert!(status.keystore_ready);
|
||||
|
||||
// ML-DSA and ML-KEM depend on feature flag
|
||||
#[cfg(feature = "production-crypto")]
|
||||
{
|
||||
assert!(status.ml_dsa_ready);
|
||||
assert!(status.ml_kem_ready);
|
||||
assert!(status.production_ready);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "production-crypto"))]
|
||||
{
|
||||
assert!(!status.ml_dsa_ready);
|
||||
assert!(!status.ml_kem_ready);
|
||||
assert!(!status.production_ready);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_production_ready() {
|
||||
#[cfg(feature = "production-crypto")]
|
||||
assert!(is_production_ready());
|
||||
|
||||
#[cfg(not(feature = "production-crypto"))]
|
||||
assert!(!is_production_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let status = security_status();
|
||||
let display = format!("{}", status);
|
||||
assert!(display.contains("QuDAG Cryptography Security Status"));
|
||||
assert!(display.contains("ML-DSA-65"));
|
||||
assert!(display.contains("ML-KEM-768"));
|
||||
}
|
||||
}
|
||||
21
vendor/ruvector/crates/ruvector-dag/src/qudag/mod.rs
vendored
Normal file
21
vendor/ruvector/crates/ruvector-dag/src/qudag/mod.rs
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
//! QuDAG Integration - Quantum-Resistant Distributed Pattern Learning
|
||||
|
||||
mod client;
|
||||
mod consensus;
|
||||
pub mod crypto;
|
||||
mod network;
|
||||
mod proposal;
|
||||
mod sync;
|
||||
pub mod tokens;
|
||||
|
||||
pub use client::QuDagClient;
|
||||
pub use consensus::{ConsensusResult, Vote};
|
||||
pub use network::{NetworkConfig, NetworkStatus};
|
||||
pub use proposal::{PatternProposal, ProposalStatus};
|
||||
pub use sync::PatternSync;
|
||||
pub use tokens::{
|
||||
GovernanceError, Proposal as GovProposal, ProposalStatus as GovProposalStatus, ProposalType,
|
||||
VoteChoice,
|
||||
};
|
||||
pub use tokens::{GovernanceSystem, RewardCalculator, StakingManager};
|
||||
pub use tokens::{RewardClaim, RewardSource, StakeInfo, StakingError};
|
||||
48
vendor/ruvector/crates/ruvector-dag/src/qudag/network.rs
vendored
Normal file
48
vendor/ruvector/crates/ruvector-dag/src/qudag/network.rs
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Network Configuration and Status
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkConfig {
|
||||
pub endpoints: Vec<String>,
|
||||
pub min_peers: usize,
|
||||
pub max_peers: usize,
|
||||
pub heartbeat_interval_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for NetworkConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
endpoints: vec!["https://qudag.network:8443".to_string()],
|
||||
min_peers: 3,
|
||||
max_peers: 50,
|
||||
heartbeat_interval_ms: 30000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkStatus {
|
||||
pub connected: bool,
|
||||
pub peer_count: usize,
|
||||
pub latest_round: u64,
|
||||
pub sync_status: SyncStatus,
|
||||
pub network_version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SyncStatus {
|
||||
Synced,
|
||||
Syncing,
|
||||
Behind,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SyncStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SyncStatus::Synced => write!(f, "synced"),
|
||||
SyncStatus::Syncing => write!(f, "syncing"),
|
||||
SyncStatus::Behind => write!(f, "behind"),
|
||||
SyncStatus::Disconnected => write!(f, "disconnected"),
|
||||
}
|
||||
}
|
||||
}
|
||||
70
vendor/ruvector/crates/ruvector-dag/src/qudag/proposal.rs
vendored
Normal file
70
vendor/ruvector/crates/ruvector-dag/src/qudag/proposal.rs
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Pattern Proposal System
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PatternProposal {
|
||||
pub pattern_vector: Vec<f32>,
|
||||
pub metadata: serde_json::Value,
|
||||
pub quality_score: f64,
|
||||
pub noise_epsilon: Option<f64>, // Differential privacy
|
||||
}
|
||||
|
||||
impl PatternProposal {
|
||||
pub fn new(pattern_vector: Vec<f32>, metadata: serde_json::Value, quality_score: f64) -> Self {
|
||||
Self {
|
||||
pattern_vector,
|
||||
metadata,
|
||||
quality_score,
|
||||
noise_epsilon: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_differential_privacy(mut self, epsilon: f64) -> Self {
|
||||
self.noise_epsilon = Some(epsilon);
|
||||
// Add Laplace noise to pattern
|
||||
self.add_laplace_noise(epsilon);
|
||||
self
|
||||
}
|
||||
|
||||
fn add_laplace_noise(&mut self, epsilon: f64) {
|
||||
let scale = 1.0 / epsilon;
|
||||
for v in &mut self.pattern_vector {
|
||||
// Simple approximation of Laplace noise
|
||||
let u: f64 = rand::random::<f64>() - 0.5;
|
||||
let noise = -scale * u.signum() * (1.0 - 2.0 * u.abs()).ln();
|
||||
*v += noise as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ProposalStatus {
|
||||
Pending,
|
||||
Voting,
|
||||
Accepted,
|
||||
Rejected,
|
||||
Finalized,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ProposalStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProposalStatus::Pending => write!(f, "pending"),
|
||||
ProposalStatus::Voting => write!(f, "voting"),
|
||||
ProposalStatus::Accepted => write!(f, "accepted"),
|
||||
ProposalStatus::Rejected => write!(f, "rejected"),
|
||||
ProposalStatus::Finalized => write!(f, "finalized"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProposalResult {
|
||||
pub proposal_id: String,
|
||||
pub status: ProposalStatus,
|
||||
pub votes_for: u64,
|
||||
pub votes_against: u64,
|
||||
pub finalized_at: Option<std::time::SystemTime>,
|
||||
}
|
||||
52
vendor/ruvector/crates/ruvector-dag/src/qudag/sync.rs
vendored
Normal file
52
vendor/ruvector/crates/ruvector-dag/src/qudag/sync.rs
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Pattern Synchronization
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncedPattern {
|
||||
pub id: String,
|
||||
pub pattern_vector: Vec<f32>,
|
||||
pub quality_score: f64,
|
||||
pub source_node: String,
|
||||
pub round_accepted: u64,
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct PatternSync {
|
||||
last_synced_round: u64,
|
||||
pending_patterns: Vec<SyncedPattern>,
|
||||
}
|
||||
|
||||
impl PatternSync {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_synced_round: 0,
|
||||
pending_patterns: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_round(&self) -> u64 {
|
||||
self.last_synced_round
|
||||
}
|
||||
|
||||
pub fn add_pattern(&mut self, pattern: SyncedPattern) {
|
||||
if pattern.round_accepted > self.last_synced_round {
|
||||
self.last_synced_round = pattern.round_accepted;
|
||||
}
|
||||
self.pending_patterns.push(pattern);
|
||||
}
|
||||
|
||||
pub fn drain_pending(&mut self) -> Vec<SyncedPattern> {
|
||||
std::mem::take(&mut self.pending_patterns)
|
||||
}
|
||||
|
||||
pub fn pending_count(&self) -> usize {
|
||||
self.pending_patterns.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PatternSync {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
338
vendor/ruvector/crates/ruvector-dag/src/qudag/tokens/governance.rs
vendored
Normal file
338
vendor/ruvector/crates/ruvector-dag/src/qudag/tokens/governance.rs
vendored
Normal file
@@ -0,0 +1,338 @@
|
||||
//! Governance Voting System
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Proposal {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub proposer: String,
|
||||
pub created_at: std::time::Instant,
|
||||
pub voting_ends: std::time::Duration,
|
||||
pub proposal_type: ProposalType,
|
||||
pub status: ProposalStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ProposalType {
|
||||
ParameterChange,
|
||||
PatternPolicy,
|
||||
RewardAdjustment,
|
||||
ProtocolUpgrade,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ProposalStatus {
|
||||
Active,
|
||||
Passed,
|
||||
Failed,
|
||||
Executed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GovernanceVote {
|
||||
pub voter: String,
|
||||
pub proposal_id: String,
|
||||
pub vote: VoteChoice,
|
||||
pub weight: f64,
|
||||
pub timestamp: std::time::Instant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum VoteChoice {
|
||||
For,
|
||||
Against,
|
||||
Abstain,
|
||||
}
|
||||
|
||||
pub struct GovernanceSystem {
|
||||
proposals: HashMap<String, Proposal>,
|
||||
votes: HashMap<String, Vec<GovernanceVote>>,
|
||||
quorum_threshold: f64, // Minimum participation (e.g., 0.1 = 10%)
|
||||
approval_threshold: f64, // Minimum approval (e.g., 0.67 = 67%)
|
||||
}
|
||||
|
||||
impl GovernanceSystem {
|
||||
pub fn new(quorum_threshold: f64, approval_threshold: f64) -> Self {
|
||||
Self {
|
||||
proposals: HashMap::new(),
|
||||
votes: HashMap::new(),
|
||||
quorum_threshold,
|
||||
approval_threshold,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_proposal(
|
||||
&mut self,
|
||||
title: String,
|
||||
description: String,
|
||||
proposer: String,
|
||||
proposal_type: ProposalType,
|
||||
voting_duration: std::time::Duration,
|
||||
) -> String {
|
||||
let id = format!("prop_{}", rand::random::<u64>());
|
||||
|
||||
let proposal = Proposal {
|
||||
id: id.clone(),
|
||||
title,
|
||||
description,
|
||||
proposer,
|
||||
created_at: std::time::Instant::now(),
|
||||
voting_ends: voting_duration,
|
||||
proposal_type,
|
||||
status: ProposalStatus::Active,
|
||||
};
|
||||
|
||||
self.proposals.insert(id.clone(), proposal);
|
||||
self.votes.insert(id.clone(), Vec::new());
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
pub fn vote(
|
||||
&mut self,
|
||||
voter: String,
|
||||
proposal_id: &str,
|
||||
choice: VoteChoice,
|
||||
stake_weight: f64,
|
||||
) -> Result<(), GovernanceError> {
|
||||
let proposal = self
|
||||
.proposals
|
||||
.get(proposal_id)
|
||||
.ok_or(GovernanceError::ProposalNotFound)?;
|
||||
|
||||
if proposal.status != ProposalStatus::Active {
|
||||
return Err(GovernanceError::ProposalNotActive);
|
||||
}
|
||||
|
||||
if proposal.created_at.elapsed() > proposal.voting_ends {
|
||||
return Err(GovernanceError::VotingEnded);
|
||||
}
|
||||
|
||||
// Check if already voted
|
||||
let votes = self.votes.get_mut(proposal_id).unwrap();
|
||||
if votes.iter().any(|v| v.voter == voter) {
|
||||
return Err(GovernanceError::AlreadyVoted);
|
||||
}
|
||||
|
||||
votes.push(GovernanceVote {
|
||||
voter,
|
||||
proposal_id: proposal_id.to_string(),
|
||||
vote: choice,
|
||||
weight: stake_weight,
|
||||
timestamp: std::time::Instant::now(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tally(&self, proposal_id: &str, total_stake: f64) -> Option<VoteTally> {
|
||||
let votes = self.votes.get(proposal_id)?;
|
||||
|
||||
let mut for_weight = 0.0;
|
||||
let mut against_weight = 0.0;
|
||||
let mut abstain_weight = 0.0;
|
||||
|
||||
for vote in votes {
|
||||
match vote.vote {
|
||||
VoteChoice::For => for_weight += vote.weight,
|
||||
VoteChoice::Against => against_weight += vote.weight,
|
||||
VoteChoice::Abstain => abstain_weight += vote.weight,
|
||||
}
|
||||
}
|
||||
|
||||
let total_voted = for_weight + against_weight + abstain_weight;
|
||||
let participation = total_voted / total_stake;
|
||||
let approval = if for_weight + against_weight > 0.0 {
|
||||
for_weight / (for_weight + against_weight)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let quorum_met = participation >= self.quorum_threshold;
|
||||
let approved = approval >= self.approval_threshold && quorum_met;
|
||||
|
||||
Some(VoteTally {
|
||||
for_weight,
|
||||
against_weight,
|
||||
abstain_weight,
|
||||
participation,
|
||||
approval,
|
||||
quorum_met,
|
||||
approved,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finalize(
|
||||
&mut self,
|
||||
proposal_id: &str,
|
||||
total_stake: f64,
|
||||
) -> Result<ProposalStatus, GovernanceError> {
|
||||
// First, validate the proposal without holding a mutable borrow
|
||||
{
|
||||
let proposal = self
|
||||
.proposals
|
||||
.get(proposal_id)
|
||||
.ok_or(GovernanceError::ProposalNotFound)?;
|
||||
|
||||
if proposal.status != ProposalStatus::Active {
|
||||
return Err(GovernanceError::ProposalNotActive);
|
||||
}
|
||||
|
||||
if proposal.created_at.elapsed() < proposal.voting_ends {
|
||||
return Err(GovernanceError::VotingNotEnded);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate tally (immutable borrow)
|
||||
let tally = self
|
||||
.tally(proposal_id, total_stake)
|
||||
.ok_or(GovernanceError::ProposalNotFound)?;
|
||||
|
||||
let new_status = if tally.approved {
|
||||
ProposalStatus::Passed
|
||||
} else {
|
||||
ProposalStatus::Failed
|
||||
};
|
||||
|
||||
// Now update the status (mutable borrow)
|
||||
let proposal = self.proposals.get_mut(proposal_id).unwrap();
|
||||
proposal.status = new_status;
|
||||
Ok(new_status)
|
||||
}
|
||||
|
||||
pub fn get_proposal(&self, proposal_id: &str) -> Option<&Proposal> {
|
||||
self.proposals.get(proposal_id)
|
||||
}
|
||||
|
||||
pub fn active_proposals(&self) -> Vec<&Proposal> {
|
||||
self.proposals
|
||||
.values()
|
||||
.filter(|p| p.status == ProposalStatus::Active)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VoteTally {
|
||||
pub for_weight: f64,
|
||||
pub against_weight: f64,
|
||||
pub abstain_weight: f64,
|
||||
pub participation: f64,
|
||||
pub approval: f64,
|
||||
pub quorum_met: bool,
|
||||
pub approved: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GovernanceError {
|
||||
#[error("Proposal not found")]
|
||||
ProposalNotFound,
|
||||
#[error("Proposal not active")]
|
||||
ProposalNotActive,
|
||||
#[error("Voting has ended")]
|
||||
VotingEnded,
|
||||
#[error("Voting has not ended")]
|
||||
VotingNotEnded,
|
||||
#[error("Already voted")]
|
||||
AlreadyVoted,
|
||||
#[error("Insufficient stake to propose")]
|
||||
InsufficientStake,
|
||||
}
|
||||
|
||||
impl Default for GovernanceSystem {
|
||||
fn default() -> Self {
|
||||
Self::new(0.1, 0.67) // 10% quorum, 67% approval
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_proposal_creation() {
|
||||
let mut gov = GovernanceSystem::default();
|
||||
let id = gov.create_proposal(
|
||||
"Test".to_string(),
|
||||
"Description".to_string(),
|
||||
"proposer1".to_string(),
|
||||
ProposalType::ParameterChange,
|
||||
Duration::from_secs(86400),
|
||||
);
|
||||
|
||||
let proposal = gov.get_proposal(&id).unwrap();
|
||||
assert_eq!(proposal.title, "Test");
|
||||
assert_eq!(proposal.status, ProposalStatus::Active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_voting() {
|
||||
let mut gov = GovernanceSystem::default();
|
||||
let id = gov.create_proposal(
|
||||
"Test".to_string(),
|
||||
"Description".to_string(),
|
||||
"proposer1".to_string(),
|
||||
ProposalType::ParameterChange,
|
||||
Duration::from_secs(86400),
|
||||
);
|
||||
|
||||
// First vote succeeds
|
||||
assert!(gov
|
||||
.vote("voter1".to_string(), &id, VoteChoice::For, 100.0)
|
||||
.is_ok());
|
||||
|
||||
// Duplicate vote fails
|
||||
assert!(matches!(
|
||||
gov.vote("voter1".to_string(), &id, VoteChoice::For, 50.0),
|
||||
Err(GovernanceError::AlreadyVoted)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tally() {
|
||||
let mut gov = GovernanceSystem::new(0.1, 0.5);
|
||||
let id = gov.create_proposal(
|
||||
"Test".to_string(),
|
||||
"Description".to_string(),
|
||||
"proposer1".to_string(),
|
||||
ProposalType::ParameterChange,
|
||||
Duration::from_secs(86400),
|
||||
);
|
||||
|
||||
gov.vote("voter1".to_string(), &id, VoteChoice::For, 700.0)
|
||||
.unwrap();
|
||||
gov.vote("voter2".to_string(), &id, VoteChoice::Against, 300.0)
|
||||
.unwrap();
|
||||
|
||||
let tally = gov.tally(&id, 10000.0).unwrap();
|
||||
assert_eq!(tally.for_weight, 700.0);
|
||||
assert_eq!(tally.against_weight, 300.0);
|
||||
assert_eq!(tally.participation, 0.1); // 1000/10000
|
||||
assert_eq!(tally.approval, 0.7); // 700/1000
|
||||
assert!(tally.quorum_met);
|
||||
assert!(tally.approved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quorum_not_met() {
|
||||
let mut gov = GovernanceSystem::new(0.5, 0.67);
|
||||
let id = gov.create_proposal(
|
||||
"Test".to_string(),
|
||||
"Description".to_string(),
|
||||
"proposer1".to_string(),
|
||||
ProposalType::ParameterChange,
|
||||
Duration::from_secs(86400),
|
||||
);
|
||||
|
||||
gov.vote("voter1".to_string(), &id, VoteChoice::For, 100.0)
|
||||
.unwrap();
|
||||
|
||||
let tally = gov.tally(&id, 10000.0).unwrap();
|
||||
assert!(!tally.quorum_met); // Only 1% participation
|
||||
assert!(!tally.approved);
|
||||
}
|
||||
}
|
||||
50
vendor/ruvector/crates/ruvector-dag/src/qudag/tokens/mod.rs
vendored
Normal file
50
vendor/ruvector/crates/ruvector-dag/src/qudag/tokens/mod.rs
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
//! rUv Token Integration for QuDAG
|
||||
|
||||
mod governance;
|
||||
mod rewards;
|
||||
mod staking;
|
||||
|
||||
pub use governance::{
|
||||
GovernanceError, GovernanceSystem, GovernanceVote, Proposal, ProposalStatus, ProposalType,
|
||||
VoteChoice,
|
||||
};
|
||||
pub use rewards::{RewardCalculator, RewardClaim, RewardSource};
|
||||
pub use staking::{StakeInfo, StakingError, StakingManager};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_staking_integration() {
|
||||
let mut manager = StakingManager::new(10.0, 1000.0);
|
||||
let stake = manager.stake("node1", 100.0, 30).unwrap();
|
||||
assert_eq!(stake.amount, 100.0);
|
||||
assert_eq!(manager.total_staked(), 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rewards_calculation() {
|
||||
let calculator = RewardCalculator::default();
|
||||
let reward = calculator.pattern_validation_reward(1.0, 0.9);
|
||||
assert!(reward > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_governance_voting() {
|
||||
let mut gov = GovernanceSystem::default();
|
||||
let proposal_id = gov.create_proposal(
|
||||
"Test Proposal".to_string(),
|
||||
"Test Description".to_string(),
|
||||
"proposer1".to_string(),
|
||||
ProposalType::ParameterChange,
|
||||
Duration::from_secs(86400),
|
||||
);
|
||||
|
||||
gov.vote("voter1".to_string(), &proposal_id, VoteChoice::For, 100.0)
|
||||
.unwrap();
|
||||
let tally = gov.tally(&proposal_id, 1000.0).unwrap();
|
||||
assert_eq!(tally.for_weight, 100.0);
|
||||
}
|
||||
}
|
||||
166
vendor/ruvector/crates/ruvector-dag/src/qudag/tokens/rewards.rs
vendored
Normal file
166
vendor/ruvector/crates/ruvector-dag/src/qudag/tokens/rewards.rs
vendored
Normal file
@@ -0,0 +1,166 @@
|
||||
//! Reward Calculation and Distribution
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RewardClaim {
|
||||
pub node_id: String,
|
||||
pub amount: f64,
|
||||
pub source: RewardSource,
|
||||
pub claimed_at: std::time::Instant,
|
||||
pub tx_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum RewardSource {
|
||||
PatternValidation,
|
||||
ConsensusParticipation,
|
||||
PatternContribution,
|
||||
Staking,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RewardSource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RewardSource::PatternValidation => write!(f, "pattern_validation"),
|
||||
RewardSource::ConsensusParticipation => write!(f, "consensus_participation"),
|
||||
RewardSource::PatternContribution => write!(f, "pattern_contribution"),
|
||||
RewardSource::Staking => write!(f, "staking"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RewardCalculator {
|
||||
base_reward: f64,
|
||||
pattern_bonus: f64,
|
||||
staking_apy: f64,
|
||||
pending_rewards: HashMap<String, f64>,
|
||||
}
|
||||
|
||||
impl RewardCalculator {
|
||||
pub fn new(base_reward: f64, pattern_bonus: f64, staking_apy: f64) -> Self {
|
||||
Self {
|
||||
base_reward,
|
||||
pattern_bonus,
|
||||
staking_apy,
|
||||
pending_rewards: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate reward for pattern validation
|
||||
pub fn pattern_validation_reward(&self, stake_weight: f64, pattern_quality: f64) -> f64 {
|
||||
self.base_reward * stake_weight * pattern_quality
|
||||
}
|
||||
|
||||
/// Calculate reward for pattern contribution
|
||||
pub fn pattern_contribution_reward(&self, pattern_quality: f64, usage_count: usize) -> f64 {
|
||||
let usage_factor = (usage_count as f64).ln_1p();
|
||||
self.pattern_bonus * pattern_quality * usage_factor
|
||||
}
|
||||
|
||||
/// Calculate staking reward for a period
|
||||
pub fn staking_reward(&self, stake_amount: f64, days: f64) -> f64 {
|
||||
// Daily rate from APY
|
||||
let daily_rate = (1.0 + self.staking_apy).powf(1.0 / 365.0) - 1.0;
|
||||
stake_amount * daily_rate * days
|
||||
}
|
||||
|
||||
/// Add pending reward
|
||||
pub fn add_pending(&mut self, node_id: &str, amount: f64, _source: RewardSource) {
|
||||
*self
|
||||
.pending_rewards
|
||||
.entry(node_id.to_string())
|
||||
.or_insert(0.0) += amount;
|
||||
}
|
||||
|
||||
/// Get pending rewards for a node
|
||||
pub fn pending_rewards(&self, node_id: &str) -> f64 {
|
||||
self.pending_rewards.get(node_id).copied().unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Claim rewards
|
||||
pub fn claim(&mut self, node_id: &str) -> Option<RewardClaim> {
|
||||
let amount = self.pending_rewards.remove(node_id)?;
|
||||
|
||||
if amount <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(RewardClaim {
|
||||
node_id: node_id.to_string(),
|
||||
amount,
|
||||
source: RewardSource::Staking, // Simplified
|
||||
claimed_at: std::time::Instant::now(),
|
||||
tx_hash: format!("reward_tx_{}", rand::random::<u64>()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get total pending rewards across all nodes
|
||||
pub fn total_pending(&self) -> f64 {
|
||||
self.pending_rewards.values().sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RewardCalculator {
|
||||
fn default() -> Self {
|
||||
Self::new(
|
||||
1.0, // base_reward
|
||||
10.0, // pattern_bonus
|
||||
0.05, // 5% APY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pattern_validation_reward() {
|
||||
let calc = RewardCalculator::default();
|
||||
let reward = calc.pattern_validation_reward(1.0, 0.9);
|
||||
assert_eq!(reward, 0.9); // 1.0 * 1.0 * 0.9
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_contribution_reward() {
|
||||
let calc = RewardCalculator::default();
|
||||
let reward = calc.pattern_contribution_reward(1.0, 100);
|
||||
assert!(reward > 0.0);
|
||||
// Higher usage should give more reward
|
||||
let higher = calc.pattern_contribution_reward(1.0, 1000);
|
||||
assert!(higher > reward);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_staking_reward() {
|
||||
let calc = RewardCalculator::default();
|
||||
let reward = calc.staking_reward(100.0, 365.0);
|
||||
// With 5% APY, should be close to 5.0
|
||||
assert!(reward > 4.8 && reward < 5.2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pending_rewards() {
|
||||
let mut calc = RewardCalculator::default();
|
||||
|
||||
calc.add_pending("node1", 5.0, RewardSource::Staking);
|
||||
calc.add_pending("node1", 3.0, RewardSource::PatternValidation);
|
||||
|
||||
assert_eq!(calc.pending_rewards("node1"), 8.0);
|
||||
assert_eq!(calc.total_pending(), 8.0);
|
||||
|
||||
let claim = calc.claim("node1").unwrap();
|
||||
assert_eq!(claim.amount, 8.0);
|
||||
assert_eq!(calc.pending_rewards("node1"), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reward_source_display() {
|
||||
assert_eq!(RewardSource::Staking.to_string(), "staking");
|
||||
assert_eq!(
|
||||
RewardSource::PatternValidation.to_string(),
|
||||
"pattern_validation"
|
||||
);
|
||||
}
|
||||
}
|
||||
188
vendor/ruvector/crates/ruvector-dag/src/qudag/tokens/staking.rs
vendored
Normal file
188
vendor/ruvector/crates/ruvector-dag/src/qudag/tokens/staking.rs
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
//! Token Staking for Pattern Validation
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StakeInfo {
|
||||
pub amount: f64,
|
||||
pub staked_at: Instant,
|
||||
pub lock_duration: Duration,
|
||||
pub validator_weight: f64,
|
||||
}
|
||||
|
||||
impl StakeInfo {
|
||||
pub fn new(amount: f64, lock_days: u64) -> Self {
|
||||
let lock_duration = Duration::from_secs(lock_days * 24 * 3600);
|
||||
|
||||
// Weight increases with lock duration
|
||||
let weight_multiplier = 1.0 + (lock_days as f64 / 365.0);
|
||||
|
||||
Self {
|
||||
amount,
|
||||
staked_at: Instant::now(),
|
||||
lock_duration,
|
||||
validator_weight: amount * weight_multiplier,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_locked(&self) -> bool {
|
||||
self.staked_at.elapsed() < self.lock_duration
|
||||
}
|
||||
|
||||
pub fn time_remaining(&self) -> Duration {
|
||||
if self.is_locked() {
|
||||
self.lock_duration - self.staked_at.elapsed()
|
||||
} else {
|
||||
Duration::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_unstake(&self) -> bool {
|
||||
!self.is_locked()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StakingManager {
|
||||
stakes: HashMap<String, StakeInfo>,
|
||||
total_staked: f64,
|
||||
min_stake: f64,
|
||||
max_stake: f64,
|
||||
}
|
||||
|
||||
impl StakingManager {
|
||||
pub fn new(min_stake: f64, max_stake: f64) -> Self {
|
||||
Self {
|
||||
stakes: HashMap::new(),
|
||||
total_staked: 0.0,
|
||||
min_stake,
|
||||
max_stake,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stake(
|
||||
&mut self,
|
||||
node_id: &str,
|
||||
amount: f64,
|
||||
lock_days: u64,
|
||||
) -> Result<StakeInfo, StakingError> {
|
||||
if amount < self.min_stake {
|
||||
return Err(StakingError::BelowMinimum(self.min_stake));
|
||||
}
|
||||
|
||||
if amount > self.max_stake {
|
||||
return Err(StakingError::AboveMaximum(self.max_stake));
|
||||
}
|
||||
|
||||
if self.stakes.contains_key(node_id) {
|
||||
return Err(StakingError::AlreadyStaked);
|
||||
}
|
||||
|
||||
let stake = StakeInfo::new(amount, lock_days);
|
||||
self.total_staked += amount;
|
||||
self.stakes.insert(node_id.to_string(), stake.clone());
|
||||
|
||||
Ok(stake)
|
||||
}
|
||||
|
||||
pub fn unstake(&mut self, node_id: &str) -> Result<f64, StakingError> {
|
||||
let stake = self.stakes.get(node_id).ok_or(StakingError::NotStaked)?;
|
||||
|
||||
if stake.is_locked() {
|
||||
return Err(StakingError::StillLocked(stake.time_remaining()));
|
||||
}
|
||||
|
||||
let amount = stake.amount;
|
||||
self.total_staked -= amount;
|
||||
self.stakes.remove(node_id);
|
||||
|
||||
Ok(amount)
|
||||
}
|
||||
|
||||
pub fn get_stake(&self, node_id: &str) -> Option<&StakeInfo> {
|
||||
self.stakes.get(node_id)
|
||||
}
|
||||
|
||||
pub fn total_staked(&self) -> f64 {
|
||||
self.total_staked
|
||||
}
|
||||
|
||||
pub fn validator_weight(&self, node_id: &str) -> f64 {
|
||||
self.stakes
|
||||
.get(node_id)
|
||||
.map(|s| s.validator_weight)
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn relative_weight(&self, node_id: &str) -> f64 {
|
||||
if self.total_staked == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.validator_weight(node_id) / self.total_staked
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum StakingError {
|
||||
#[error("Amount below minimum stake of {0}")]
|
||||
BelowMinimum(f64),
|
||||
#[error("Amount above maximum stake of {0}")]
|
||||
AboveMaximum(f64),
|
||||
#[error("Already staked")]
|
||||
AlreadyStaked,
|
||||
#[error("Not staked")]
|
||||
NotStaked,
|
||||
#[error("Stake still locked for {0:?}")]
|
||||
StillLocked(Duration),
|
||||
#[error("Insufficient balance")]
|
||||
InsufficientBalance,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_stake_creation() {
|
||||
let stake = StakeInfo::new(100.0, 30);
|
||||
assert_eq!(stake.amount, 100.0);
|
||||
assert!(stake.validator_weight > 100.0); // Has weight multiplier
|
||||
assert!(stake.is_locked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_staking_manager() {
|
||||
let mut manager = StakingManager::new(10.0, 1000.0);
|
||||
|
||||
// Test successful stake
|
||||
let result = manager.stake("node1", 100.0, 30);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(manager.total_staked(), 100.0);
|
||||
|
||||
// Test duplicate stake
|
||||
let duplicate = manager.stake("node1", 50.0, 30);
|
||||
assert!(duplicate.is_err());
|
||||
|
||||
// Test below minimum
|
||||
let too_low = manager.stake("node2", 5.0, 30);
|
||||
assert!(matches!(too_low, Err(StakingError::BelowMinimum(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validator_weight() {
|
||||
let mut manager = StakingManager::new(10.0, 1000.0);
|
||||
manager.stake("node1", 100.0, 365).unwrap();
|
||||
|
||||
let weight = manager.validator_weight("node1");
|
||||
assert!(weight > 100.0);
|
||||
assert!(weight <= 200.0); // Max 2x multiplier for 1 year
|
||||
|
||||
// relative_weight = validator_weight / total_staked
|
||||
// With only one staker, this equals validator_weight / amount
|
||||
// Since validator_weight > amount (due to lock multiplier),
|
||||
// relative weight will be > 1.0
|
||||
let relative = manager.relative_weight("node1");
|
||||
assert!(relative > 0.0);
|
||||
assert!(relative <= 2.0); // Max 2x due to lock multiplier
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user