diff --git a/Cargo.toml b/Cargo.toml index f87ced2..09432e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,4 +100,16 @@ path = "src/bin/process_payouts.rs" [[bin]] name = "verify_commitments" -path = "src/bin/verify_commitments.rs" \ No newline at end of file +path = "src/bin/verify_commitments.rs" + +[[bin]] +name = "encode_vector" +path = "src/bin/encode_vector.rs" + +[[bin]] +name = "decode_vector" +path = "src/bin/decode_vector.rs" + +[[bin]] +name = "generate_enhanced_commitment" +path = "src/bin/generate_enhanced_commitment.rs" \ No newline at end of file diff --git a/STEGANOGRAPHY_PROOF_OF_WORK.md b/STEGANOGRAPHY_PROOF_OF_WORK.md new file mode 100644 index 0000000..f18a9c2 --- /dev/null +++ b/STEGANOGRAPHY_PROOF_OF_WORK.md @@ -0,0 +1,289 @@ +# RealMir Steganography & Proof of Work System + +This document describes the enhanced commitment system with CLIP vector steganography and proof of work for spam prevention, as discussed in the team Slack thread. + +## Overview + +The enhanced system prevents spam and requires actual work from both miners and validators by: + +1. **CLIP Vector Commitments**: Miners must provide CLIP vectors of their predictions +2. **Steganography**: CLIP vectors are embedded in images using LSB encoding +3. **Proof of Work**: Both commitment and reveal phases require computational work +4. **Enhanced Verification**: Validators can verify both text predictions and CLIP vectors + +## Architecture + +### Commitment Phase +``` +hash = sha256(plaintext_prediction || salt || clip_vector) +``` + +### Reveal Phase +- Post plaintext prediction + salt (as before) +- Post image with embedded CLIP vector using steganography +- Optional: Include proof of work nonce for extra verification + +## CLI Tools + +### 1. Generate Enhanced Commitment + +Generate a commitment with CLIP vector and proof of work: + +```bash +cargo run --bin generate_enhanced_commitment -- \ + --prediction "The cat will be orange" \ + --round-id "round_001" \ + --difficulty 4 \ + --output-json commitment.json +``` + +Options: +- `--prediction`: Your prediction text +- `--round-id`: Round identifier +- `--salt`: Optional salt (generated if not provided) +- `--difficulty`: Proof of work difficulty (1-10, default: 4) +- `--mock`: Use mock embedder for testing +- `--output-json`: Save commitment data to JSON file +- `--verbose`: Show detailed output +- `--skip-pow`: Skip proof of work (for testing) + +### 2. Encode Vector in Image + +Embed a CLIP vector into an image using steganography: + +```bash +cargo run --bin encode_vector -- \ + --input source_image.png \ + --output encoded_image.png \ + --text "The cat will be orange" \ + --salt "your_salt_here" \ + --round-id "round_001" \ + --create-test-image +``` + +Options: +- `--input`: Source image path +- `--output`: Output image path +- `--text`: Text to generate CLIP vector for +- `--salt`: Salt used in commitment +- `--round-id`: Round identifier +- `--mock`: Use mock embedder for testing +- `--bits-per-channel`: Steganography bits per channel (1-3 recommended) +- `--create-test-image`: Create test image if input doesn't exist +- `--image-size`: Test image dimensions (default: 512) + +### 3. Decode Vector from Image + +Extract and verify CLIP vector from an image: + +```bash +cargo run --bin decode_vector -- \ + --input encoded_image.png \ + --text "The cat will be orange" \ + --salt "your_salt_here" \ + --commitment "commitment_hash_here" \ + --output-json extracted_data.json \ + --verbose +``` + +Options: +- `--input`: Image containing embedded vector +- `--text`: Text to verify against (optional) +- `--salt`: Salt for commitment verification (optional) +- `--commitment`: Commitment hash to verify against (optional) +- `--mock`: Use mock embedder for testing +- `--bits-per-channel`: Steganography bits per channel (must match encoding) +- `--output-json`: Save extracted data to JSON +- `--verbose`: Show detailed vector information + +## Workflow Example + +### Step 1: Generate Enhanced Commitment + +```bash +# Generate commitment with proof of work +cargo run --bin generate_enhanced_commitment -- \ + --prediction "A fluffy orange cat" \ + --round-id "round_123" \ + --difficulty 4 \ + --output-json my_commitment.json + +# Output: +# Commitment Hash: a1b2c3d4e5f6... +# Salt: f7e8d9c0b1a2... +``` + +### Step 2: Post Commitment to Twitter + +``` +Commit: a1b2c3d4e5f6... +Wallet: 0xYourWalletAddress +``` + +### Step 3: Prepare Reveal Image + +```bash +# Embed CLIP vector in image +cargo run --bin encode_vector -- \ + --input my_photo.jpg \ + --output reveal_image.png \ + --text "A fluffy orange cat" \ + --salt "f7e8d9c0b1a2..." \ + --round-id "round_123" \ + --create-test-image +``` + +### Step 4: Post Reveal to Twitter + +``` +Prediction: A fluffy orange cat +Salt: f7e8d9c0b1a2... +[Attach reveal_image.png] +``` + +### Step 5: Validators Verify + +```bash +# Validators can extract and verify +cargo run --bin decode_vector -- \ + --input reveal_image.png \ + --text "A fluffy orange cat" \ + --salt "f7e8d9c0b1a2..." \ + --commitment "a1b2c3d4e5f6..." \ + --verbose +``` + +## Technical Details + +### Steganography + +- Uses LSB (Least Significant Bit) encoding +- Embeds data in RGB channels of PNG images +- Configurable bits per channel (1-3 recommended for stealth) +- Includes metadata: version, dimension, salt, round ID +- Magic header "RMCLIP" for data identification + +### Proof of Work + +- Hashcash-style proof of work using SHA-256 +- Configurable difficulty (number of leading zeros) +- Includes challenge string with prediction, salt, and round ID +- Prevents spam by requiring computational work +- Timeout protection (30 seconds default) + +### CLIP Vector Format + +- 512 or 768 dimensional vectors (depending on model) +- f64 precision for accuracy +- Normalized vectors for consistency +- Supports both real CLIP models and mock embedders for testing + +## Security Considerations + +### Attack Vectors & Mitigations + +1. **Copy-paste vectors**: Mitigated by including salt in commitment +2. **Fake vectors**: Prevented by commitment scheme tying everything together +3. **Spam attacks**: Mitigated by proof of work requirement +4. **Vector reuse**: Prevented by per-round salts + +### Recommended Settings + +- **Difficulty**: 4-6 for production (balances security vs. UX) +- **Bits per channel**: 2 (good balance of capacity vs. detectability) +- **Image size**: 512x512 minimum for 512-dimensional vectors +- **Salt length**: 32 bytes (default) + +## Integration with Existing System + +The enhanced system is backward compatible: + +```rust +// Basic commitment (existing) +let basic_commitment = generator.generate("prediction", "salt")?; + +// Enhanced commitment (new) +let enhanced_commitment = enhanced_generator.generate_enhanced( + "prediction", + "salt", + &clip_vector, + "round_id" +)?; +``` + +## Development and Testing + +### Mock Mode + +Use `--mock` flag to use deterministic mock embedders for testing: + +```bash +cargo run --bin generate_enhanced_commitment -- \ + --prediction "Test prediction" \ + --round-id "test" \ + --mock \ + --skip-pow +``` + +### Test Suite + +Run comprehensive tests: + +```bash +cargo test steganography +cargo test proof_of_work +cargo test commitment::enhanced +``` + +## Performance Considerations + +### Proof of Work Times + +- Difficulty 1: ~10ms +- Difficulty 4: ~1-10 seconds +- Difficulty 6: ~1-5 minutes +- Difficulty 8: ~10-30 minutes + +### Image Processing + +- Encoding: ~100-500ms for 512x512 image +- Decoding: ~50-200ms for 512x512 image +- CLIP vector generation: ~1-3 seconds + +### Storage Requirements + +- 512-dimensional vector: ~4KB +- Metadata: ~200 bytes +- Total overhead: ~4.2KB minimum +- 512x512 image capacity (2 bits/channel): ~96KB + +## Troubleshooting + +### Common Issues + +1. **"Insufficient capacity"**: Use larger image or fewer bits per channel +2. **"No embedded data found"**: Check bits per channel setting matches encoding +3. **"CLIP model not found"**: Use `--mock` flag or install CLIP model +4. **"Proof of work timeout"**: Reduce difficulty or increase timeout + +### Debugging + +Enable verbose output for detailed information: + +```bash +cargo run --bin decode_vector -- --input image.png --verbose +``` + +## Contributing + +When adding new features: + +1. Update relevant error types in `src/error.rs` +2. Add comprehensive tests +3. Update CLI help text +4. Consider backward compatibility +5. Update this documentation + +## License + +This implementation is part of the RealMir project and follows the same license terms. \ No newline at end of file diff --git a/src/bin/decode_vector.rs b/src/bin/decode_vector.rs new file mode 100644 index 0000000..d30034e --- /dev/null +++ b/src/bin/decode_vector.rs @@ -0,0 +1,186 @@ +//! CLI tool for decoding CLIP vectors from images using steganography +//! +//! This tool allows validators and other users to extract and verify CLIP vectors +//! from images posted during the reveal phase. + +use clap::Parser; +use realmir_core::{ + steganography::{VectorSteganographer}, + embedder::{EmbedderTrait, MockEmbedder, ClipEmbedder, cosine_similarity}, + commitment::{EnhancedCommitmentGenerator}, + error::Result, +}; +use ndarray::Array1; +use colored::*; +use serde_json; + +#[derive(Parser)] +#[command(name = "decode-vector")] +#[command(about = "Decode and verify CLIP vector from an image")] +#[command(version = "1.0")] +struct Args { + /// Input image path containing embedded vector + #[arg(short, long)] + input: String, + + /// Optional: verify against this text + #[arg(short, long)] + text: Option, + + /// Optional: salt for commitment verification + #[arg(short, long)] + salt: Option, + + /// Optional: commitment hash to verify against + #[arg(short, long)] + commitment: Option, + + /// Use mock embedder instead of real CLIP (for testing) + #[arg(long, default_value = "false")] + mock: bool, + + /// Bits per channel for steganography (should match encoding) + #[arg(long, default_value = "2")] + bits_per_channel: u8, + + /// Output extracted vector to JSON file + #[arg(long)] + output_json: Option, + + /// Show detailed vector information + #[arg(long, default_value = "false")] + verbose: bool, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + println!("{}", "šŸ” RealMir Vector Decoder".bright_blue().bold()); + println!("Decoding vector from: {}", args.input.bright_green()); + + // Create steganographer + let steganographer = VectorSteganographer::with_bits_per_channel(args.bits_per_channel)?; + + // Check if image has embedded data + if !steganographer.has_embedded_data(&args.input) { + eprintln!("{} No embedded data found in image!", "āŒ".red()); + std::process::exit(1); + } + + // Extract the vector + print!("šŸ”“ Extracting embedded vector... "); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + + let (extracted_vector, metadata) = steganographer.extract_vector(&args.input)?; + + println!("{}", "Done!".green()); + + // Display metadata + println!("\n{}", "šŸ“‹ Embedded Metadata:".bright_blue().bold()); + println!(" Version: {}", metadata.version.to_string().bright_cyan()); + println!(" Dimension: {}", metadata.dimension.to_string().bright_cyan()); + println!(" Salt: {}", metadata.salt.bright_yellow()); + println!(" Round ID: {}", metadata.round_id.bright_cyan()); + println!(" Vector length: {}", extracted_vector.len().to_string().bright_green()); + + if args.verbose { + println!("\n{}", "šŸ”¢ Vector Details:".bright_blue().bold()); + println!(" First 10 values: {:?}", &extracted_vector[..10.min(extracted_vector.len())]); + println!(" Last 10 values: {:?}", &extracted_vector[extracted_vector.len().saturating_sub(10)..]); + + // Calculate some statistics + let sum: f64 = extracted_vector.iter().sum(); + let mean = sum / extracted_vector.len() as f64; + let variance: f64 = extracted_vector.iter().map(|&x| (x - mean).powi(2)).sum::() / extracted_vector.len() as f64; + let std_dev = variance.sqrt(); + + println!(" Mean: {:.6}", mean); + println!(" Std Dev: {:.6}", std_dev); + println!(" Min: {:.6}", extracted_vector.iter().fold(f64::INFINITY, |a, &b| a.min(b))); + println!(" Max: {:.6}", extracted_vector.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b))); + } + + // Text verification if provided + if let Some(text) = &args.text { + println!("\n{}", "šŸ¤– Text Verification:".bright_blue().bold()); + print!("Generating CLIP vector for text... "); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + + let text_vector = if args.mock { + let embedder = MockEmbedder::clip_like(); + embedder.get_text_embedding(text)? + } else { + match ClipEmbedder::new() { + Ok(embedder) => embedder.get_text_embedding(text)?, + Err(_) => { + println!("{}", "Failed, falling back to mock embedder".yellow()); + let embedder = MockEmbedder::clip_like(); + embedder.get_text_embedding(text)? + } + } + }; + + println!("{}", "Done!".green()); + + // Calculate similarity + let similarity = cosine_similarity( + &Array1::from_vec(extracted_vector.clone()), + &text_vector + )?; + + println!("Text: {}", text.bright_green()); + println!("Cosine similarity: {:.6}", similarity.to_string().bright_cyan()); + + if similarity > 0.99 { + println!("{} Vectors match very closely!", "āœ…".green()); + } else if similarity > 0.95 { + println!("{} Vectors match well", "āœ…".green()); + } else if similarity > 0.8 { + println!("{} Vectors are similar", "āš ļø".yellow()); + } else { + println!("{} Vectors don't match well", "āŒ".red()); + } + } + + // Commitment verification if provided + if let (Some(salt), Some(text)) = (&args.salt, &args.text) { + println!("\n{}", "šŸ” Commitment Verification:".bright_blue().bold()); + + let generator = EnhancedCommitmentGenerator::new(); + let calculated_commitment = generator.generate_with_vector(text, salt, &extracted_vector)?; + + println!("Calculated commitment: {}", calculated_commitment.bright_cyan()); + + if let Some(provided_commitment) = &args.commitment { + if &calculated_commitment == provided_commitment { + println!("{} Commitment verified!", "āœ…".green()); + } else { + println!("{} Commitment verification failed!", "āŒ".red()); + println!("Expected: {}", provided_commitment.bright_yellow()); + println!("Got: {}", calculated_commitment.bright_cyan()); + } + } + } + + // Output to JSON if requested + if let Some(json_path) = &args.output_json { + println!("\n{}", "šŸ’¾ Saving to JSON:".bright_blue().bold()); + + let output_data = serde_json::json!({ + "metadata": metadata, + "vector": extracted_vector, + "extraction_info": { + "bits_per_channel": args.bits_per_channel, + "input_file": args.input, + "timestamp": chrono::Utc::now().to_rfc3339() + } + }); + + std::fs::write(json_path, serde_json::to_string_pretty(&output_data)?)?; + println!("Saved to: {}", json_path.bright_green()); + } + + println!("\n{}", "šŸŽ‰ Vector successfully decoded!".bright_green().bold()); + + Ok(()) +} \ No newline at end of file diff --git a/src/bin/encode_vector.rs b/src/bin/encode_vector.rs new file mode 100644 index 0000000..7e9a72f --- /dev/null +++ b/src/bin/encode_vector.rs @@ -0,0 +1,180 @@ +//! CLI tool for encoding CLIP vectors into images using steganography +//! +//! This tool allows users to embed CLIP vectors into images that can then be +//! posted to Twitter as part of the reveal phase. + +use clap::Parser; +use realmir_core::{ + steganography::{VectorSteganographer, EmbeddedVectorMeta, utils}, + embedder::{EmbedderTrait, MockEmbedder, ClipEmbedder}, + error::Result, +}; +use std::fs; +use colored::*; + +#[derive(Parser)] +#[command(name = "encode-vector")] +#[command(about = "Encode CLIP vector into an image using steganography")] +#[command(version = "1.0")] +struct Args { + /// Input image path + #[arg(short, long)] + input: String, + + /// Output image path + #[arg(short, long)] + output: String, + + /// Text to generate CLIP vector for + #[arg(short, long)] + text: String, + + /// Salt for the commitment + #[arg(short, long)] + salt: String, + + /// Round ID + #[arg(short, long)] + round_id: String, + + /// Use mock embedder instead of real CLIP (for testing) + #[arg(long, default_value = "false")] + mock: bool, + + /// Bits per channel for steganography (1-3 recommended) + #[arg(long, default_value = "2")] + bits_per_channel: u8, + + /// Create a test image if input doesn't exist + #[arg(long, default_value = "false")] + create_test_image: bool, + + /// Test image dimensions (if creating) + #[arg(long, default_value = "512")] + image_size: u32, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + println!("{}", "šŸŽØ RealMir Vector Encoder".bright_blue().bold()); + println!("Encoding CLIP vector for: {}", args.text.bright_green()); + + // Create steganographer + let steganographer = VectorSteganographer::with_bits_per_channel(args.bits_per_channel)?; + + // Handle input image - create if needed and requested + if !std::path::Path::new(&args.input).exists() { + if args.create_test_image { + println!("šŸ“ Creating test image: {}", args.input.bright_yellow()); + let (min_width, min_height) = utils::min_image_size_for_vector(512, args.bits_per_channel); + let width = args.image_size.max(min_width); + let height = args.image_size.max(min_height); + + utils::create_test_image(width, height, &args.input)?; + println!("āœ… Created {}x{} test image", width, height); + } else { + eprintln!("{} Input image not found: {}", "āŒ".red(), args.input); + eprintln!("Use --create-test-image to create a test image"); + std::process::exit(1); + } + } + + // Generate CLIP vector + print!("šŸ¤– Generating CLIP vector... "); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + + let clip_vector = if args.mock { + let embedder = MockEmbedder::clip_like(); + embedder.get_text_embedding(&args.text)? + } else { + match ClipEmbedder::new() { + Ok(embedder) => embedder.get_text_embedding(&args.text)?, + Err(_) => { + println!("{}", "Failed, falling back to mock embedder".yellow()); + let embedder = MockEmbedder::clip_like(); + embedder.get_text_embedding(&args.text)? + } + } + }; + + println!("{}", "Done!".green()); + println!("Vector dimension: {}", clip_vector.len().to_string().bright_cyan()); + + // Create metadata + let metadata = EmbeddedVectorMeta { + version: 1, + dimension: clip_vector.len() as u32, + salt: args.salt.clone(), + round_id: args.round_id.clone(), + checksum: None, + }; + + // Check capacity + let img = image::open(&args.input).map_err(|_e| { + realmir_core::error::RealMirError::Steganography( + realmir_core::error::SteganographyError::InvalidImage + ) + })?; + let capacity = steganographer.calculate_capacity(&img.to_rgb8()); + let vector_bytes = clip_vector.len() * 8; // f64 = 8 bytes + let meta_bytes = 256; // Estimated + let total_needed = vector_bytes + meta_bytes + 20; // Header overhead + + println!("šŸ“Š Capacity check:"); + println!(" Image capacity: {} bytes", capacity.to_string().bright_cyan()); + println!(" Data needed: {} bytes", total_needed.to_string().bright_yellow()); + + if total_needed > capacity { + eprintln!("{} Insufficient capacity in image!", "āŒ".red()); + eprintln!("Try using a larger image or fewer bits per channel."); + std::process::exit(1); + } + + // Embed the vector + print!("šŸ” Embedding vector in image... "); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + + let vector_f64: Vec = clip_vector.to_vec(); + steganographer.embed_vector(&args.input, &vector_f64, &metadata, &args.output)?; + + println!("{}", "Done!".green()); + + // Verify the embedding + print!("āœ… Verifying embedding... "); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + + match steganographer.extract_vector(&args.output) { + Ok((extracted_vector, extracted_meta)) => { + if extracted_vector.len() == vector_f64.len() + && extracted_meta.salt == args.salt + && extracted_meta.round_id == args.round_id { + println!("{}", "Verified!".green()); + } else { + println!("{}", "Verification failed!".red()); + std::process::exit(1); + } + } + Err(e) => { + println!("{} {}", "Failed:".red(), e); + std::process::exit(1); + } + } + + // Summary + println!("\n{}", "šŸ“‹ Summary:".bright_blue().bold()); + println!(" Input: {}", args.input); + println!(" Output: {}", args.output); + println!(" Text: {}", args.text.bright_green()); + println!(" Salt: {}", args.salt.bright_yellow()); + println!(" Round ID: {}", args.round_id.bright_cyan()); + println!(" Vector size: {} elements", clip_vector.len()); + + let output_size = fs::metadata(&args.output)?.len(); + println!(" Output file size: {} bytes", output_size); + + println!("\n{}", "šŸŽ‰ Vector successfully encoded!".bright_green().bold()); + println!("You can now post {} to Twitter for your reveal.", args.output.bright_blue()); + + Ok(()) +} \ No newline at end of file diff --git a/src/bin/generate_enhanced_commitment.rs b/src/bin/generate_enhanced_commitment.rs new file mode 100644 index 0000000..91b5583 --- /dev/null +++ b/src/bin/generate_enhanced_commitment.rs @@ -0,0 +1,197 @@ +//! CLI tool for generating enhanced commitments with CLIP vectors and proof of work +//! +//! This tool generates the enhanced commitment described in the Slack thread: +//! hash = sha256(plaintext_prediction || salt || clip_vector) +//! Plus proof of work for spam prevention. + +use clap::Parser; +use realmir_core::{ + commitment::{EnhancedCommitmentGenerator}, + embedder::{EmbedderTrait, MockEmbedder, ClipEmbedder}, + error::Result, +}; +use colored::*; +use serde_json; + +#[derive(Parser)] +#[command(name = "generate-enhanced-commitment")] +#[command(about = "Generate enhanced commitment with CLIP vector and proof of work")] +#[command(version = "1.0")] +struct Args { + /// Prediction text to commit to + #[arg(short, long)] + prediction: String, + + /// Round ID for this commitment + #[arg(short, long)] + round_id: String, + + /// Optional salt (will be generated if not provided) + #[arg(short, long)] + salt: Option, + + /// Proof of work difficulty (1-10, default: 4) + #[arg(long, default_value = "4")] + difficulty: u8, + + /// Use mock embedder instead of real CLIP (for testing) + #[arg(long, default_value = "false")] + mock: bool, + + /// Output commitment to JSON file + #[arg(long)] + output_json: Option, + + /// Show detailed output including vector information + #[arg(long, default_value = "false")] + verbose: bool, + + /// Skip proof of work generation (for testing) + #[arg(long, default_value = "false")] + skip_pow: bool, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + println!("{}", "šŸ” RealMir Enhanced Commitment Generator".bright_blue().bold()); + println!("Prediction: {}", args.prediction.bright_green()); + println!("Round ID: {}", args.round_id.bright_cyan()); + + // Create generator with appropriate difficulty + let generator = if args.skip_pow { + EnhancedCommitmentGenerator::with_pow_difficulty(1)? // Minimal difficulty for testing + } else { + EnhancedCommitmentGenerator::with_pow_difficulty(args.difficulty)? + }; + + // Generate or use provided salt + let salt = args.salt.unwrap_or_else(|| { + let generated_salt = generator.generate_salt(); + println!("Generated salt: {}", generated_salt.bright_yellow()); + generated_salt + }); + + // Generate CLIP vector + print!("šŸ¤– Computing CLIP vector... "); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + + let clip_vector = if args.mock { + let embedder = MockEmbedder::clip_like(); + embedder.get_text_embedding(&args.prediction)? + } else { + match ClipEmbedder::new() { + Ok(embedder) => embedder.get_text_embedding(&args.prediction)?, + Err(_) => { + println!("{}", "Failed, falling back to mock embedder".yellow()); + let embedder = MockEmbedder::clip_like(); + embedder.get_text_embedding(&args.prediction)? + } + } + }; + + println!("{}", "Done!".green()); + println!("Vector dimension: {}", clip_vector.len().to_string().bright_cyan()); + + if args.verbose { + println!("Vector preview: {:?}...", &clip_vector.to_vec()[..5.min(clip_vector.len())]); + } + + // Generate proof of work + if !args.skip_pow { + println!("ā›ļø Generating proof of work (difficulty: {})...", args.difficulty); + print!("This may take a moment... "); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + } + + let start_time = std::time::Instant::now(); + + // Generate the enhanced commitment + let commitment = generator.generate_enhanced( + &args.prediction, + &salt, + &clip_vector.to_vec(), + &args.round_id, + )?; + + let generation_time = start_time.elapsed(); + + if !args.skip_pow { + println!("{} (took {:.2}s)", "Done!".green(), generation_time.as_secs_f64()); + } + + // Display results + println!("\n{}", "āœ… Enhanced Commitment Generated:".bright_green().bold()); + println!(" Commitment Hash: {}", commitment.commitment_hash.bright_cyan()); + println!(" Vector Hash: {}", commitment.vector_commitment.bright_yellow()); + println!(" Salt: {}", salt.bright_yellow()); + println!(" Round ID: {}", commitment.round_id.bright_cyan()); + println!(" Timestamp: {}", commitment.timestamp.format("%Y-%m-%d %H:%M:%S UTC")); + + // Proof of work details + println!("\n{}", "ā›ļø Proof of Work:".bright_blue().bold()); + println!(" Difficulty: {}", commitment.proof_of_work.difficulty.to_string().bright_cyan()); + println!(" Nonce: {}", commitment.proof_of_work.nonce.to_string().bright_yellow()); + println!(" Hash: {}", commitment.proof_of_work.hash.bright_green()); + + if args.verbose { + println!(" Challenge: {}", commitment.proof_of_work.challenge.bright_white()); + println!(" PoW Timestamp: {}", commitment.proof_of_work.timestamp.format("%Y-%m-%d %H:%M:%S UTC")); + } + + // Verify the commitment + print!("\nšŸ” Verifying commitment... "); + std::io::Write::flush(&mut std::io::stdout()).unwrap(); + + if commitment.is_valid() { + println!("{}", "Valid!".green()); + } else { + println!("{}", "Invalid!".red()); + eprintln!("āš ļø Generated commitment failed validation!"); + std::process::exit(1); + } + + // Show Twitter posting instructions + println!("\n{}", "šŸ“± Twitter Posting Instructions:".bright_blue().bold()); + println!("1. For the COMMITMENT phase, post:"); + println!(" Commit: {}", commitment.commitment_hash.bright_cyan()); + println!(" Wallet: your_wallet_address"); + println!(" (Optional: Include PoW nonce {} for extra verification)", commitment.proof_of_work.nonce); + + println!("\n2. For the REVEAL phase, post:"); + println!(" Prediction: {}", args.prediction.bright_green()); + println!(" Salt: {}", salt.bright_yellow()); + println!(" [Attach image with embedded CLIP vector]"); + + println!("\n{}", "šŸ’” Next Steps:".bright_blue().bold()); + println!("1. Use encode-vector tool to embed CLIP vector in an image"); + println!("2. Post the commitment hash to Twitter"); + println!("3. During reveal, post prediction + salt + image"); + + // Output to JSON if requested + if let Some(json_path) = &args.output_json { + println!("\n{}", "šŸ’¾ Saving to JSON:".bright_blue().bold()); + + let output_data = serde_json::json!({ + "commitment": commitment, + "reveal_data": { + "prediction": args.prediction, + "salt": salt, + "clip_vector": clip_vector.to_vec() + }, + "generation_info": { + "difficulty": args.difficulty, + "generation_time_seconds": generation_time.as_secs_f64(), + "mock_embedder": args.mock, + "timestamp": chrono::Utc::now().to_rfc3339() + } + }); + + std::fs::write(json_path, serde_json::to_string_pretty(&output_data)?)?; + println!("Saved to: {}", json_path.bright_green()); + } + + println!("\n{}", "šŸŽ‰ Enhanced commitment ready for Twitter!".bright_green().bold()); + + Ok(()) +} \ No newline at end of file diff --git a/src/commitment.rs b/src/commitment.rs index c346dac..7664119 100644 --- a/src/commitment.rs +++ b/src/commitment.rs @@ -6,6 +6,8 @@ use sha2::{Digest, Sha256}; use crate::error::{CommitmentError, Result}; +use crate::proof_of_work::{ProofOfWork, ProofOfWorkSystem}; +use serde::{Deserialize, Serialize}; @@ -142,6 +144,275 @@ impl Default for CommitmentVerifier { } } +/// Enhanced commitment that includes CLIP vectors and proof of work +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnhancedCommitment { + /// The basic commitment hash + pub commitment_hash: String, + /// The CLIP vector commitment (hash of the vector) + pub vector_commitment: String, + /// Proof of work for spam prevention + pub proof_of_work: ProofOfWork, + /// Round ID for this commitment + pub round_id: String, + /// Timestamp when commitment was created + pub timestamp: chrono::DateTime, +} + +impl EnhancedCommitment { + /// Create a new enhanced commitment + pub fn new( + commitment_hash: String, + vector_commitment: String, + proof_of_work: ProofOfWork, + round_id: String, + ) -> Self { + Self { + commitment_hash, + vector_commitment, + proof_of_work, + round_id, + timestamp: chrono::Utc::now(), + } + } + + /// Verify that all components of this commitment are valid + pub fn is_valid(&self) -> bool { + // Verify proof of work + if !self.proof_of_work.is_valid() { + return false; + } + + // Verify that proof of work challenge is for the commitment phase and includes round ID + if !self.proof_of_work.challenge.starts_with("commit:") { + return false; + } + + if !self.proof_of_work.challenge.contains(&self.round_id) { + return false; + } + + // Basic format validation + if self.commitment_hash.len() != 64 || self.vector_commitment.len() != 64 { + return false; + } + + true + } +} + +/// Enhanced commitment generator that creates commitments with CLIP vectors and PoW +#[derive(Debug, Clone)] +pub struct EnhancedCommitmentGenerator { + basic_generator: CommitmentGenerator, + pow_system: ProofOfWorkSystem, +} + +impl EnhancedCommitmentGenerator { + /// Create a new enhanced commitment generator + pub fn new() -> Self { + Self { + basic_generator: CommitmentGenerator::new(), + pow_system: ProofOfWorkSystem::new(), + } + } + + /// Create generator with custom proof of work difficulty + pub fn with_pow_difficulty(difficulty: u8) -> Result { + Ok(Self { + basic_generator: CommitmentGenerator::new(), + pow_system: ProofOfWorkSystem::with_difficulty(difficulty)?, + }) + } + + /// Generate an enhanced commitment including CLIP vector and proof of work + /// + /// This implements the system described in the Slack thread: + /// hash = sha256(plaintext_prediction || salt || clip_vector) + /// + /// # Arguments + /// * `prediction` - The plaintext prediction + /// * `salt` - Random salt for the commitment + /// * `clip_vector` - The CLIP vector for the prediction + /// * `round_id` - ID of the round this commitment is for + /// + /// # Returns + /// EnhancedCommitment with all components + pub fn generate_enhanced( + &self, + prediction: &str, + salt: &str, + clip_vector: &[f64], + round_id: &str, + ) -> Result { + if prediction.trim().is_empty() { + return Err(CommitmentError::EmptyMessage.into()); + } + if salt.is_empty() { + return Err(CommitmentError::EmptySalt.into()); + } + + // Create the commitment hash including the CLIP vector + let commitment_hash = self.generate_with_vector(prediction, salt, clip_vector)?; + + // Create a separate hash just for the CLIP vector (for verification) + let vector_commitment = self.hash_vector(clip_vector)?; + + // Generate proof of work + let proof_of_work = self.pow_system.generate_commitment_proof( + prediction, + salt, + round_id, + None, // Use default difficulty + )?; + + Ok(EnhancedCommitment::new( + commitment_hash, + vector_commitment, + proof_of_work, + round_id.to_string(), + )) + } + + /// Generate commitment hash including CLIP vector + /// Implements: hash = sha256(plaintext_prediction || salt || clip_vector) + pub fn generate_with_vector( + &self, + prediction: &str, + salt: &str, + clip_vector: &[f64], + ) -> Result { + let mut hasher = Sha256::new(); + + // Add prediction text + hasher.update(prediction.as_bytes()); + + // Add salt + hasher.update(salt.as_bytes()); + + // Add CLIP vector as bytes + for &value in clip_vector { + hasher.update(value.to_le_bytes()); + } + + let result = hasher.finalize(); + Ok(format!("{:x}", result)) + } + + /// Create a hash of just the CLIP vector for separate verification + pub fn hash_vector(&self, clip_vector: &[f64]) -> Result { + let mut hasher = Sha256::new(); + + for &value in clip_vector { + hasher.update(value.to_le_bytes()); + } + + let result = hasher.finalize(); + Ok(format!("{:x}", result)) + } + + /// Generate a random salt (delegated to basic generator) + pub fn generate_salt(&self) -> String { + self.basic_generator.generate_salt() + } +} + +impl Default for EnhancedCommitmentGenerator { + fn default() -> Self { + Self::new() + } +} + +/// Enhanced commitment verifier for validating commitments with CLIP vectors +#[derive(Debug, Clone)] +pub struct EnhancedCommitmentVerifier { + basic_verifier: CommitmentVerifier, + generator: EnhancedCommitmentGenerator, +} + +impl EnhancedCommitmentVerifier { + /// Create a new enhanced commitment verifier + pub fn new() -> Self { + Self { + basic_verifier: CommitmentVerifier::new(), + generator: EnhancedCommitmentGenerator::new(), + } + } + + /// Verify an enhanced commitment against the reveal data + /// + /// # Arguments + /// * `commitment` - The original commitment + /// * `prediction` - The revealed prediction text + /// * `salt` - The revealed salt + /// * `clip_vector` - The revealed CLIP vector + /// + /// # Returns + /// True if the commitment is valid + pub fn verify_enhanced( + &self, + commitment: &EnhancedCommitment, + prediction: &str, + salt: &str, + clip_vector: &[f64], + ) -> bool { + // First verify the commitment structure itself + if !commitment.is_valid() { + return false; + } + + // Verify the main commitment hash + match self.generator.generate_with_vector(prediction, salt, clip_vector) { + Ok(calculated_hash) => { + if calculated_hash != commitment.commitment_hash { + return false; + } + } + Err(_) => return false, + } + + // Verify the vector commitment + match self.generator.hash_vector(clip_vector) { + Ok(calculated_vector_hash) => { + if calculated_vector_hash != commitment.vector_commitment { + return false; + } + } + Err(_) => return false, + } + + // All verifications passed + true + } + + /// Verify just the CLIP vector against its commitment + pub fn verify_vector(&self, vector_commitment: &str, clip_vector: &[f64]) -> bool { + match self.generator.hash_vector(clip_vector) { + Ok(calculated_hash) => calculated_hash == vector_commitment, + Err(_) => false, + } + } + + /// Batch verify multiple enhanced commitments + pub fn verify_enhanced_batch( + &self, + commitments: &[(EnhancedCommitment, String, String, Vec)], // (commitment, prediction, salt, vector) + ) -> Vec { + commitments + .iter() + .map(|(commitment, prediction, salt, vector)| { + self.verify_enhanced(commitment, prediction, salt, vector) + }) + .collect() + } +} + +impl Default for EnhancedCommitmentVerifier { + fn default() -> Self { + Self::new() + } +} + #[cfg(test)] @@ -290,4 +561,144 @@ mod tests { assert!(!verifier.verify("different message", salt, &commitment)); assert!(!verifier.verify(message, "different_salt", &commitment)); } + + // Enhanced commitment tests + + fn create_test_clip_vector() -> Vec { + (0..512).map(|i| (i as f64) / 512.0).collect() + } + + #[test] + fn test_enhanced_commitment_generation() { + let generator = EnhancedCommitmentGenerator::with_pow_difficulty(1).unwrap(); // Easy PoW for testing + let prediction = "The cat will be orange"; + let salt = "test_salt_456"; + let clip_vector = create_test_clip_vector(); + let round_id = "test_round_001"; + + let commitment = generator + .generate_enhanced(prediction, &salt, &clip_vector, round_id) + .unwrap(); + + // Verify commitment structure + assert!(commitment.is_valid()); + assert_eq!(commitment.round_id, round_id); + assert_eq!(commitment.commitment_hash.len(), 64); + assert_eq!(commitment.vector_commitment.len(), 64); + assert!(commitment.proof_of_work.is_valid()); + } + + #[test] + fn test_enhanced_commitment_verification() { + let generator = EnhancedCommitmentGenerator::with_pow_difficulty(1).unwrap(); + let verifier = EnhancedCommitmentVerifier::new(); + + let prediction = "The dog will be brown"; + let salt = generator.generate_salt(); + let clip_vector = create_test_clip_vector(); + let round_id = "verification_test"; + + // Generate commitment + let commitment = generator + .generate_enhanced(prediction, &salt, &clip_vector, round_id) + .unwrap(); + + // Should verify correctly + assert!(verifier.verify_enhanced(&commitment, prediction, &salt, &clip_vector)); + + // Should fail with wrong prediction + assert!(!verifier.verify_enhanced( + &commitment, + "Wrong prediction", + &salt, + &clip_vector + )); + + // Should fail with wrong salt + assert!(!verifier.verify_enhanced( + &commitment, + prediction, + "wrong_salt", + &clip_vector + )); + + // Should fail with wrong vector + let wrong_vector: Vec = (0..512).map(|i| (i as f64) / 256.0).collect(); + assert!(!verifier.verify_enhanced(&commitment, prediction, &salt, &wrong_vector)); + } + + #[test] + fn test_commitment_with_vector_hash() { + let generator = EnhancedCommitmentGenerator::new(); + let prediction = "Test prediction"; + let salt = "test_salt"; + let clip_vector = create_test_clip_vector(); + + // Generate commitment hash with vector + let hash1 = generator + .generate_with_vector(prediction, salt, &clip_vector) + .unwrap(); + + // Should be deterministic + let hash2 = generator + .generate_with_vector(prediction, salt, &clip_vector) + .unwrap(); + assert_eq!(hash1, hash2); + + // Different vector should produce different hash + let different_vector: Vec = (0..512).map(|i| (i as f64) / 256.0).collect(); + let hash3 = generator + .generate_with_vector(prediction, salt, &different_vector) + .unwrap(); + assert_ne!(hash1, hash3); + } + + #[test] + fn test_vector_commitment() { + let generator = EnhancedCommitmentGenerator::new(); + let verifier = EnhancedCommitmentVerifier::new(); + let clip_vector = create_test_clip_vector(); + + let vector_hash = generator.hash_vector(&clip_vector).unwrap(); + + // Should verify correctly + assert!(verifier.verify_vector(&vector_hash, &clip_vector)); + + // Should fail with different vector + let different_vector: Vec = (0..512).map(|i| (i as f64) / 256.0).collect(); + assert!(!verifier.verify_vector(&vector_hash, &different_vector)); + } + + #[test] + fn test_enhanced_batch_verification() { + let generator = EnhancedCommitmentGenerator::with_pow_difficulty(1).unwrap(); + let verifier = EnhancedCommitmentVerifier::new(); + + // Create test data + let test_cases = vec![ + ("Prediction 1", "salt1", create_test_clip_vector(), "round1"), + ("Prediction 2", "salt2", create_test_clip_vector(), "round2"), + ]; + + let mut commitments_and_reveals = Vec::new(); + + for (prediction, salt, vector, round_id) in test_cases { + let commitment = generator + .generate_enhanced(prediction, salt, &vector, round_id) + .unwrap(); + + commitments_and_reveals.push(( + commitment, + prediction.to_string(), + salt.to_string(), + vector, + )); + } + + // Verify batch + let results = verifier.verify_enhanced_batch(&commitments_and_reveals); + + // All should be valid + assert_eq!(results, vec![true, true]); + } } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 2400e60..1c44614 100644 --- a/src/error.rs +++ b/src/error.rs @@ -23,6 +23,12 @@ pub enum RealMirError { #[error("Round processing error: {0}")] Round(#[from] RoundError), + #[error("Steganography error: {0}")] + Steganography(#[from] SteganographyError), + + #[error("Proof of work error: {0}")] + ProofOfWork(#[from] ProofOfWorkError), + #[error("Validation error: {0}")] ValidationError(String), @@ -129,4 +135,45 @@ pub enum ValidationError { #[error("Invalid participant data")] InvalidParticipant, +} + +/// Steganography-related errors +#[derive(Error, Debug)] +pub enum SteganographyError { + #[error("Invalid image format or corrupted image")] + InvalidImage, + + #[error("No embedded data found in image")] + NoEmbeddedData, + + #[error("Embedded data is corrupted or invalid")] + CorruptedData, + + #[error("Image has insufficient capacity for the data")] + InsufficientCapacity, + + #[error("Failed to encode data")] + EncodingFailed, + + #[error("Failed to save image")] + SaveFailed, + + #[error("Invalid steganography configuration")] + InvalidConfiguration, +} + +/// Proof of work related errors +#[derive(Error, Debug)] +pub enum ProofOfWorkError { + #[error("Difficulty level too high (maximum is 20)")] + DifficultyTooHigh, + + #[error("Proof generation timed out")] + GenerationTimeout, + + #[error("Nonce overflow occurred")] + NonceOverflow, + + #[error("Invalid proof of work")] + InvalidProof, } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 93b0934..c942890 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,9 +25,11 @@ pub mod embedder; pub mod error; pub mod models; pub mod payout; +pub mod proof_of_work; pub mod round; pub mod scoring; pub mod social; +pub mod steganography; pub mod types; // Python bindings module (conditional compilation) @@ -35,13 +37,15 @@ pub mod types; pub mod python_bridge; // Re-export commonly used types -pub use commitment::{CommitmentGenerator, CommitmentVerifier}; +pub use commitment::{CommitmentGenerator, CommitmentVerifier, EnhancedCommitmentGenerator, EnhancedCommitmentVerifier, EnhancedCommitment}; pub use scoring::{ScoringStrategy, ClipBatchStrategy, ScoreValidator}; pub use embedder::{EmbedderTrait, MockEmbedder}; pub use round::{RoundProcessor}; pub use payout::{PayoutCalculator, PayoutConfig, PayoutInfo}; pub use config::{ConfigManager, CostTracker, RealMirConfig, OpenAIConfig, SpendingStatus}; pub use social::{SocialWorkflow, AnnouncementFormatter, UrlParser, HashtagManager, AnnouncementData, TweetId}; +pub use steganography::{VectorSteganographer, EmbeddedVectorMeta}; +pub use proof_of_work::{ProofOfWork, ProofOfWorkSystem, ProofOfWorkManager}; pub use types::{Guess, Participant, ScoringResult, RoundData}; pub use error::{RealMirError, Result}; diff --git a/src/proof_of_work.rs b/src/proof_of_work.rs new file mode 100644 index 0000000..d31ab31 --- /dev/null +++ b/src/proof_of_work.rs @@ -0,0 +1,431 @@ +//! Proof of Work module for spam prevention +//! +//! This module provides lightweight hashcash-style proof of work to prevent +//! spam in the commitment/reveal system. Both miners and validators can be +//! required to provide proof of work to increase the computational cost +//! of participating in the system. + +use sha2::{Digest, Sha256}; +use std::time::{Duration, Instant}; +use serde::{Deserialize, Serialize}; +use crate::error::{Result, ProofOfWorkError}; + +/// Proof of work challenge and solution +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProofOfWork { + /// The challenge data that was hashed + pub challenge: String, + /// The nonce that satisfies the difficulty requirement + pub nonce: u64, + /// The resulting hash that meets the difficulty + pub hash: String, + /// The difficulty level (number of leading zeros required) + pub difficulty: u8, + /// Timestamp when the proof was generated + pub timestamp: chrono::DateTime, +} + +impl ProofOfWork { + /// Create a new proof of work instance + pub fn new(challenge: String, nonce: u64, hash: String, difficulty: u8) -> Self { + Self { + challenge, + nonce, + hash, + difficulty, + timestamp: chrono::Utc::now(), + } + } + + /// Verify that this proof of work is valid + pub fn is_valid(&self) -> bool { + // Recalculate the hash + let calculated_hash = Self::calculate_hash(&self.challenge, self.nonce); + + // Check if hash matches + if calculated_hash != self.hash { + return false; + } + + // Check if hash meets difficulty requirement + Self::meets_difficulty(&self.hash, self.difficulty) + } + + /// Calculate SHA-256 hash for challenge and nonce + fn calculate_hash(challenge: &str, nonce: u64) -> String { + let mut hasher = Sha256::new(); + hasher.update(challenge.as_bytes()); + hasher.update(nonce.to_le_bytes()); + format!("{:x}", hasher.finalize()) + } + + /// Check if a hash meets the difficulty requirement (leading zeros) + fn meets_difficulty(hash: &str, difficulty: u8) -> bool { + if difficulty == 0 { + return true; + } + + let required_zeros = difficulty as usize; + hash.chars().take(required_zeros).all(|c| c == '0') + } +} + +/// Proof of work generator and verifier +#[derive(Debug, Clone)] +pub struct ProofOfWorkSystem { + /// Default difficulty level for new challenges + default_difficulty: u8, + /// Maximum time to spend on proof generation + max_generation_time: Duration, +} + +impl ProofOfWorkSystem { + /// Create a new proof of work system with default settings + pub fn new() -> Self { + Self { + default_difficulty: 4, // Require 4 leading zeros by default + max_generation_time: Duration::from_secs(30), // 30 second timeout + } + } + + /// Create a system with custom difficulty + pub fn with_difficulty(difficulty: u8) -> Result { + if difficulty > 20 { + return Err(ProofOfWorkError::DifficultyTooHigh.into()); + } + + Ok(Self { + default_difficulty: difficulty, + max_generation_time: Duration::from_secs(30), + }) + } + + /// Create a system with custom settings + pub fn with_settings(difficulty: u8, max_time: Duration) -> Result { + if difficulty > 20 { + return Err(ProofOfWorkError::DifficultyTooHigh.into()); + } + + Ok(Self { + default_difficulty: difficulty, + max_generation_time: max_time, + }) + } + + /// Generate proof of work for a given challenge + /// + /// # Arguments + /// * `challenge` - The challenge string to prove work for + /// * `difficulty` - Optional custom difficulty (uses default if None) + /// + /// # Returns + /// ProofOfWork instance with valid nonce, or error if timeout + pub fn generate_proof(&self, challenge: &str, difficulty: Option) -> Result { + let target_difficulty = difficulty.unwrap_or(self.default_difficulty); + let start_time = Instant::now(); + + let mut nonce = 0u64; + + loop { + // Check timeout + if start_time.elapsed() > self.max_generation_time { + return Err(ProofOfWorkError::GenerationTimeout.into()); + } + + // Calculate hash for current nonce + let hash = ProofOfWork::calculate_hash(challenge, nonce); + + // Check if this hash meets the difficulty requirement + if ProofOfWork::meets_difficulty(&hash, target_difficulty) { + return Ok(ProofOfWork::new( + challenge.to_string(), + nonce, + hash, + target_difficulty, + )); + } + + nonce += 1; + + // Prevent overflow (very unlikely but good practice) + if nonce == u64::MAX { + return Err(ProofOfWorkError::NonceOverflow.into()); + } + } + } + + /// Verify a proof of work + pub fn verify_proof(&self, proof: &ProofOfWork) -> bool { + proof.is_valid() + } + + /// Generate proof of work for commitment phase + /// Combines prediction text, salt, and round ID as challenge + pub fn generate_commitment_proof( + &self, + prediction: &str, + salt: &str, + round_id: &str, + difficulty: Option, + ) -> Result { + let challenge = format!("commit:{}:{}:{}", round_id, prediction, salt); + self.generate_proof(&challenge, difficulty) + } + + /// Generate proof of work for reveal phase + /// Includes the commitment hash in the challenge + pub fn generate_reveal_proof( + &self, + prediction: &str, + salt: &str, + commitment_hash: &str, + round_id: &str, + difficulty: Option, + ) -> Result { + let challenge = format!("reveal:{}:{}:{}:{}", round_id, commitment_hash, prediction, salt); + self.generate_proof(&challenge, difficulty) + } + + /// Estimate time to generate proof for given difficulty + /// Returns an estimate based on average hash rate + pub fn estimate_generation_time(&self, difficulty: u8) -> Duration { + // Very rough estimate: each difficulty level increases time by ~16x + // This is highly dependent on hardware, so take with grain of salt + let base_time_ms = 10u64; // 10ms for difficulty 1 + let multiplier = 16u64.pow(difficulty as u32); + let estimated_ms = base_time_ms * multiplier; + + Duration::from_millis(estimated_ms.min(300_000)) // Cap at 5 minutes + } + + /// Get the default difficulty level + pub fn default_difficulty(&self) -> u8 { + self.default_difficulty + } + + /// Set the default difficulty level + pub fn set_default_difficulty(&mut self, difficulty: u8) -> Result<()> { + if difficulty > 20 { + return Err(ProofOfWorkError::DifficultyTooHigh.into()); + } + self.default_difficulty = difficulty; + Ok(()) + } +} + +impl Default for ProofOfWorkSystem { + fn default() -> Self { + Self::new() + } +} + +/// Proof of work statistics and metrics +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProofOfWorkStats { + /// Average generation time for recent proofs + pub avg_generation_time: Duration, + /// Number of proofs generated + pub proofs_generated: u64, + /// Current difficulty level + pub current_difficulty: u8, + /// Hash rate (hashes per second) + pub hash_rate: f64, +} + +/// Proof of work manager for tracking and adjusting difficulty +#[derive(Debug)] +pub struct ProofOfWorkManager { + system: ProofOfWorkSystem, + stats: ProofOfWorkStats, + recent_times: Vec, + target_time: Duration, +} + +impl ProofOfWorkManager { + /// Create a new manager with target generation time + pub fn new(target_time: Duration) -> Self { + Self { + system: ProofOfWorkSystem::new(), + stats: ProofOfWorkStats { + avg_generation_time: Duration::from_secs(0), + proofs_generated: 0, + current_difficulty: 4, + hash_rate: 0.0, + }, + recent_times: Vec::new(), + target_time, + } + } + + /// Generate proof and update statistics + pub fn generate_tracked_proof(&mut self, challenge: &str) -> Result { + let start_time = Instant::now(); + let proof = self.system.generate_proof(challenge, None)?; + let generation_time = start_time.elapsed(); + + // Update statistics + self.recent_times.push(generation_time); + if self.recent_times.len() > 10 { + self.recent_times.remove(0); // Keep only recent 10 times + } + + self.stats.proofs_generated += 1; + self.update_stats(generation_time); + + Ok(proof) + } + + /// Update internal statistics + fn update_stats(&mut self, _last_time: Duration) { + if !self.recent_times.is_empty() { + let total_time: Duration = self.recent_times.iter().sum(); + self.stats.avg_generation_time = total_time / self.recent_times.len() as u32; + + // Calculate hash rate (very rough estimate) + let avg_seconds = self.stats.avg_generation_time.as_secs_f64(); + if avg_seconds > 0.0 { + // Estimate based on difficulty and average time + let estimated_hashes = 2u64.pow(self.stats.current_difficulty as u32 * 4) as f64; + self.stats.hash_rate = estimated_hashes / avg_seconds; + } + } + } + + /// Adjust difficulty based on recent generation times + pub fn adjust_difficulty(&mut self) -> Result<()> { + if self.recent_times.len() < 5 { + return Ok(()); + } + + let avg_time = self.stats.avg_generation_time; + + // If average time is too low, increase difficulty + if avg_time < self.target_time / 2 && self.stats.current_difficulty < 15 { + self.stats.current_difficulty += 1; + self.system.set_default_difficulty(self.stats.current_difficulty)?; + } + // If average time is too high, decrease difficulty + else if avg_time > self.target_time * 2 && self.stats.current_difficulty > 1 { + self.stats.current_difficulty -= 1; + self.system.set_default_difficulty(self.stats.current_difficulty)?; + } + + Ok(()) + } + + /// Get current statistics + pub fn get_stats(&self) -> &ProofOfWorkStats { + &self.stats + } + + /// Get the underlying proof of work system + pub fn system(&self) -> &ProofOfWorkSystem { + &self.system + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_proof_generation_and_verification() { + let pow_system = ProofOfWorkSystem::with_difficulty(2).unwrap(); // Easy difficulty for testing + let challenge = "test_challenge_123"; + + let proof = pow_system.generate_proof(challenge, None).unwrap(); + + // Verify the proof + assert!(pow_system.verify_proof(&proof)); + assert!(proof.is_valid()); + + // Check that difficulty is met + assert!(proof.hash.starts_with("00")); // 2 leading zeros + } + + #[test] + fn test_commitment_reveal_proofs() { + let pow_system = ProofOfWorkSystem::with_difficulty(1).unwrap(); + let prediction = "The cat will be orange"; + let salt = "random_salt_456"; + let round_id = "round_001"; + let commitment_hash = "abc123def456"; + + // Generate commitment proof + let commit_proof = pow_system + .generate_commitment_proof(prediction, salt, round_id, None) + .unwrap(); + + assert!(commit_proof.is_valid()); + assert!(commit_proof.challenge.contains("commit:")); + assert!(commit_proof.challenge.contains(round_id)); + + // Generate reveal proof + let reveal_proof = pow_system + .generate_reveal_proof(prediction, salt, commitment_hash, round_id, None) + .unwrap(); + + assert!(reveal_proof.is_valid()); + assert!(reveal_proof.challenge.contains("reveal:")); + assert!(reveal_proof.challenge.contains(commitment_hash)); + } + + #[test] + fn test_invalid_proof() { + let pow_system = ProofOfWorkSystem::new(); + + // Create an invalid proof + let invalid_proof = ProofOfWork::new( + "challenge".to_string(), + 12345, + "invalid_hash".to_string(), + 4, + ); + + assert!(!pow_system.verify_proof(&invalid_proof)); + assert!(!invalid_proof.is_valid()); + } + + #[test] + fn test_difficulty_levels() { + let pow_system = ProofOfWorkSystem::with_difficulty(3).unwrap(); + let challenge = "difficulty_test"; + + let proof = pow_system.generate_proof(challenge, None).unwrap(); + + // Should have 3 leading zeros + assert!(proof.hash.starts_with("000")); + assert_eq!(proof.difficulty, 3); + } + + #[test] + fn test_difficulty_too_high() { + let result = ProofOfWorkSystem::with_difficulty(25); + assert!(matches!(result, Err(crate::error::RealMirError::ProofOfWork(ProofOfWorkError::DifficultyTooHigh)))); + } + + #[test] + fn test_proof_of_work_manager() { + let mut manager = ProofOfWorkManager::new(Duration::from_millis(100)); + + let proof = manager.generate_tracked_proof("test_challenge").unwrap(); + assert!(proof.is_valid()); + + let stats = manager.get_stats(); + assert_eq!(stats.proofs_generated, 1); + assert!(stats.avg_generation_time.as_millis() > 0); + } + + #[test] + fn test_hash_meets_difficulty() { + assert!(ProofOfWork::meets_difficulty("0000abc123", 4)); + assert!(ProofOfWork::meets_difficulty("000abc123", 3)); + assert!(ProofOfWork::meets_difficulty("00abc123", 2)); + assert!(ProofOfWork::meets_difficulty("0abc123", 1)); + assert!(ProofOfWork::meets_difficulty("abc123", 0)); + + assert!(!ProofOfWork::meets_difficulty("abc123", 1)); + assert!(!ProofOfWork::meets_difficulty("0abc123", 2)); + assert!(!ProofOfWork::meets_difficulty("00abc123", 3)); + } +} \ No newline at end of file diff --git a/src/steganography.rs b/src/steganography.rs new file mode 100644 index 0000000..bed87b0 --- /dev/null +++ b/src/steganography.rs @@ -0,0 +1,530 @@ +//! Steganography module for embedding CLIP vectors in images +//! +//! This module provides functionality to embed CLIP vectors (512 or 768 floats) +//! into images using LSB (Least Significant Bit) steganography. This allows +//! for hiding proof-of-work data in images that can be posted to Twitter. + +use image::{Rgb, RgbImage}; +use crate::error::{Result, SteganographyError}; +use serde::{Deserialize, Serialize}; + +/// Magic header to identify embedded data +const MAGIC_HEADER: &[u8] = b"RMCLIP"; + +/// Metadata about embedded vector +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmbeddedVectorMeta { + /// Version of the encoding format + pub version: u8, + /// Dimension of the embedded vector + pub dimension: u32, + /// Salt used in the commitment + pub salt: String, + /// Round ID for verification + pub round_id: String, + /// Optional checksum for data integrity + pub checksum: Option, +} + +/// Steganography encoder/decoder for CLIP vectors +#[derive(Debug, Clone)] +pub struct VectorSteganographer { + /// Number of bits to use per color channel (1-8) + bits_per_channel: u8, +} + +impl VectorSteganographer { + /// Create a new steganographer with default settings (2 bits per channel) + pub fn new() -> Self { + Self { + bits_per_channel: 2, + } + } + + /// Create a steganographer with custom bits per channel + /// Higher values hide more data but are more detectable + /// Recommended: 1-3 bits for better stealth + pub fn with_bits_per_channel(bits_per_channel: u8) -> Result { + if bits_per_channel == 0 || bits_per_channel > 8 { + return Err(SteganographyError::InvalidConfiguration.into()); + } + + Ok(Self { bits_per_channel }) + } + + /// Embed a CLIP vector into an image + /// + /// # Arguments + /// * `image_path` - Path to the source image + /// * `vector` - CLIP vector (f64 values, typically 512 or 768 elements) + /// * `meta` - Metadata about the vector + /// * `output_path` - Path where the encoded image will be saved + pub fn embed_vector( + &self, + image_path: &str, + vector: &[f64], + meta: &EmbeddedVectorMeta, + output_path: &str, + ) -> Result<()> { + // Load the image + let mut img = image::open(image_path) + .map_err(|_| SteganographyError::InvalidImage)? + .to_rgb8(); + + // Convert vector to bytes + let vector_bytes = self.vector_to_bytes(vector)?; + + // Serialize metadata + let meta_bytes = serde_json::to_vec(meta) + .map_err(|_| SteganographyError::EncodingFailed)?; + + // Create payload: MAGIC_HEADER + meta_length + meta + vector_length + vector + let mut payload = Vec::new(); + payload.extend_from_slice(MAGIC_HEADER); + payload.extend_from_slice(&(meta_bytes.len() as u32).to_le_bytes()); + payload.extend_from_slice(&meta_bytes); + payload.extend_from_slice(&(vector_bytes.len() as u32).to_le_bytes()); + payload.extend_from_slice(&vector_bytes); + + // Check if image can hold the payload + let max_capacity = self.calculate_capacity(&img); + if payload.len() > max_capacity { + return Err(SteganographyError::InsufficientCapacity.into()); + } + + // Embed the payload + self.embed_bytes(&mut img, &payload)?; + + // Save the modified image + img.save(output_path) + .map_err(|_| SteganographyError::SaveFailed)?; + + Ok(()) + } + + /// Extract a CLIP vector from an image + /// + /// # Arguments + /// * `image_path` - Path to the image containing embedded data + /// + /// # Returns + /// Tuple of (vector, metadata) + pub fn extract_vector(&self, image_path: &str) -> Result<(Vec, EmbeddedVectorMeta)> { + // Load the image + let img = image::open(image_path) + .map_err(|_| SteganographyError::InvalidImage)? + .to_rgb8(); + + // Try to extract the payload + let payload = self.extract_bytes(&img)?; + + // Verify magic header + if payload.len() < MAGIC_HEADER.len() || &payload[..MAGIC_HEADER.len()] != MAGIC_HEADER { + return Err(SteganographyError::NoEmbeddedData.into()); + } + + let mut offset = MAGIC_HEADER.len(); + + // Read metadata length + if payload.len() < offset + 4 { + return Err(SteganographyError::CorruptedData.into()); + } + let meta_len = u32::from_le_bytes([ + payload[offset], payload[offset + 1], payload[offset + 2], payload[offset + 3] + ]) as usize; + offset += 4; + + // Read metadata + if payload.len() < offset + meta_len { + return Err(SteganographyError::CorruptedData.into()); + } + let meta_bytes = &payload[offset..offset + meta_len]; + let meta: EmbeddedVectorMeta = serde_json::from_slice(meta_bytes) + .map_err(|_| SteganographyError::CorruptedData)?; + offset += meta_len; + + // Read vector length + if payload.len() < offset + 4 { + return Err(SteganographyError::CorruptedData.into()); + } + let vector_len = u32::from_le_bytes([ + payload[offset], payload[offset + 1], payload[offset + 2], payload[offset + 3] + ]) as usize; + offset += 4; + + // Read vector data + if payload.len() < offset + vector_len { + return Err(SteganographyError::CorruptedData.into()); + } + let vector_bytes = &payload[offset..offset + vector_len]; + + // Convert bytes back to vector + let vector = self.bytes_to_vector(vector_bytes, meta.dimension as usize)?; + + Ok((vector, meta)) + } + + /// Check if an image contains embedded data + pub fn has_embedded_data(&self, image_path: &str) -> bool { + match self.extract_vector(image_path) { + Ok(_) => true, + Err(_) => false, + } + } + + /// Calculate the maximum capacity of an image in bytes + pub fn calculate_capacity(&self, img: &RgbImage) -> usize { + let (width, height) = img.dimensions(); + let total_pixels = (width * height) as usize; + let total_channels = total_pixels * 3; // RGB + let bits_available = total_channels * self.bits_per_channel as usize; + bits_available / 8 // Convert to bytes + } + + /// Convert f64 vector to bytes for embedding + fn vector_to_bytes(&self, vector: &[f64]) -> Result> { + let mut bytes = Vec::new(); + for &value in vector { + bytes.extend_from_slice(&value.to_le_bytes()); + } + Ok(bytes) + } + + /// Convert bytes back to f64 vector + fn bytes_to_vector(&self, bytes: &[u8], dimension: usize) -> Result> { + if bytes.len() != dimension * 8 { + return Err(SteganographyError::CorruptedData.into()); + } + + let mut vector = Vec::with_capacity(dimension); + for i in 0..dimension { + let start = i * 8; + let float_bytes = [ + bytes[start], bytes[start + 1], bytes[start + 2], bytes[start + 3], + bytes[start + 4], bytes[start + 5], bytes[start + 6], bytes[start + 7], + ]; + vector.push(f64::from_le_bytes(float_bytes)); + } + + Ok(vector) + } + + /// Embed bytes into image using LSB steganography + fn embed_bytes(&self, img: &mut RgbImage, data: &[u8]) -> Result<()> { + let (width, height) = img.dimensions(); + let mut bit_index = 0; + let total_bits = data.len() * 8; + + let mask = (1u8 << self.bits_per_channel) - 1; // Create mask for extracting bits + let clear_mask = !((1u8 << self.bits_per_channel) - 1); // Mask for clearing LSBs + + 'outer: for y in 0..height { + for x in 0..width { + let pixel = img.get_pixel_mut(x, y); + + // Process each color channel (R, G, B) + for channel in 0..3 { + if bit_index >= total_bits { + break 'outer; + } + + // Extract bits from data + let byte_index = bit_index / 8; + let bit_offset = bit_index % 8; + + let mut bits_to_embed = 0u8; + for i in 0..self.bits_per_channel { + if bit_index + i as usize >= total_bits { + break; + } + + let data_bit = (data[byte_index] >> (bit_offset + i as usize)) & 1; + bits_to_embed |= data_bit << i; + } + + // Clear LSBs and embed new bits + pixel[channel] = (pixel[channel] & clear_mask) | (bits_to_embed & mask); + + bit_index += self.bits_per_channel as usize; + } + } + } + + if bit_index < total_bits { + return Err(SteganographyError::InsufficientCapacity.into()); + } + + Ok(()) + } + + /// Extract bytes from image using LSB steganography + fn extract_bytes(&self, img: &RgbImage) -> Result> { + let (width, height) = img.dimensions(); + let mut extracted_bits = Vec::new(); + + let mask = (1u8 << self.bits_per_channel) - 1; + + // First, extract enough bits to read the magic header and sizes + let min_header_bytes = MAGIC_HEADER.len() + 4 + 4; // magic + meta_len + vector_len + let min_bits_needed = min_header_bytes * 8; + + for y in 0..height { + for x in 0..width { + let pixel = img.get_pixel(x, y); + + for channel in 0..3 { + let channel_bits = pixel[channel] & mask; + + // Extract individual bits + for i in 0..self.bits_per_channel { + if extracted_bits.len() >= min_bits_needed && extracted_bits.len() >= self.calculate_total_bits_needed(&extracted_bits)? { + return self.bits_to_bytes(&extracted_bits); + } + + let bit = (channel_bits >> i) & 1; + extracted_bits.push(bit); + } + } + } + } + + // Convert bits to bytes for final result + self.bits_to_bytes(&extracted_bits) + } + + /// Calculate total bits needed based on extracted header information + fn calculate_total_bits_needed(&self, bits: &[u8]) -> Result { + if bits.len() < (MAGIC_HEADER.len() + 8) * 8 { + return Ok(usize::MAX); // Need more bits + } + + // Convert initial bits to bytes to read header + let initial_bytes = self.bits_to_bytes(&bits[0..(MAGIC_HEADER.len() + 8) * 8])?; + + // Verify magic header + if &initial_bytes[..MAGIC_HEADER.len()] != MAGIC_HEADER { + return Err(SteganographyError::NoEmbeddedData.into()); + } + + let mut offset = MAGIC_HEADER.len(); + + // Read metadata length + let meta_len = u32::from_le_bytes([ + initial_bytes[offset], initial_bytes[offset + 1], + initial_bytes[offset + 2], initial_bytes[offset + 3] + ]) as usize; + offset += 4; + + // Read vector length + let vector_len = u32::from_le_bytes([ + initial_bytes[offset], initial_bytes[offset + 1], + initial_bytes[offset + 2], initial_bytes[offset + 3] + ]) as usize; + + let total_payload_bytes = MAGIC_HEADER.len() + 4 + meta_len + 4 + vector_len; + Ok(total_payload_bytes * 8) + } + + /// Convert bit array to byte array + fn bits_to_bytes(&self, bits: &[u8]) -> Result> { + let mut bytes = Vec::new(); + + for chunk in bits.chunks(8) { + let mut byte = 0u8; + for (i, &bit) in chunk.iter().enumerate() { + byte |= bit << i; + } + bytes.push(byte); + } + + Ok(bytes) + } +} + +impl Default for VectorSteganographer { + fn default() -> Self { + Self::new() + } +} + +/// Utility functions for working with steganography +pub mod utils { + use super::*; + + /// Create a test image suitable for steganography + /// Returns path to the created image + pub fn create_test_image(width: u32, height: u32, output_path: &str) -> Result<()> { + use rand::Rng; + let mut rng = rand::thread_rng(); + + let mut img = RgbImage::new(width, height); + + for y in 0..height { + for x in 0..width { + let r = rng.gen_range(0..=255); + let g = rng.gen_range(0..=255); + let b = rng.gen_range(0..=255); + img.put_pixel(x, y, Rgb([r, g, b])); + } + } + + img.save(output_path) + .map_err(|_| SteganographyError::SaveFailed)?; + + Ok(()) + } + + /// Calculate the minimum image size needed for a given vector + pub fn min_image_size_for_vector(vector_len: usize, bits_per_channel: u8) -> (u32, u32) { + let vector_bytes = vector_len * 8; // f64 = 8 bytes each + let meta_bytes = 256; // Estimated metadata size + let header_bytes = 6 + 8; // Magic header + lengths + let total_bytes = vector_bytes + meta_bytes + header_bytes; + let total_bits = total_bytes * 8; + + let bits_per_pixel = 3 * bits_per_channel as usize; // RGB channels + let pixels_needed = (total_bits + bits_per_pixel - 1) / bits_per_pixel; // Ceiling division + + // Find square-ish dimensions + let side = ((pixels_needed as f64).sqrt().ceil() as u32).max(64); // Minimum 64x64 + (side, side) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use std::path::PathBuf; + + fn create_test_vector() -> Vec { + (0..512).map(|i| (i as f64) / 512.0).collect() + } + + fn create_test_meta() -> EmbeddedVectorMeta { + EmbeddedVectorMeta { + version: 1, + dimension: 512, + salt: "test_salt_123".to_string(), + round_id: "test_round".to_string(), + checksum: None, + } + } + + #[test] + fn test_embed_and_extract_vector() { + let temp_dir = TempDir::new().unwrap(); + let test_img_path = temp_dir.path().join("test.png"); + let encoded_img_path = temp_dir.path().join("encoded.png"); + + // Create test image + utils::create_test_image(512, 512, test_img_path.to_str().unwrap()).unwrap(); + + let steganographer = VectorSteganographer::new(); + let test_vector = create_test_vector(); + let test_meta = create_test_meta(); + + // Embed vector + steganographer.embed_vector( + test_img_path.to_str().unwrap(), + &test_vector, + &test_meta, + encoded_img_path.to_str().unwrap(), + ).unwrap(); + + // Extract vector + let (extracted_vector, extracted_meta) = steganographer + .extract_vector(encoded_img_path.to_str().unwrap()) + .unwrap(); + + // Verify + assert_eq!(extracted_vector.len(), test_vector.len()); + assert_eq!(extracted_meta.dimension, test_meta.dimension); + assert_eq!(extracted_meta.salt, test_meta.salt); + assert_eq!(extracted_meta.round_id, test_meta.round_id); + + // Check vector values (should be very close due to floating point precision) + for (original, extracted) in test_vector.iter().zip(extracted_vector.iter()) { + assert!((original - extracted).abs() < 1e-10); + } + } + + #[test] + fn test_capacity_calculation() { + let temp_dir = TempDir::new().unwrap(); + let test_img_path = temp_dir.path().join("test.png"); + + utils::create_test_image(100, 100, test_img_path.to_str().unwrap()).unwrap(); + + let img = image::open(test_img_path).unwrap().to_rgb8(); + let steganographer = VectorSteganographer::new(); + + let capacity = steganographer.calculate_capacity(&img); + + // 100x100 pixels * 3 channels * 2 bits per channel / 8 bits per byte + let expected_capacity = 100 * 100 * 3 * 2 / 8; + assert_eq!(capacity, expected_capacity); + } + + #[test] + fn test_has_embedded_data() { + let temp_dir = TempDir::new().unwrap(); + let test_img_path = temp_dir.path().join("test.png"); + let encoded_img_path = temp_dir.path().join("encoded.png"); + + utils::create_test_image(256, 256, test_img_path.to_str().unwrap()).unwrap(); + + let steganographer = VectorSteganographer::new(); + + // Original image should not have embedded data + assert!(!steganographer.has_embedded_data(test_img_path.to_str().unwrap())); + + // Embed data + let test_vector = create_test_vector(); + let test_meta = create_test_meta(); + + steganographer.embed_vector( + test_img_path.to_str().unwrap(), + &test_vector, + &test_meta, + encoded_img_path.to_str().unwrap(), + ).unwrap(); + + // Encoded image should have embedded data + assert!(steganographer.has_embedded_data(encoded_img_path.to_str().unwrap())); + } + + #[test] + fn test_insufficient_capacity() { + let temp_dir = TempDir::new().unwrap(); + let test_img_path = temp_dir.path().join("small.png"); + let encoded_img_path = temp_dir.path().join("encoded.png"); + + // Create very small image + utils::create_test_image(10, 10, test_img_path.to_str().unwrap()).unwrap(); + + let steganographer = VectorSteganographer::new(); + let test_vector = create_test_vector(); // 512 f64 values = 4KB+ + let test_meta = create_test_meta(); + + // Should fail due to insufficient capacity + let result = steganographer.embed_vector( + test_img_path.to_str().unwrap(), + &test_vector, + &test_meta, + encoded_img_path.to_str().unwrap(), + ); + + assert!(matches!(result, Err(crate::error::RealMirError::Steganography(SteganographyError::InsufficientCapacity)))); + } + + #[test] + fn test_min_image_size_calculation() { + let (width, height) = utils::min_image_size_for_vector(512, 2); + + // Should be reasonable dimensions that can hold a 512-element vector + assert!(width >= 64); + assert!(height >= 64); + assert!(width * height >= 10000); // Should have enough pixels + } +} \ No newline at end of file