newton-core 0.4.16

newton protocol core sdk
//! Trusted dealer Feldman VSS share generation (Phase 2A).
//!
//! Generates a t-of-n threshold key split using a trusted dealer. The dealer
//! picks a random polynomial `f(x) = a_0 + a_1*x + ... + a_{t-1}*x^{t-1}`,
//! evaluates it at points `1..=n` to produce secret shares, and publishes
//! commitments `C_j = a_j * G` for verification.
//!
//! Phase 2B replaces this with FROST DKG (no trusted dealer).

use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, edwards::EdwardsPoint, scalar::Scalar};
use rand_core::OsRng;

use super::types::{KeyShare, ThresholdConfig, ThresholdPublicKey, VssCommitment};
use crate::crypto::error::CryptoError;

/// Generates a t-of-n Feldman VSS sharing from a trusted dealer.
///
/// Returns the threshold public key, VSS commitment, and one [`KeyShare`] per
/// operator. The dealer holds the full polynomial in memory during generation
/// and should zeroize it after distributing shares.
///
/// # Arguments
///
/// * `config` - Threshold parameters (t, n)
///
/// # Errors
///
/// Returns an error if `threshold == 0`, `total == 0`, or `threshold > total`.
pub fn generate_shares(
    config: ThresholdConfig,
) -> Result<(ThresholdPublicKey, VssCommitment, Vec<KeyShare>), CryptoError> {
    validate_config(config)?;

    let mut rng = OsRng;

    // Generate random polynomial coefficients: a_0, a_1, ..., a_{t-1}
    let mut coefficients: Vec<Scalar> = (0..config.threshold).map(|_| Scalar::random(&mut rng)).collect();

    // Commitments: C_j = a_j * G
    let commitments: Vec<EdwardsPoint> = coefficients.iter().map(|a_j| a_j * ED25519_BASEPOINT_POINT).collect();

    // Master public key
    let mpk = commitments[0];
    let mpk_montgomery = mpk.to_montgomery();

    let threshold_pk = ThresholdPublicKey {
        edwards: mpk,
        hpke_public_key: mpk_montgomery.to_bytes(),
    };

    let vss_commitment = VssCommitment { commitments };

    // Generate shares: s_i = f(i) for i in 1..=n
    let shares: Vec<KeyShare> = (1..=config.total)
        .map(|i| {
            let x = Scalar::from(i);
            let secret_share = evaluate_polynomial(&coefficients, &x);
            let public_share = secret_share * ED25519_BASEPOINT_POINT;
            KeyShare {
                index: i,
                secret_share,
                public_share,
            }
        })
        .collect();

    // Zeroize polynomial coefficients — a_0 is the master secret key
    coefficients.fill(Scalar::ZERO);

    Ok((threshold_pk, vss_commitment, shares))
}

/// Generates a Feldman VSS sharing from a specific master secret.
///
/// Used when the master secret is derived from an existing key (e.g., ECDSA →
/// Ed25519 → X25519 key derivation chain from Phase 1). The first coefficient
/// `a_0` is set to the provided secret; remaining coefficients are random.
pub fn generate_shares_from_secret(
    config: ThresholdConfig,
    master_secret: Scalar,
) -> Result<(ThresholdPublicKey, VssCommitment, Vec<KeyShare>), CryptoError> {
    validate_config(config)?;

    let mut rng = OsRng;

    // a_0 = master_secret, a_1..a_{t-1} random
    let mut coefficients = Vec::with_capacity(config.threshold as usize);
    coefficients.push(master_secret);
    for _ in 1..config.threshold {
        coefficients.push(Scalar::random(&mut rng));
    }

    let commitments: Vec<EdwardsPoint> = coefficients.iter().map(|a_j| a_j * ED25519_BASEPOINT_POINT).collect();

    let mpk = commitments[0];
    let threshold_pk = ThresholdPublicKey {
        edwards: mpk,
        hpke_public_key: mpk.to_montgomery().to_bytes(),
    };

    let vss_commitment = VssCommitment { commitments };

    let shares: Vec<KeyShare> = (1..=config.total)
        .map(|i| {
            let x = Scalar::from(i);
            let secret_share = evaluate_polynomial(&coefficients, &x);
            let public_share = secret_share * ED25519_BASEPOINT_POINT;
            KeyShare {
                index: i,
                secret_share,
                public_share,
            }
        })
        .collect();

    // Zeroize polynomial coefficients — a_0 is the master secret key
    coefficients.fill(Scalar::ZERO);

    Ok((threshold_pk, vss_commitment, shares))
}

/// Verifies a key share against a VSS commitment.
///
/// Checks that `s_i * G == Σ_j C_j * i^j` (Feldman verification equation).
pub fn verify_share(share: &KeyShare, commitment: &VssCommitment) -> bool {
    let x = Scalar::from(share.index);
    let mut x_pow = Scalar::ONE; // x^0 = 1
    let mut expected = EdwardsPoint::default(); // identity

    for c_j in &commitment.commitments {
        expected += x_pow * c_j;
        x_pow *= x;
    }

    // Constant-time comparison
    share.public_share.compress() == expected.compress()
}

