newton-core 0.4.16

newton protocol core sdk
//! DLEQ (Discrete Log Equality) proofs for threshold decryption share validity.
//!
//! A DLEQ proof demonstrates that `log_G(A) == log_H(B)` — the same scalar `s`
//! satisfies `A = s * G` and `B = s * H`. In the threshold decryption context:
//!
//! - `G` = Ed25519 basepoint
//! - `A` = operator's public share `pk_i = s_i * G`
//! - `H` = `enc` point (HPKE ephemeral key, lifted to Edwards)
//! - `B` = partial decryption `D_i = s_i * H`
//!
//! Uses Fiat-Shamir with SHA-256 for non-interactive proofs.

use curve25519_dalek::{
    edwards::{CompressedEdwardsY, EdwardsPoint},
    scalar::Scalar,
};
use rand_core::OsRng;
use sha2::{Digest, Sha256};

use super::types::DleqProof;

/// Generate a DLEQ proof that `log_G(public_key) == log_H(partial)`.
///
/// The prover knows `secret` such that `public_key = secret * base_g` and
/// `partial = secret * base_h`.
///
/// # Arguments
///
/// * `secret` - The discrete log (operator's secret share `s_i`)
/// * `base_g` - First base point (Ed25519 basepoint)
/// * `public_key` - `secret * base_g` (operator's public share)
/// * `base_h` - Second base point (enc lifted to Edwards)
/// * `partial` - `secret * base_h` (partial decryption)
pub fn prove(
    secret: &Scalar,
    base_g: &EdwardsPoint,
    public_key: &EdwardsPoint,
    base_h: &EdwardsPoint,
    partial: &EdwardsPoint,
) -> DleqProof {
    let mut rng = OsRng;
    let k = Scalar::random(&mut rng);

    // Commitments
    let r1 = k * base_g; // R1 = k * G
    let r2 = k * base_h; // R2 = k * H

    // Fiat-Shamir challenge
    let challenge = compute_challenge(base_g, public_key, base_h, partial, &r1, &r2);

    // Response: z = k - c * secret
    let response = k - challenge * secret;

    DleqProof { challenge, response }
}

/// Verify a DLEQ proof that `log_G(public_key) == log_H(partial)`.
///
/// # Arguments
///
/// * `proof` - The DLEQ proof to verify
/// * `base_g` - First base point (Ed25519 basepoint)
/// * `public_key` - Claimed `secret * base_g`
/// * `base_h` - Second base point (enc lifted to Edwards)
/// * `partial` - Claimed `secret * base_h`
pub fn verify(
    proof: &DleqProof,
    base_g: &EdwardsPoint,
    public_key: &EdwardsPoint,
    base_h: &EdwardsPoint,
    partial: &EdwardsPoint,
) -> bool {
    // Recompute commitments:
    // R1' = z * G + c * public_key
    // R2' = z * H + c * partial
    let r1 = proof.response * base_g + proof.challenge * public_key;
    let r2 = proof.response * base_h + proof.challenge * partial;

    // Recompute challenge and check equality
    let expected_challenge = compute_challenge(base_g, public_key, base_h, partial, &r1, &r2);

    proof.challenge == expected_challenge
}

/// Compute the Fiat-Shamir challenge hash.
///
/// `c = SHA-256(G || A || H || B || R1 || R2)` reduced mod group order.
fn compute_challenge(
    base_g: &EdwardsPoint,
    public_key: &EdwardsPoint,
    base_h: &EdwardsPoint,
    partial: &EdwardsPoint,
    r1: &EdwardsPoint,
    r2: &EdwardsPoint,
) -> Scalar {
    let mut hasher = Sha256::new();
    hasher.update(b"newton-dleq-v1");
    hasher.update(compress(base_g));
    hasher.update(compress(public_key));
    hasher.update(compress(base_h));
    hasher.update(compress(partial));
    hasher.update(compress(r1));
    hasher.update(compress(r2));

    let hash: [u8; 32] = hasher.finalize().into();
    Scalar::from_bytes_mod_order(hash)
}

fn compress(point: &EdwardsPoint) -> [u8; 32] {
    let compressed: CompressedEdwardsY = point.compress();
    compressed.to_bytes()
}

