use std::path::Path;
use anyhow::anyhow;
use rand_chacha::ChaCha20Rng;
use rand_core::{CryptoRng, RngCore, SeedableRng};
use crate::StdResult;
use crate::crypto_helper::GenesisEd25519SecretKey;
use crate::crypto_helper::GenesisEd25519Signer;
use crate::crypto_helper::GenesisVerifier;
#[cfg(feature = "future_snark")]
use crate::crypto_helper::{
BUNDLE_FIRST_HEX_CHAR, GenesisBundleError, GenesisSchnorrSigner, GenesisSigningKeyBundle,
GenesisVerificationKeyBundle, LEGACY_FIRST_HEX_CHAR, PREIMAGE_SIZE, ProtocolKey, sha256_digest,
};
use crate::entities::{CertificateSignature, ProtocolMessage, SupportedEra};
use crate::protocol::ToMessage;
#[derive(Debug, Clone)]
pub struct GenesisSigner {
pub ed25519: GenesisEd25519Signer,
#[cfg(feature = "future_snark")]
pub schnorr: Option<GenesisSchnorrSigner>,
}
impl GenesisSigner {
pub fn from_ed25519(ed25519: GenesisEd25519Signer) -> Self {
Self {
ed25519,
#[cfg(feature = "future_snark")]
schnorr: None,
}
}
pub fn create_deterministic_signer() -> Self {
Self {
ed25519: GenesisEd25519Signer::create_deterministic_signer(),
#[cfg(feature = "future_snark")]
schnorr: Some(GenesisSchnorrSigner::create_deterministic_signer()),
}
}
#[cfg(feature = "future_snark")]
pub fn from_bundle(bundle: GenesisSigningKeyBundle) -> Self {
Self {
ed25519: GenesisEd25519Signer::from_secret_key(bundle.ed25519),
schnorr: Some(GenesisSchnorrSigner::from_secret_key(bundle.schnorr)),
}
}
pub fn create_verifier(&self) -> GenesisVerifier {
GenesisVerifier {
ed25519: self.ed25519.create_verifier(),
#[cfg(feature = "future_snark")]
schnorr: self.schnorr.as_ref().map(|schnorr| schnorr.create_verifier()),
}
}
pub fn try_from_hex(raw: &str) -> StdResult<Self> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(anyhow!("genesis signer input is empty"));
}
#[cfg(feature = "future_snark")]
{
match trimmed.as_bytes().first() {
Some(&BUNDLE_FIRST_HEX_CHAR) => {
let bundle = GenesisSigningKeyBundle::try_from_hex(trimmed)?;
Ok(Self::from_bundle(bundle))
}
Some(&LEGACY_FIRST_HEX_CHAR) => {
let secret_key = GenesisEd25519SecretKey::from_json_hex(trimmed)?;
Ok(Self::from_ed25519(GenesisEd25519Signer::from_secret_key(
secret_key,
)))
}
_ => Err(anyhow!(
"unrecognised genesis signing key format: expected dual bundle or legacy Ed25519 hex"
)),
}
}
#[cfg(not(feature = "future_snark"))]
{
let secret_key = GenesisEd25519SecretKey::from_json_hex(trimmed)?;
Ok(Self::from_ed25519(GenesisEd25519Signer::from_secret_key(
secret_key,
)))
}
}
pub fn read_from_file(path: &Path) -> StdResult<Self> {
let raw = std::fs::read_to_string(path)?;
Self::try_from_hex(&raw)
}
pub fn sign<R: CryptoRng + RngCore>(
&self,
protocol_message: &ProtocolMessage,
mithril_era: SupportedEra,
#[cfg_attr(not(feature = "future_snark"), allow(unused_variables))] rng: &mut R,
) -> StdResult<CertificateSignature> {
let ed25519_signature = self.ed25519.sign(protocol_message.to_message().as_bytes());
match mithril_era {
SupportedEra::Pythagoras => {
Ok(CertificateSignature::GenesisSignature(ed25519_signature))
}
#[cfg(feature = "future_snark")]
SupportedEra::Lagrange => {
let schnorr_signer =
self.schnorr.as_ref().ok_or(GenesisBundleError::LegacySigningKey)?;
if !protocol_message.is_rigid() {
return Err(anyhow!(
"Lagrange genesis signing requires a rigid protocol message"
));
}
protocol_message.check_rigid_integrity()?;
let preimage = protocol_message.rigid_preimage();
if preimage.len() != PREIMAGE_SIZE {
return Err(anyhow!(
"rigid protocol-message preimage must be exactly {PREIMAGE_SIZE} bytes (got {})",
preimage.len()
));
}
let schnorr_signature = schnorr_signer.sign(&sha256_digest(&preimage), rng)?;
Ok(CertificateSignature::GenesisDualSignature(
ed25519_signature,
schnorr_signature,
))
}
#[cfg(not(feature = "future_snark"))]
SupportedEra::Lagrange => Ok(CertificateSignature::GenesisSignature(ed25519_signature)),
}
}
pub(crate) fn sign_deterministic(
&self,
protocol_message: &ProtocolMessage,
mithril_era: SupportedEra,
) -> StdResult<CertificateSignature> {
self.sign(
protocol_message,
mithril_era,
&mut ChaCha20Rng::from_seed([0u8; 32]),
)
}
pub fn sign_non_deterministic(
&self,
protocol_message: &ProtocolMessage,
mithril_era: SupportedEra,
) -> StdResult<CertificateSignature> {
self.sign(protocol_message, mithril_era, &mut rand_core::OsRng)
}
#[cfg(feature = "future_snark")]
pub fn verification_key_bundle(&self) -> Option<GenesisVerificationKeyBundle> {
self.schnorr.as_ref().map(|schnorr| GenesisVerificationKeyBundle {
ed25519: self.ed25519.verification_key(),
schnorr: schnorr.verification_key(),
})
}
pub fn write_to_file(&self, path: &Path) -> StdResult<()> {
#[cfg(feature = "future_snark")]
{
if let Some(schnorr) = self.schnorr.as_ref() {
let bundle =
GenesisSigningKeyBundle::new(self.ed25519.secret_key(), schnorr.secret_key());
let hex_string = ProtocolKey::new(bundle).to_bytes_hex()?;
std::fs::write(path, hex_string)?;
return Ok(());
}
}
self.ed25519.secret_key().write_json_hex_to_file(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::temp_dir_create;
#[test]
fn try_from_hex_accepts_legacy_single_ed25519() {
let signer = GenesisEd25519Signer::create_deterministic_signer();
let legacy_hex = signer.secret_key().to_json_hex().unwrap();
let parsed = GenesisSigner::try_from_hex(&legacy_hex).unwrap();
assert_eq!(
parsed.ed25519.secret_key().to_bytes(),
signer.secret_key().to_bytes()
);
#[cfg(feature = "future_snark")]
assert!(parsed.schnorr.is_none());
}
#[test]
fn create_deterministic_signer_is_reproducible() {
let first = GenesisSigner::create_deterministic_signer();
let second = GenesisSigner::create_deterministic_signer();
assert_eq!(
first.ed25519.secret_key().to_bytes(),
second.ed25519.secret_key().to_bytes()
);
}
#[test]
fn create_verifier_verifies_an_ed25519_signature() {
let signer = GenesisSigner::create_deterministic_signer();
let message = b"genesis-message";
let signature = signer.ed25519.sign(message);
signer
.create_verifier()
.verify_ed25519(message, &signature)
.expect("the derived verifier must accept a signature from its signer");
}
#[test]
fn try_from_hex_rejects_empty_input() {
GenesisSigner::try_from_hex(" ").expect_err("empty input must be rejected");
}
#[test]
fn read_from_file_round_trips_legacy() {
let temp = temp_dir_create!();
let signer = GenesisEd25519Signer::create_deterministic_signer();
let path = temp.join("genesis.sk");
signer.secret_key().write_json_hex_to_file(&path).unwrap();
let restored = GenesisSigner::read_from_file(&path).unwrap();
assert_eq!(
restored.ed25519.secret_key().to_bytes(),
signer.secret_key().to_bytes()
);
}
#[cfg(feature = "future_snark")]
mod schnorr {
use rand_chacha::ChaCha20Rng;
use rand_core::SeedableRng;
use super::*;
fn build_bundle() -> GenesisSigningKeyBundle {
let ed25519 = GenesisEd25519Signer::create_deterministic_signer();
let mut rng = ChaCha20Rng::from_seed([7u8; 32]);
let schnorr = GenesisSchnorrSigner::generate(&mut rng);
GenesisSigningKeyBundle::new(ed25519.secret_key(), schnorr.secret_key())
}
#[test]
fn sign_lagrange_rejects_a_non_rigid_protocol_message() {
let signer = GenesisSigner::from_bundle(build_bundle());
let mut rng = ChaCha20Rng::from_seed([3u8; 32]);
let error = signer
.sign(&ProtocolMessage::new(), SupportedEra::Lagrange, &mut rng)
.expect_err("Lagrange signing must reject a non-rigid protocol message");
assert!(error.to_string().contains("rigid protocol message"));
}
#[test]
fn from_bundle_pairs_both_halves() {
let bundle = build_bundle();
let expected_ed25519 = bundle.ed25519.to_bytes();
let expected_schnorr = bundle.schnorr.to_bytes();
let signer = GenesisSigner::from_bundle(bundle);
assert_eq!(signer.ed25519.secret_key().to_bytes(), expected_ed25519);
assert_eq!(
signer.schnorr.as_ref().unwrap().secret_key().to_bytes(),
expected_schnorr
);
}
#[test]
fn try_from_hex_accepts_dual_bundle() {
let bundle = build_bundle();
let expected_schnorr = bundle.schnorr.to_bytes();
let hex_string = ProtocolKey::new(bundle).to_bytes_hex().unwrap();
let parsed = GenesisSigner::try_from_hex(&hex_string).unwrap();
assert_eq!(
parsed.schnorr.as_ref().unwrap().secret_key().to_bytes(),
expected_schnorr
);
}
#[test]
fn verification_key_bundle_mirrors_signer_halves() {
let signer = GenesisSigner::from_bundle(build_bundle());
let expected_schnorr = signer.schnorr.as_ref().unwrap().verification_key().to_bytes();
let bundle = signer.verification_key_bundle().unwrap();
assert_eq!(
bundle.ed25519.as_bytes(),
signer.ed25519.verification_key().as_bytes()
);
assert_eq!(bundle.schnorr.to_bytes(), expected_schnorr);
}
#[test]
fn write_to_file_round_trips_bundle() {
let temp = temp_dir_create!();
let signer = GenesisSigner::from_bundle(build_bundle());
let path = temp.join("genesis.sk");
signer.write_to_file(&path).unwrap();
let restored = GenesisSigner::read_from_file(&path).unwrap();
assert_eq!(
restored.ed25519.secret_key().to_bytes(),
signer.ed25519.secret_key().to_bytes()
);
assert_eq!(
restored.schnorr.as_ref().unwrap().secret_key().to_bytes(),
signer.schnorr.as_ref().unwrap().secret_key().to_bytes()
);
}
}
}