Skip to main content

vrf_contract_verifier/
types.rs

1use crate::constants::{
2    CHALLENGE_LENGTH,
3    VRF_HASH_TO_CURVE_DOMAIN,
4    EXPAND_MESSAGE_OUTPUT_LENGTH,
5    SUITE_STRING,
6    CHALLENGE_GENERATION_DOMAIN_SEPARATOR_FRONT,
7    CHALLENGE_GENERATION_DOMAIN_SEPARATOR_BACK,
8    PROOF_TO_HASH_DOMAIN_SEPARATOR_FRONT,
9    PROOF_TO_HASH_DOMAIN_SEPARATOR_BACK
10};
11use sha2::{Digest, Sha512};
12
13
14pub use curve25519_dalek_ng::ristretto::{CompressedRistretto, RistrettoPoint};
15pub use curve25519_dalek_ng::scalar::Scalar;
16
17// Import the same expand_message_xmd as vrf-wasm
18use elliptic_curve::hash2curve::{ExpandMsg, ExpandMsgXmd, Expander};
19
20
21/// VRF proof verification result
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum VerificationError {
24    InvalidProof,
25    InvalidInput,
26    InvalidPublicKey,
27    InvalidProofLength,
28    DecompressionFailed,
29    InvalidScalar,
30    InvalidGamma,
31    ZeroPublicKey,
32    ExpandMessageXmdFailed,
33}
34
35/// VRF public key (32 bytes - compressed point)
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct VrfPublicKey(pub [u8; 32]);
38
39impl VrfPublicKey {
40    /// Validate public key according to RFC 9381
41    /// Check for zero point and ensure valid curve point
42    pub fn validate(&self) -> Result<RistrettoPoint, VerificationError> {
43        // Check for zero public key
44        if self.0.iter().all(|&b| b == 0) {
45            return Err(VerificationError::ZeroPublicKey);
46        }
47
48        // Decompress and validate curve point
49        let pk_point = CompressedRistretto(self.0)
50            .decompress()
51            .ok_or(VerificationError::InvalidPublicKey)?;
52
53        // Additional check: ensure not identity point (zero point on curve)
54        if pk_point == RistrettoPoint::default() {
55            return Err(VerificationError::ZeroPublicKey);
56        }
57
58        Ok(pk_point)
59    }
60}
61
62/// RFC 9381 compliant VRF proof (80 bytes total: 32 + 16 + 32)
63#[cfg_attr(feature = "near", near_sdk::near(serializers = [borsh, json]))]
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct VrfProof {
66    pub gamma: [u8; 32],            // 32 bytes (VRF output point)
67    pub c: [u8; CHALLENGE_LENGTH],  // 16 bytes (RFC 9381 challenge)
68    pub s: [u8; 32],                // 32 bytes (scalar)
69}
70
71/// VRF output hash (64 bytes)
72pub type VrfOutput = [u8; 64];
73
74impl VrfProof {
75    /// Verify a VRF proof against a public key and input
76    pub fn verify(
77        &self,
78        input: &[u8],
79        public_key: &VrfPublicKey,
80    ) -> Result<VrfOutput, VerificationError> {
81        let output = self.to_output()?;
82        self.verify_output(input, public_key, &output)?;
83        Ok(output)
84    }
85
86    /// Verify a VRF proof against expected output
87    pub fn verify_output(
88        &self,
89        input: &[u8],
90        public_key: &VrfPublicKey,
91        expected_output: &VrfOutput,
92    ) -> Result<(), VerificationError> {
93        // Validate and decompress public key
94        let pk_point = public_key.validate()?;
95
96        // Decompress gamma
97        let gamma = CompressedRistretto(self.gamma)
98            .decompress()
99            .ok_or(VerificationError::DecompressionFailed)?;
100
101        // Decode scalar s - use canonical bytes check
102        let s = Scalar::from_canonical_bytes(self.s)
103            .ok_or(VerificationError::InvalidScalar)?;
104
105        // RFC 9381 compliant hash to curve
106        let h = hash_to_curve(&pk_point, input)?;
107
108        // Convert challenge from 16-byte challenge to scalar
109        let challenge = challenge_from_hash(&self.c);
110
111        // Verification equations:
112        // u = s*G - c*PK
113        // v = s*H - c*Gamma
114        use curve25519_dalek_ng::constants::RISTRETTO_BASEPOINT_POINT;
115        let u = &s * &RISTRETTO_BASEPOINT_POINT - &challenge * &pk_point;
116        let v = &s * &h - &challenge * &gamma;
117
118        // Recompute challenge
119        let c_prime = generate_challenge(&pk_point, &h, &gamma, &u, &v)?;
120
121        // Compare challenge values (16 bytes)
122        if c_prime != self.c {
123            return Err(VerificationError::InvalidProof);
124        }
125
126        // Verify output matches
127        let computed_output = proof_to_hash(&gamma)?;
128        if computed_output != *expected_output {
129            return Err(VerificationError::InvalidProof);
130        }
131
132        Ok(())
133    }
134
135    /// Convert proof to VRF output - now returns Result to avoid panics
136    pub fn to_output(&self) -> Result<VrfOutput, VerificationError> {
137        let gamma = CompressedRistretto(self.gamma)
138            .decompress()
139            .ok_or(VerificationError::InvalidGamma)?;
140        proof_to_hash(&gamma)
141    }
142}
143
144// RFC 9381 compliant functions for verification only
145
146/// RFC 9381 compliant hash-to-curve implementation
147/// Uses the same expand_message_xmd as vrf-wasm for perfect compatibility
148pub fn hash_to_curve(pk: &RistrettoPoint, input: &[u8]) -> Result<RistrettoPoint, VerificationError> {
149    let pk_compressed = pk.compress().0;
150
151    // Use the same expand_message_xmd as vrf-wasm
152    let mut expanded_message = ExpandMsgXmd::<Sha512>::expand_message(
153        &[&pk_compressed, input],
154        &[VRF_HASH_TO_CURVE_DOMAIN],
155        EXPAND_MESSAGE_OUTPUT_LENGTH,
156    )
157    .map_err(|_| VerificationError::ExpandMessageXmdFailed)?;
158
159    let mut uniform_bytes = [0u8; EXPAND_MESSAGE_OUTPUT_LENGTH];
160    expanded_message.fill_bytes(&mut uniform_bytes);
161
162    Ok(RistrettoPoint::from_uniform_bytes(&uniform_bytes))
163}
164
165/// RFC 9381 compliant challenge generation from 16-byte hash
166pub fn challenge_from_hash(hash: &[u8; CHALLENGE_LENGTH]) -> Scalar {
167    // Pad 16-byte challenge to 32 bytes for scalar conversion
168    let mut scalar_bytes = [0u8; 32];
169    scalar_bytes[..CHALLENGE_LENGTH].copy_from_slice(hash);
170    Scalar::from_bytes_mod_order(scalar_bytes)
171}
172
173/// RFC 9381 compliant challenge generation (returns 16 bytes)
174/// Matches the implementation in the main vrf-wasm library
175fn generate_challenge(
176    pk: &RistrettoPoint,
177    h: &RistrettoPoint,
178    gamma: &RistrettoPoint,
179    u: &RistrettoPoint,
180    v: &RistrettoPoint,
181) -> Result<[u8; CHALLENGE_LENGTH], VerificationError> {
182    let mut hasher = Sha512::new();
183
184    let pk_bytes = pk.compress().0;
185    let h_bytes = h.compress().0;
186    let gamma_bytes = gamma.compress().0;
187    let u_bytes = u.compress().0;
188    let v_bytes = v.compress().0;
189
190    hasher.update(SUITE_STRING);
191    hasher.update([CHALLENGE_GENERATION_DOMAIN_SEPARATOR_FRONT]);
192    hasher.update(pk_bytes);
193    hasher.update(h_bytes);
194    hasher.update(gamma_bytes);
195    hasher.update(u_bytes);
196    hasher.update(v_bytes);
197    hasher.update([CHALLENGE_GENERATION_DOMAIN_SEPARATOR_BACK]);
198    let hash = hasher.finalize();
199
200    // RFC 9381: challenge is first 16 bytes of hash
201    let mut challenge = [0u8; CHALLENGE_LENGTH];
202    challenge.copy_from_slice(&hash[..CHALLENGE_LENGTH]);
203
204    Ok(challenge)
205}
206
207/// RFC 9381 compliant VRF output generation
208/// Matches the implementation in the main vrf-wasm library
209pub fn proof_to_hash(gamma: &RistrettoPoint) -> Result<VrfOutput, VerificationError> {
210    let mut hasher = Sha512::new();
211    hasher.update(SUITE_STRING);
212    hasher.update([PROOF_TO_HASH_DOMAIN_SEPARATOR_FRONT]);
213    hasher.update(&gamma.compress().0);
214    hasher.update([PROOF_TO_HASH_DOMAIN_SEPARATOR_BACK]);
215    Ok(hasher.finalize().into())
216}