#[cfg(test)]
mod tests {
    use super::*;
    use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;

    #[test]
    fn valid_proof_verifies() {
        let mut rng = OsRng;
        let secret = Scalar::random(&mut rng);
        let base_g = ED25519_BASEPOINT_POINT;
        let base_h = Scalar::random(&mut rng) * ED25519_BASEPOINT_POINT;

        let public_key = secret * base_g;
        let partial = secret * base_h;

        let proof = prove(&secret, &base_g, &public_key, &base_h, &partial);
        assert!(verify(&proof, &base_g, &public_key, &base_h, &partial));
    }

    #[test]
    fn wrong_secret_fails() {
        let mut rng = OsRng;
        let secret = Scalar::random(&mut rng);
        let wrong_secret = Scalar::random(&mut rng);
        let base_g = ED25519_BASEPOINT_POINT;
        let base_h = Scalar::random(&mut rng) * ED25519_BASEPOINT_POINT;

        let public_key = secret * base_g;
        // Partial uses wrong secret
        let partial = wrong_secret * base_h;

        let proof = prove(&secret, &base_g, &public_key, &base_h, &partial);
        // Proof was generated with `secret` but partial uses `wrong_secret`,
        // so the relationship doesn't hold. Proof generation itself doesn't
        // check this — verification catches it.
        assert!(!verify(&proof, &base_g, &public_key, &base_h, &partial));
    }

    #[test]
    fn wrong_partial_fails() {
        let mut rng = OsRng;
        let secret = Scalar::random(&mut rng);
        let base_g = ED25519_BASEPOINT_POINT;
        let base_h = Scalar::random(&mut rng) * ED25519_BASEPOINT_POINT;

        let public_key = secret * base_g;
        let partial = secret * base_h;
        let wrong_partial = Scalar::random(&mut rng) * base_h;

        let proof = prove(&secret, &base_g, &public_key, &base_h, &partial);
        // Verify against wrong partial
        assert!(!verify(&proof, &base_g, &public_key, &base_h, &wrong_partial));
    }

    #[test]
    fn wrong_public_key_fails() {
        let mut rng = OsRng;
        let secret = Scalar::random(&mut rng);
        let base_g = ED25519_BASEPOINT_POINT;
        let base_h = Scalar::random(&mut rng) * ED25519_BASEPOINT_POINT;

        let public_key = secret * base_g;
        let wrong_pk = Scalar::random(&mut rng) * base_g;
        let partial = secret * base_h;

        let proof = prove(&secret, &base_g, &public_key, &base_h, &partial);
        assert!(!verify(&proof, &base_g, &wrong_pk, &base_h, &partial));
    }

    #[test]
    fn proof_is_non_interactive_deterministic_verification() {
        let mut rng = OsRng;
        let secret = Scalar::random(&mut rng);
        let base_g = ED25519_BASEPOINT_POINT;
        let base_h = Scalar::random(&mut rng) * ED25519_BASEPOINT_POINT;

        let public_key = secret * base_g;
        let partial = secret * base_h;

        let proof = prove(&secret, &base_g, &public_key, &base_h, &partial);
        // Verify multiple times — must be deterministic
        assert!(verify(&proof, &base_g, &public_key, &base_h, &partial));
        assert!(verify(&proof, &base_g, &public_key, &base_h, &partial));
    }

    #[test]
    fn different_proofs_for_same_statement() {
        let mut rng = OsRng;
        let secret = Scalar::random(&mut rng);
        let base_g = ED25519_BASEPOINT_POINT;
        let base_h = Scalar::random(&mut rng) * ED25519_BASEPOINT_POINT;

        let public_key = secret * base_g;
        let partial = secret * base_h;

        let proof1 = prove(&secret, &base_g, &public_key, &base_h, &partial);
        let proof2 = prove(&secret, &base_g, &public_key, &base_h, &partial);

        // Different randomness → different proofs
        assert_ne!(proof1.challenge, proof2.challenge);
        // Both verify
        assert!(verify(&proof1, &base_g, &public_key, &base_h, &partial));
        assert!(verify(&proof2, &base_g, &public_key, &base_h, &partial));
    }
}