Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View 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,
}

View 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(),
})
}
}

View 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)
}
}

View 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,
}

View 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),
}

View 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")
}

View 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")
}

View 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,
};

View 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"));
}
}

View 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};

View 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"),
}
}
}

View 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>,
}

View 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()
}
}

View 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);
}
}

View 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);
}
}

View 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"
);
}
}

View 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
}
}