use anyhow::{Context, anyhow};
use rand_chacha::ChaCha20Rng;
use rand_core::{CryptoRng, RngCore, SeedableRng};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use mithril_stm::{BaseFieldElement, SchnorrSigningKey, StandardSchnorrSignature};
use crate::{StdError, StdResult};
pub type SchnorrSecretKey = SchnorrSigningKey;
pub type SchnorrVerificationKey = mithril_stm::SchnorrVerificationKey;
pub type SchnorrSignature = StandardSchnorrSignature;
pub const PREIMAGE_SIZE: usize = 190;
#[derive(Error, Debug)]
pub enum SchnorrError {
#[error("SNARK genesis expected a 32-byte SHA-256 digest, got {actual} bytes")]
InvalidDigestLength {
actual: usize,
},
#[error("SNARK genesis failed to reduce SHA-256 digest into the Jubjub base field")]
FieldReduction(#[source] StdError),
#[error("SNARK genesis signing failed")]
Sign(#[source] StdError),
#[error("SNARK genesis verification failed")]
Verify(#[source] StdError),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchnorrSigner {
secret_key: SchnorrSecretKey,
}
impl SchnorrSigner {
pub fn from_secret_key(secret_key: SchnorrSecretKey) -> Self {
Self { secret_key }
}
pub fn generate<R>(rng: &mut R) -> Self
where
R: CryptoRng + RngCore,
{
Self::from_secret_key(SchnorrSecretKey::generate(rng))
}
pub fn create_deterministic_signer() -> Self {
Self::generate(&mut ChaCha20Rng::from_seed([0u8; 32]))
}
pub fn create_non_deterministic_signer() -> Self {
Self::generate(&mut rand_core::OsRng)
}
pub fn secret_key(&self) -> SchnorrSecretKey {
self.secret_key.clone()
}
pub fn verification_key(&self) -> SchnorrVerificationKey {
SchnorrVerificationKey::new_from_signing_key(self.secret_key.clone())
}
pub(crate) fn sign<R>(&self, sha256_digest: &[u8], rng: &mut R) -> StdResult<SchnorrSignature>
where
R: CryptoRng + RngCore,
{
let field_element = SchnorrVerifier::digest_to_field_element(sha256_digest)?;
self.secret_key
.sign_standard(&[field_element], rng)
.map_err(|e| anyhow!(SchnorrError::Sign(e)))
.with_context(|| "SNARK genesis signer failed to produce a standard Schnorr signature")
}
pub fn sign_non_deterministic(&self, sha256_digest: &[u8]) -> StdResult<SchnorrSignature> {
self.sign(sha256_digest, &mut rand_core::OsRng)
}
pub fn create_verifier(&self) -> SchnorrVerifier {
SchnorrVerifier::from_verification_key(self.verification_key())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchnorrVerifier {
verification_key: SchnorrVerificationKey,
}
impl SchnorrVerifier {
pub fn from_verification_key(verification_key: SchnorrVerificationKey) -> Self {
Self { verification_key }
}
pub fn to_verification_key(&self) -> SchnorrVerificationKey {
self.verification_key
}
pub fn verify(&self, sha256_digest: &[u8], signature: &SchnorrSignature) -> StdResult<()> {
let field_element = Self::digest_to_field_element(sha256_digest)?;
signature
.verify(&[field_element], &self.verification_key)
.map_err(|e| anyhow!(SchnorrError::Verify(e)))
.with_context(|| "SNARK genesis verifier failed to verify a standard Schnorr signature")
}
pub fn digest_to_field_element(sha256_digest: &[u8]) -> StdResult<BaseFieldElement> {
if sha256_digest.len() != 32 {
return Err(SchnorrError::InvalidDigestLength {
actual: sha256_digest.len(),
}
.into());
}
let mut bytes = [0u8; 32];
bytes.copy_from_slice(sha256_digest);
BaseFieldElement::from_raw(&bytes)
.map_err(|e| anyhow!(SchnorrError::FieldReduction(e)))
.with_context(|| "Failed to reduce SHA-256 digest into the Jubjub base field")
}
}
pub fn sha256_digest(message: &[u8]) -> [u8; 32] {
Sha256::digest(message).into()
}
pub fn signed_message_from_digest(sha256_digest: &[u8]) -> String {
hex::encode(sha256_digest)
}
pub fn schnorr_signature_to_hex(signature: &SchnorrSignature) -> String {
hex::encode(signature.to_bytes())
}
pub fn schnorr_signature_from_hex(raw: &str) -> StdResult<SchnorrSignature> {
let bytes = hex::decode(raw).with_context(|| "Failed to hex-decode SNARK genesis signature")?;
SchnorrSignature::from_bytes(&bytes)
.with_context(|| "Failed to deserialise SNARK genesis signature from bytes")
}
#[cfg(test)]
mod tests {
use rand_chacha::ChaCha20Rng;
use rand_core::SeedableRng;
use super::*;
fn new_signer() -> SchnorrSigner {
let mut rng = ChaCha20Rng::from_seed([7u8; 32]);
SchnorrSigner::generate(&mut rng)
}
#[test]
fn sign_and_verify_a_random_digest() {
let signer = new_signer();
let verifier = signer.create_verifier();
let mut rng = ChaCha20Rng::from_seed([1u8; 32]);
let digest = [123u8; 32];
let signature = signer.sign(&digest, &mut rng).unwrap();
verifier.verify(&digest, &signature).expect("Signature should verify");
}
#[test]
fn schnorr_signature_from_hex_rejects_non_hex_input() {
schnorr_signature_from_hex("not-hex").expect_err("a non-hex string must fail to decode");
}
#[test]
fn schnorr_signature_from_hex_rejects_wrong_length() {
let wrong_length = hex::encode([0u8; 16]);
schnorr_signature_from_hex(&wrong_length)
.expect_err("a wrong-length payload must fail to deserialise");
}
#[test]
fn verification_fails_on_tampered_digest() {
let signer = new_signer();
let verifier = signer.create_verifier();
let mut rng = ChaCha20Rng::from_seed([2u8; 32]);
let digest = [0xABu8; 32];
let signature = signer.sign(&digest, &mut rng).unwrap();
let mut tampered = digest;
tampered[0] ^= 0xFF;
verifier
.verify(&tampered, &signature)
.expect_err("Tampered digest should not verify");
}
#[test]
fn signer_rejects_non_32_byte_input() {
let signer = new_signer();
let mut rng = ChaCha20Rng::from_seed([3u8; 32]);
signer
.sign(&[0u8; 16], &mut rng)
.expect_err("Signer must reject digests that are not 32 bytes");
signer
.sign(&[0u8; 64], &mut rng)
.expect_err("Signer must reject digests that are not 32 bytes");
}
#[test]
fn digest_above_field_modulus_round_trips_deterministically() {
let signer = new_signer();
let verifier = signer.create_verifier();
let mut rng = ChaCha20Rng::from_seed([5u8; 32]);
let high_bits_digest = [0xFFu8; 32];
let signature = signer.sign(&high_bits_digest, &mut rng).unwrap();
verifier
.verify(&high_bits_digest, &signature)
.expect("Digest above modulus must round-trip via reduction");
}
#[test]
fn verification_key_matches_signer_secret() {
let signer = new_signer();
let derived = SchnorrVerificationKey::new_from_signing_key(signer.secret_key());
assert_eq!(signer.verification_key(), derived);
}
#[test]
fn sha256_digest_matches_direct_sha256() {
let payload = b"genesis-preimage";
assert_eq!(
sha256_digest(payload),
<[u8; 32]>::from(Sha256::digest(payload))
);
}
#[test]
fn two_signatures_of_same_digest_under_distinct_rngs_differ() {
let signer = new_signer();
let verifier = signer.create_verifier();
let digest = [0x99u8; 32];
let mut rng_a = ChaCha20Rng::from_seed([10u8; 32]);
let mut rng_b = ChaCha20Rng::from_seed([11u8; 32]);
let signature_a = signer.sign(&digest, &mut rng_a).unwrap();
let signature_b = signer.sign(&digest, &mut rng_b).unwrap();
verifier
.verify(&digest, &signature_a)
.expect("Signature A must verify");
verifier
.verify(&digest, &signature_b)
.expect("Signature B must verify");
assert_ne!(
schnorr_signature_to_hex(&signature_a),
schnorr_signature_to_hex(&signature_b),
"Schnorr signatures of the same digest under distinct CSPRNG states must differ; \
a collision here means the per-signature nonce is not actually random and the \
secret key is at risk of algebraic recovery"
);
}
#[test]
fn two_sign_non_deterministic_invocations_produce_distinct_signatures() {
let signer = new_signer();
let verifier = signer.create_verifier();
let digest = [0x55u8; 32];
let signature_a = signer.sign_non_deterministic(&digest).unwrap();
let signature_b = signer.sign_non_deterministic(&digest).unwrap();
verifier
.verify(&digest, &signature_a)
.expect("OS-RNG signature A must verify");
verifier
.verify(&digest, &signature_b)
.expect("OS-RNG signature B must verify");
assert_ne!(
schnorr_signature_to_hex(&signature_a),
schnorr_signature_to_hex(&signature_b),
"sign_non_deterministic must draw a fresh per-signature nonce from OsRng on each call"
);
}
}