diff --git a/README.md b/README.md index 3a4c1d7..b5c235c 100644 --- a/README.md +++ b/README.md @@ -25,27 +25,39 @@ Generate validator key pairs for hash-based signatures: cargo run --release --bin hashsig -- generate \ --num-validators 5 \ --log-num-active-epochs 18 \ - --output-dir ./generated_keys + --output-dir ./generated_keys \ + --export-format both ``` **Parameters:** - `--num-validators`: Number of validator key pairs to generate - `--log-num-active-epochs`: Log2 of the number of active epochs (e.g., 18 for 2^18 = 262,144 active epochs) - `--output-dir`: Directory where keys will be saved +- `--export-format`: Key export format, one of: + - `both` (default): export **SSZ binaries** (`.ssz`) and **legacy JSON** (`.json`) + - `ssz`: export **only** SSZ binaries (`.ssz`) - `--create-manifest`: Create a manifest file (optional, defaults to `true`) -**Output:** -The tool creates a directory with key pairs in JSON format: +**Output (default `--export-format both`):** +The tool creates a directory with key pairs exported as **SSZ-encoded binary files** plus **legacy JSON**: ``` generated_keys/ ├── validator-keys-manifest.yaml # Manifest file (if --create-manifest is true) -├── validator_0_pk.json # Public key for validator 0 -├── validator_0_sk.json # Secret key for validator 0 -├── validator_1_pk.json # Public key for validator 1 -├── validator_1_sk.json # Secret key for validator 1 +├── validator_0_pk.ssz # Public key for validator 0 (SSZ bytes) +├── validator_0_sk.ssz # Secret key for validator 0 (SSZ bytes) +├── validator_0_pk.json # Public key for validator 0 (legacy JSON) +├── validator_0_sk.json # Secret key for validator 0 (legacy JSON) +├── validator_1_pk.ssz # Public key for validator 1 (SSZ bytes) +├── validator_1_sk.ssz # Secret key for validator 1 (SSZ bytes) +├── validator_1_pk.json # Public key for validator 1 (legacy JSON) +├── validator_1_sk.json # Secret key for validator 1 (legacy JSON) └── ... ``` +The `.ssz` files contain the **canonical SSZ serialization** (`to_bytes()`) of the underlying key types from `leanSig`, written directly as raw bytes (not JSON or hex). + +The `.json` files are provided **only for backwards compatibility** and may be removed in a future version once all clients consume SSZ. + ## Current Implementation Currently uses the `SIGTopLevelTargetSumLifetime32Dim64Base8` scheme: diff --git a/src/main.rs b/src/main.rs index 6599932..edecbbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,15 +2,20 @@ use std::fs::{self, File}; use std::io::Write; use std::path::PathBuf; -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use leansig::serialization::Serializable; use leansig::signature::{ generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8, SignatureScheme, }; -// Type alias for the public key type -type PublicKeyType = ::PublicKey; +#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)] +enum ExportFormat { + /// Export only SSZ-encoded binary files (`.ssz`) + Ssz, + /// Export both SSZ-encoded binaries (`.ssz`) and legacy JSON files + Both, +} /// A CLI tool to generate cryptographic keys for hash-based signatures. #[derive(Parser, Debug)] @@ -36,6 +41,10 @@ enum Commands { #[arg(long)] output_dir: PathBuf, + /// Export format for keys: `ssz` (binary only) or `both` (SSZ + JSON, legacy) + #[arg(long, value_enum, default_value_t = ExportFormat::Both)] + export_format: ExportFormat, + /// Create a manifest file for validator keys #[arg(long, default_value = "true")] create_manifest: bool, @@ -50,9 +59,15 @@ fn main() -> std::io::Result<()> { num_validators, log_num_active_epochs, output_dir, + export_format, create_manifest, } => { - generate_keys(num_validators, log_num_active_epochs, output_dir.clone())?; + generate_keys( + num_validators, + log_num_active_epochs, + export_format, + output_dir.clone(), + )?; if create_manifest { create_validator_manifest(&output_dir, num_validators, log_num_active_epochs)?; @@ -66,6 +81,7 @@ fn main() -> std::io::Result<()> { fn generate_keys( num_validators: usize, log_num_active_epochs: usize, + export_format: ExportFormat, output_dir: PathBuf, ) -> std::io::Result<()> { // Create the output directory if it doesn't exist @@ -86,26 +102,50 @@ fn generate_keys( let mut rng = rand::rng(); + let write_json = matches!(export_format, ExportFormat::Both); + for i in 0..num_validators { let key_prefix = format!("validator_{}", i); println!("Generating {}...", key_prefix); // Generate the key pair - let (pk, sk) = SIGTopLevelTargetSumLifetime32Dim64Base8::key_gen(&mut rng, 0, activation_duration); - - // Serialize the public key - let pk_json = serde_json::to_string_pretty(&pk).expect("Failed to serialize public key"); - let mut pk_file = File::create(output_dir.join(format!("{}_pk.json", key_prefix)))?; - pk_file.write_all(pk_json.as_bytes())?; - - // Serialize the secret key - let sk_json = serde_json::to_string_pretty(&sk).expect("Failed to serialize secret key"); - let mut sk_file = File::create(output_dir.join(format!("{}_sk.json", key_prefix)))?; - sk_file.write_all(sk_json.as_bytes())?; - - println!(" ✅ {}_pk.json", key_prefix); - println!(" ✅ {}_sk.json", key_prefix); + let (pk, sk) = SIGTopLevelTargetSumLifetime32Dim64Base8::key_gen( + &mut rng, + 0, + activation_duration, + ); + + // Serialize the public key to SSZ bytes and write to a binary .ssz file + let pk_bytes = pk.to_bytes(); + let mut pk_file = File::create(output_dir.join(format!("{}_pk.ssz", key_prefix)))?; + pk_file.write_all(&pk_bytes)?; + + // Serialize the secret key to SSZ bytes and write to a binary .ssz file + let sk_bytes = sk.to_bytes(); + let mut sk_file = File::create(output_dir.join(format!("{}_sk.ssz", key_prefix)))?; + sk_file.write_all(&sk_bytes)?; + + println!(" ✅ {}_pk.ssz", key_prefix); + println!(" ✅ {}_sk.ssz", key_prefix); + + if write_json { + // Also export legacy JSON representations for backwards compatibility + let pk_json = + serde_json::to_string_pretty(&pk).expect("Failed to serialize public key to JSON"); + let mut pk_json_file = + File::create(output_dir.join(format!("{}_pk.json", key_prefix)))?; + pk_json_file.write_all(pk_json.as_bytes())?; + + let sk_json = + serde_json::to_string_pretty(&sk).expect("Failed to serialize secret key to JSON"); + let mut sk_json_file = + File::create(output_dir.join(format!("{}_sk.json", key_prefix)))?; + sk_json_file.write_all(sk_json.as_bytes())?; + + println!(" ⚠️ (legacy) {}_pk.json", key_prefix); + println!(" ⚠️ (legacy) {}_sk.json", key_prefix); + } } println!("\n✅ Successfully generated and saved {} validator key pairs.", num_validators); @@ -113,24 +153,12 @@ fn generate_keys( Ok(()) } -/// Convert pubkey JSON file to hex string -/// Reads the JSON file, deserializes into PublicKey type using serde, -/// then uses SSZ serialization (to_bytes) to get canonical form bytes. -/// Returns hex string with "0x" prefix. -/// -/// Note: The JSON file contains field elements in Montgomery form (internal representation). -/// We must use to_bytes() which performs SSZ serialization to get the canonical form -/// that is expected by from_bytes() during signature verification. -fn pubkey_json_to_hex(pk_file_path: &PathBuf) -> Result> { - // Read JSON file - let json_content = fs::read_to_string(pk_file_path)?; - - // Deserialize into PublicKeyType using serde (this handles Montgomery form) - let public_key: PublicKeyType = serde_json::from_str(&json_content)?; - - // Use to_bytes() which uses SSZ serialization (canonical form) - // This is the correct format expected by from_bytes() during verification - let pubkey_bytes = public_key.to_bytes(); +/// Convert pubkey SSZ file to hex string +/// Reads the `.ssz` file as raw bytes (already in SSZ/canonical form) +/// and returns a hex string with "0x" prefix. +fn pubkey_ssz_to_hex(pk_file_path: &PathBuf) -> Result> { + // Read SSZ bytes from file + let pubkey_bytes = fs::read(pk_file_path)?; // Convert bytes to hex string with "0x" prefix let hex_string = format!("0x{}", hex::encode(&pubkey_bytes)); @@ -161,9 +189,9 @@ fn create_validator_manifest( writeln!(manifest_file, "validators:")?; for i in 0..num_validators { - // Read the pubkey JSON file and convert to hex - let pk_file_path = output_dir.join(format!("validator_{}_pk.json", i)); - let pubkey_hex = pubkey_json_to_hex(&pk_file_path) + // Read the pubkey SSZ file and convert to hex + let pk_file_path = output_dir.join(format!("validator_{}_pk.ssz", i)); + let pubkey_hex = pubkey_ssz_to_hex(&pk_file_path) .map_err(|e| std::io::Error::new( std::io::ErrorKind::Other, format!("Failed to convert pubkey to hex for validator {}: {}", i, e) @@ -171,7 +199,7 @@ fn create_validator_manifest( writeln!(manifest_file, " - index: {}", i)?; writeln!(manifest_file, " pubkey_hex: {}", pubkey_hex)?; - writeln!(manifest_file, " privkey_file: validator_{}_sk.json", i)?; + writeln!(manifest_file, " privkey_file: validator_{}_sk.ssz", i)?; if i < num_validators - 1 { writeln!(manifest_file)?; }