solana-ecvrf 0.0.1

ECVRF-EDWARDS25519-SHA512-TAI (RFC 9381) verification for Solana programs using curve25519 and sha512 syscalls
Documentation
//! Edwards25519 group operations on compressed 32-byte encodings.
//!
//! On Solana every operation is a `sol_curve_*` syscall (curve id 0). Off-chain
//! the same functions run on `curve25519-dalek`, which is also what the
//! validator runs underneath the syscall, so both paths accept and reject the
//! same encodings (including non-canonical `y >= p`, which dalek reduces).

pub type Point = [u8; 32];
pub type Scalar = [u8; 32];

/// RFC 8032 §5.1 basepoint, `y = 4/5`, compressed. Checked against dalek in tests.
pub const BASEPOINT: Point = [
    0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
    0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
];

/// The seven `y` encodings (sign bit cleared) that decode to a point of order
/// 1, 2, 4 or 8, RFC 9381 §5.4.5: 0, 1, y2, p−y2, p−1, and the non-canonical
/// p, p+1. Every other small-order encoding exceeds 2^255 and can't appear
/// once the sign bit is masked. Completeness is checked against dalek's
/// `EIGHT_TORSION` in the tests.
pub(crate) const SMALL_ORDER_Y: [Point; 7] = [
    [0; 32],
    [
        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00,
    ],
    [
        0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98,
        0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53,
        0xfc, 0x05,
    ],
    [
        0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67,
        0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac,
        0x03, 0x7a,
    ],
    [
        0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0x7f,
    ],
    [
        0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0x7f,
    ],
    [
        0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
        0xff, 0x7f,
    ],
];

/// `8·P == identity`, decided by table lookup instead of three point additions.
/// Only meaningful for an encoding that `validate` accepts.
#[inline]
pub fn is_small_order(p: &Point) -> bool {
    let mut y = *p;
    y[31] &= 0x7f;
    SMALL_ORDER_Y.contains(&y)
}

/// `8·P` as three doublings; the group-op syscall has no dedicated double but
/// its addition law is complete, so `add(P, P)` is a doubling at 473 CU
/// against 2177 for a scalar multiply.
#[inline]
pub fn mul_by_cofactor(p: &Point) -> Option<Point> {
    let p2 = add(p, p)?;
    let p4 = add(&p2, &p2)?;
    add(&p4, &p4)
}

#[cfg(target_os = "solana")]
mod backend {
    use super::{Point, Scalar};
    use crate::syscalls::*;

    const CURVE25519_EDWARDS: u64 = 0;
    const ADD: u64 = 0;

    #[inline(always)]
    pub fn validate(p: &Point) -> bool {
        let mut result = 0u8;
        unsafe { sol_curve_validate_point(CURVE25519_EDWARDS, p.as_ptr(), &mut result) == 0 }
    }

    #[inline(always)]
    pub fn add(a: &Point, b: &Point) -> Option<Point> {
        let mut out = core::mem::MaybeUninit::<Point>::uninit();
        let rc = unsafe {
            sol_curve_group_op(
                CURVE25519_EDWARDS,
                ADD,
                a.as_ptr(),
                b.as_ptr(),
                out.as_mut_ptr() as *mut u8,
            )
        };
        (rc == 0).then(|| unsafe { out.assume_init() })
    }

    /// `a·P + b·Q`. Scalars must be canonical (`< L`); the syscall rejects
    /// anything else, exactly like `Scalar::from_canonical_bytes`.
    #[inline(always)]
    pub fn double_scalar_mul(a: &Scalar, p: &Point, b: &Scalar, q: &Point) -> Option<Point> {
        let scalars = [*a, *b];
        let points = [*p, *q];
        let mut out = core::mem::MaybeUninit::<Point>::uninit();
        let rc = unsafe {
            sol_curve_multiscalar_mul(
                CURVE25519_EDWARDS,
                scalars.as_ptr() as *const u8,
                points.as_ptr() as *const u8,
                2,
                out.as_mut_ptr() as *mut u8,
            )
        };
        (rc == 0).then(|| unsafe { out.assume_init() })
    }
}

#[cfg(not(target_os = "solana"))]
mod backend {
    use super::{Point, Scalar};
    use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};

    #[inline]
    fn decompress(p: &Point) -> Option<EdwardsPoint> {
        CompressedEdwardsY(*p).decompress()
    }

    #[inline]
    fn scalar(s: &Scalar) -> Option<curve25519_dalek::Scalar> {
        curve25519_dalek::Scalar::from_canonical_bytes(*s).into()
    }

    pub fn validate(p: &Point) -> bool {
        decompress(p).is_some()
    }

    pub fn add(a: &Point, b: &Point) -> Option<Point> {
        Some((decompress(a)? + decompress(b)?).compress().0)
    }

    pub fn double_scalar_mul(a: &Scalar, p: &Point, b: &Scalar, q: &Point) -> Option<Point> {
        Some(
            (scalar(a)? * decompress(p)? + scalar(b)? * decompress(q)?)
                .compress()
                .0,
        )
    }
}

pub use backend::*;