Documentation
use curve25519_dalek::{
    edwards::{CompressedEdwardsY, EdwardsPoint},
    scalar::Scalar,
};
use ed25519_dalek::{hazmat::ExpandedSecretKey, SigningKey, VerifyingKey};
use sha2::{Digest, Sha512};
use std::{convert::TryInto, fmt};

const DOMAIN_BLOCK_SIZE: usize = 128;

#[derive(Clone, Copy, Debug)]
pub enum SignatureDomain {
    Transaction,
    BlockHeader,
    Block,
    Gossip,
}

impl SignatureDomain {
    fn tag(self) -> u64 {
        match self {
            SignatureDomain::Transaction => 1,
            SignatureDomain::BlockHeader => 2,
            SignatureDomain::Block => 3,
            SignatureDomain::Gossip => 4,
        }
    }
}

#[derive(Debug)]
pub enum TnSignatureError {
    InvalidSignature,
    InvalidPublicKey,
    InvalidScalar,
}

impl fmt::Display for TnSignatureError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            TnSignatureError::InvalidSignature => write!(f, "invalid signature"),
            TnSignatureError::InvalidPublicKey => write!(f, "invalid public key"),
            TnSignatureError::InvalidScalar => write!(f, "invalid scalar"),
        }
    }
}

impl std::error::Error for TnSignatureError {}

fn domain_block(domain: SignatureDomain) -> [u8; DOMAIN_BLOCK_SIZE] {
    let mut block = [0u8; DOMAIN_BLOCK_SIZE];
    block[..8].copy_from_slice(&domain.tag().to_be_bytes());
    block
}

pub fn sign(
    domain: SignatureDomain,
    msg: &[u8],
    public_key: &[u8; 32],
    private_key: &[u8; 32],
) -> Result<[u8; 64], TnSignatureError> {
    let signing_key = SigningKey::from_bytes(private_key);
    let expanded: ExpandedSecretKey = signing_key.as_bytes().into();
    let block = domain_block(domain);

    let mut h_r = Sha512::new();
    h_r.update(&block);
    h_r.update(&expanded.hash_prefix);
    h_r.update(msg);
    let r = Scalar::from_hash(h_r);
    let r_point = EdwardsPoint::mul_base(&r).compress();

    let mut h_k = Sha512::new();
    h_k.update(&block);
    h_k.update(r_point.as_bytes());
    h_k.update(public_key);
    h_k.update(msg);
    let k = Scalar::from_hash(h_k);

    let s = k * expanded.scalar + r;

    let mut sig = [0u8; 64];
    sig[..32].copy_from_slice(r_point.as_bytes());
    sig[32..].copy_from_slice(&s.to_bytes());
    Ok(sig)
}

pub fn verify(
    domain: SignatureDomain,
    msg: &[u8],
    sig: &[u8; 64],
    public_key: &[u8; 32],
) -> Result<(), TnSignatureError> {
    let r_bytes: [u8; 32] = sig[..32].try_into().map_err(|_| TnSignatureError::InvalidSignature)?;
    let s_bytes: [u8; 32] = sig[32..]
        .try_into()
        .map_err(|_| TnSignatureError::InvalidSignature)?;

    let r_point = CompressedEdwardsY(r_bytes)
        .decompress()
        .ok_or(TnSignatureError::InvalidSignature)?;
    if r_point.is_small_order() {
        return Err(TnSignatureError::InvalidSignature);
    }

    let s_scalar = Option::<Scalar>::from(Scalar::from_canonical_bytes(s_bytes))
        .ok_or(TnSignatureError::InvalidScalar)?;

    let verifying_key =
        VerifyingKey::from_bytes(public_key).map_err(|_| TnSignatureError::InvalidPublicKey)?;
    let a_point: EdwardsPoint = verifying_key.into();
    if a_point.is_small_order() {
        return Err(TnSignatureError::InvalidPublicKey);
    }

    let block = domain_block(domain);
    let mut h_k = Sha512::new();
    h_k.update(&block);
    h_k.update(&r_bytes);
    h_k.update(public_key);
    h_k.update(msg);
    let k = Scalar::from_hash(h_k);

    let minus_a = -a_point;
    let r_cmp =
        EdwardsPoint::vartime_double_scalar_mul_basepoint(&k, &minus_a, &s_scalar).compress();

    if r_cmp.as_bytes() == &r_bytes {
        return Ok(());
    }
    Err(TnSignatureError::InvalidSignature)
}

pub fn sign_transaction(
    msg: &[u8],
    public_key: &[u8; 32],
    private_key: &[u8; 32],
) -> Result<[u8; 64], TnSignatureError> {
    sign(SignatureDomain::Transaction, msg, public_key, private_key)
}

pub fn verify_transaction(
    msg: &[u8],
    sig: &[u8; 64],
    public_key: &[u8; 32],
) -> Result<(), TnSignatureError> {
    verify(SignatureDomain::Transaction, msg, sig, public_key)
}