Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
108 changes: 68 additions & 40 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <SIGTopLevelTargetSumLifetime32Dim64Base8 as SignatureScheme>::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)]
Expand All @@ -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,
Expand All @@ -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)?;
Expand All @@ -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
Expand All @@ -86,51 +102,63 @@ 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);

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<String, Box<dyn std::error::Error>> {
// 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<String, Box<dyn std::error::Error>> {
// 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));
Expand Down Expand Up @@ -161,17 +189,17 @@ 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)
))?;

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)?;
}
Expand Down