/// Evaluate polynomial `f(x) = Σ a_j * x^j` at the given point.
pub(crate) fn evaluate_polynomial(coefficients: &[Scalar], x: &Scalar) -> Scalar {
    // Horner's method for efficiency
    let mut result = Scalar::ZERO;
    for coeff in coefficients.iter().rev() {
        result = result * x + coeff;
    }
    result
}

fn validate_config(config: ThresholdConfig) -> Result<(), CryptoError> {
    if config.threshold == 0 {
        return Err(CryptoError::ThresholdDecrypt("threshold must be at least 1".into()));
    }
    if config.total == 0 {
        return Err(CryptoError::ThresholdDecrypt("total must be at least 1".into()));
    }
    if config.threshold > config.total {
        return Err(CryptoError::ThresholdDecrypt(format!(
            "threshold ({}) cannot exceed total ({})",
            config.threshold, config.total
        )));
    }
    Ok(())
}

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

    fn test_config() -> ThresholdConfig {
        ThresholdConfig { threshold: 3, total: 5 }
    }

    #[test]
    fn generate_shares_produces_correct_count() {
        let (_, commitment, shares) = generate_shares(test_config()).unwrap();
        assert_eq!(shares.len(), 5);
        assert_eq!(commitment.commitments.len(), 3);
    }

    #[test]
    fn all_shares_verify_against_commitment() {
        let (_, commitment, shares) = generate_shares(test_config()).unwrap();
        for share in &shares {
            assert!(
                verify_share(share, &commitment),
                "share {} failed verification",
                share.index
            );
        }
    }

    #[test]
    fn shares_have_sequential_indices() {
        let (_, _, shares) = generate_shares(test_config()).unwrap();
        for (i, share) in shares.iter().enumerate() {
            assert_eq!(share.index, (i + 1) as u32);
        }
    }

    #[test]
    fn master_public_key_matches_first_commitment() {
        let (tpk, commitment, _) = generate_shares(test_config()).unwrap();
        assert_eq!(tpk.edwards.compress(), commitment.master_public_key().compress());
    }

    #[test]
    fn hpke_public_key_is_montgomery_conversion() {
        let (tpk, _, _) = generate_shares(test_config()).unwrap();
        let expected = tpk.edwards.to_montgomery().to_bytes();
        assert_eq!(tpk.hpke_public_key, expected);
    }

    #[test]
    fn tampered_share_fails_verification() {
        let (_, commitment, mut shares) = generate_shares(test_config()).unwrap();
        // Tamper with the secret share
        shares[0].secret_share += Scalar::ONE;
        shares[0].public_share = shares[0].secret_share * ED25519_BASEPOINT_POINT;
        assert!(!verify_share(&shares[0], &commitment));
    }

    #[test]
    fn wrong_commitment_fails_verification() {
        let (_, _, shares) = generate_shares(test_config()).unwrap();
        // Generate a different set of commitments
        let (_, wrong_commitment, _) = generate_shares(test_config()).unwrap();
        assert!(!verify_share(&shares[0], &wrong_commitment));
    }

    #[test]
    fn generate_from_secret_uses_provided_key() {
        let secret = Scalar::from(42u64);
        let expected_mpk = secret * ED25519_BASEPOINT_POINT;
        let config = test_config();

        let (tpk, commitment, shares) = generate_shares_from_secret(config, secret).unwrap();

        assert_eq!(tpk.edwards.compress(), expected_mpk.compress());
        assert_eq!(commitment.master_public_key().compress(), expected_mpk.compress());

        for share in &shares {
            assert!(verify_share(share, &commitment));
        }
    }

    #[test]
    fn invalid_config_threshold_zero() {
        let config = ThresholdConfig { threshold: 0, total: 5 };
        assert!(generate_shares(config).is_err());
    }

    #[test]
    fn invalid_config_threshold_exceeds_total() {
        let config = ThresholdConfig { threshold: 6, total: 5 };
        assert!(generate_shares(config).is_err());
    }

    #[test]
    fn trivial_1_of_1_scheme() {
        let config = ThresholdConfig { threshold: 1, total: 1 };
        let (_, commitment, shares) = generate_shares(config).unwrap();
        assert_eq!(shares.len(), 1);
        assert!(verify_share(&shares[0], &commitment));
    }

    #[test]
    fn polynomial_evaluation_horner() {
        // f(x) = 3 + 2x + x^2, f(2) = 3 + 4 + 4 = 11
        let coeffs = vec![Scalar::from(3u64), Scalar::from(2u64), Scalar::from(1u64)];
        let result = evaluate_polynomial(&coeffs, &Scalar::from(2u64));
        assert_eq!(result, Scalar::from(11u64));
    }
}