Skip to main content

ark_vrf/
thin.rs

1//! # Thin VRF
2//!
3//! Same structure as Tiny VRF but produces an `(R, s)` proof storing the nonce
4//! commitment rather than the challenge. This enables batch verification at the
5//! cost of a slightly larger proof.
6//!
7//! ## Usage
8//!
9//! ```rust,ignore
10//! use ark_vrf::suites::bandersnatch::*;
11//! use ark_vrf::thin::{Prover, Verifier};
12//!
13//! let secret = Secret::from_seed([0; 32]);
14//! let public = secret.public();
15//! let input = Input::new(b"example input").unwrap();
16//! let io = secret.vrf_io(input);
17//!
18//! // Proving
19//! let proof = secret.prove(io, b"aux data");
20//!
21//! // Verification
22//! let result = public.verify(io, b"aux data", &proof);
23//! ```
24
25use crate::{utils::challenge_scalar, utils::common::DomSep, utils::straus::short_msm, *};
26
27/// Marker trait for suites that support the Thin VRF scheme.
28///
29/// Blanket-implemented for all types implementing [`Suite`].
30pub trait ThinSuite: Suite {}
31
32impl<T> ThinSuite for T where T: Suite {}
33
34/// Thin VRF proof.
35///
36/// Schnorr-like proof over the delinearized merged DLEQ relation:
37/// - `r`: Nonce commitment R = k * I_m
38/// - `s`: Response scalar s = k + c * sk
39///
40/// Deserialization via [`CanonicalDeserialize`] includes subgroup checks for
41/// curve points, so deserialized proofs are guaranteed to contain valid points.
42#[derive(Debug, Clone, CanonicalSerialize, CanonicalDeserialize)]
43pub struct Proof<S: ThinSuite> {
44    /// Nonce commitment on the merged input.
45    pub r: AffinePoint<S>,
46    /// Response scalar.
47    pub s: ScalarField<S>,
48}
49
50#[inline(always)]
51fn vrf_transcript<S: ThinSuite>(
52    public: AffinePoint<S>,
53    ios: impl AsRef<[VrfIo<S>]>,
54    ad: impl AsRef<[u8]>,
55) -> (S::Transcript, VrfIo<S>) {
56    utils::vrf_transcript_with_schnorr(DomSep::ThinVrf, public, ios, ad)
57}
58
59#[inline(always)]
60fn vrf_transcript_scalars<S: ThinSuite>(
61    public: AffinePoint<S>,
62    ios: impl AsRef<[VrfIo<S>]>,
63    ad: impl AsRef<[u8]>,
64) -> (S::Transcript, Vec<ScalarField<S>>) {
65    utils::vrf_transcript_scalars_with_schnorr(DomSep::ThinVrf, public, ios, ad)
66}
67
68/// Trait for types that can generate Thin VRF proofs.
69pub trait Prover<S: ThinSuite> {
70    /// Generate a proof for the given VRF I/O pairs and additional data.
71    ///
72    /// Multiple I/O pairs are delinearized into a single merged pair before proving.
73    fn prove(&self, ios: impl AsRef<[VrfIo<S>]>, ad: impl AsRef<[u8]>) -> Proof<S>;
74}
75
76/// Trait for entities that can verify Thin VRF proofs.
77///
78/// All curve points involved in verification (public key, I/O pairs, and proof
79/// points) are assumed to be in the prime-order subgroup. This is guaranteed
80/// when points are constructed through checked constructors ([`Public::from_affine`],
81/// [`Input::from_affine`], [`Output::from_affine`]) or through trusted
82/// operations like [`Input::new`] (hash-to-curve) and [`Secret::vrf_io`].
83/// Proof points are guaranteed valid when deserialized via [`CanonicalDeserialize`]
84/// (which includes subgroup checks) or produced by [`Prover::prove`].
85///
86/// Using unchecked constructors (e.g. [`Input::from_affine_unchecked`]) places
87/// the burden of subgroup validation on the caller. Passing points with
88/// cofactor components leads to undefined verification behavior.
89///
90/// The group identity is checked unconditionally, for the public key and for
91/// every I/O pair. Neither binds the proof to a signer: the secret scalar of
92/// the identity key is publicly known, and a pair holding the identity is
93/// satisfied by every secret key. It stays a legal value for the nonce
94/// commitment `R`, which commits to nothing.
95pub trait Verifier<S: ThinSuite> {
96    /// Verify a proof for the given VRF I/O pairs and additional data.
97    ///
98    /// Multiple I/O pairs are delinearized into a single merged pair before verifying.
99    ///
100    /// Returns `Ok(())` if verification succeeds, `Err(Error::InvalidData)` if the
101    /// public key or any I/O pair point is the group identity,
102    /// `Err(Error::VerificationFailure)` otherwise.
103    fn verify(
104        &self,
105        ios: impl AsRef<[VrfIo<S>]>,
106        ad: impl AsRef<[u8]>,
107        proof: &Proof<S>,
108    ) -> Result<(), Error>;
109}
110
111impl<S: ThinSuite> Prover<S> for Secret<S> {
112    fn prove(&self, ios: impl AsRef<[VrfIo<S>]>, ad: impl AsRef<[u8]>) -> Proof<S> {
113        let (t, merged) = vrf_transcript::<S>(self.public.0, ios, ad);
114
115        // Nonce
116        let k = S::nonce(&self.scalar, Some(t.clone()));
117
118        // R = k * I_m (secret nonce on merged input)
119        let r = smul!(merged.input.0, k).into_affine();
120
121        // Challenge
122        let c = S::challenge(&[&r], Some(t));
123
124        // Response
125        let s = k + c * self.scalar;
126
127        Proof { r, s }
128    }
129}
130
131impl<S: ThinSuite> Verifier<S> for Public<S> {
132    fn verify(
133        &self,
134        ios: impl AsRef<[VrfIo<S>]>,
135        ad: impl AsRef<[u8]>,
136        proof: &Proof<S>,
137    ) -> Result<(), Error> {
138        // With Y = 0 the challenge term drops out of the equation below and
139        // anyone can pick s and set R = s * G.
140        if self.is_identity() {
141            return Err(Error::InvalidData);
142        }
143
144        // A pair holding the identity satisfies O = x*I for every x, so it
145        // binds its VRF output to no signer.
146        let ios = ios.as_ref();
147        if ios.iter().any(VrfIo::has_identity) {
148            return Err(Error::InvalidData);
149        }
150
151        let Proof { r, s } = proof;
152        let (t, merged) = vrf_transcript::<S>(self.0, ios, ad);
153
154        // Challenge
155        let c = S::challenge(&[r], Some(t));
156
157        // Verification: s * I_m - c * O_m == R
158        let lhs = short_msm(&[merged.input.0, merged.output.0], &[*s, -c], 2);
159        if lhs != r.into_group() {
160            return Err(Error::VerificationFailure);
161        }
162
163        Ok(())
164    }
165}
166
167/// Deferred Thin VRF verification data for batch verification.
168///
169/// Stores raw points and delinearization scalars instead of the merged pair,
170/// so that `prepare` requires no EC ops (just hashing). The expanded
171/// verification equation uses these directly in the batch MSM.
172pub struct BatchItem<S: ThinSuite> {
173    c: ScalarField<S>,
174    pk: Public<S>,
175    ios: Vec<VrfIo<S>>,
176    zs: Vec<ScalarField<S>>,
177    r: AffinePoint<S>,
178    s: ScalarField<S>,
179}
180
181/// Batch verifier for Thin VRF proofs.
182///
183/// Collects multiple proofs and verifies them together via a single
184/// multi-scalar multiplication.
185///
186/// The same subgroup membership assumptions as [`Verifier`] apply to all
187/// points fed into the batch (public keys, I/O pairs, and proof points).
188pub struct BatchVerifier<S: ThinSuite> {
189    items: Vec<BatchItem<S>>,
190}
191
192impl<S: ThinSuite> Default for BatchVerifier<S> {
193    fn default() -> Self {
194        Self { items: Vec::new() }
195    }
196}
197
198impl<S: ThinSuite> BatchVerifier<S> {
199    /// Create a new empty batch verifier.
200    pub fn new() -> Self {
201        Self::default()
202    }
203
204    /// Prepare a proof for batch verification.
205    ///
206    /// Computes delinearization scalars and challenge via hashing only (no EC
207    /// ops). Stores the raw points and z scalars for the expanded verification
208    /// equation in [`Self::verify`].
209    pub fn prepare(
210        public: &Public<S>,
211        ios: impl AsRef<[VrfIo<S>]>,
212        ad: impl AsRef<[u8]>,
213        proof: &Proof<S>,
214    ) -> BatchItem<S> {
215        let ios = ios.as_ref();
216        let (t, zs) = vrf_transcript_scalars::<S>(public.0, ios, ad);
217        let c = S::challenge(&[&proof.r], Some(t));
218        BatchItem {
219            c,
220            pk: *public,
221            ios: ios.to_vec(),
222            zs,
223            r: proof.r,
224            s: proof.s,
225        }
226    }
227
228    /// Push a previously prepared entry into the batch.
229    pub fn push_prepared(&mut self, entry: BatchItem<S>) {
230        self.items.push(entry);
231    }
232
233    /// Prepare and push a proof in one step.
234    pub fn push(
235        &mut self,
236        public: &Public<S>,
237        ios: impl AsRef<[VrfIo<S>]>,
238        ad: impl AsRef<[u8]>,
239        proof: &Proof<S>,
240    ) {
241        let entry = Self::prepare(public, ios, ad, proof);
242        self.push_prepared(entry);
243    }
244
245    /// Batch-verify all collected proofs using a single multi-scalar multiplication.
246    ///
247    /// For each proof j, the expanded verification equation is:
248    ///   R_j + c_j*z0_j*pk_j + sum_i(c_j*z_ij*O_ij) - s_j*z0_j*G - sum_i(s_j*z_ij*I_ij) == 0
249    ///
250    /// With random weights w_j, G is accumulated as a shared base, yielding a
251    /// `(sum_j(2 + 2*M_j) + 1)`-point MSM (where M_j is the number of VRF
252    /// pairs in proof j).
253    ///
254    /// Returns `Ok(())` if all proofs verify, `Err(Error::InvalidData)` if any
255    /// public key or I/O pair point is the group identity,
256    /// `Err(VerificationFailure)` otherwise.
257    pub fn verify(&self) -> Result<(), Error> {
258        use ark_ec::VariableBaseMSM;
259        use ark_ff::Zero;
260
261        let items = &self.items;
262        if items.is_empty() {
263            return Ok(());
264        }
265        // Checked here rather than in `prepare`, which cannot fail.
266        if items
267            .iter()
268            .any(|item| item.pk.is_identity() || item.ios.iter().any(VrfIo::has_identity))
269        {
270            return Err(Error::InvalidData);
271        }
272
273        // Deterministic random scalars derived from all (c, s) pairs.
274        let mut t = S::Transcript::new(S::SUITE_ID);
275        t.absorb_raw(&[DomSep::BatchVerify as u8]);
276        for e in items {
277            t.absorb_serialize(&e.c);
278            t.absorb_serialize(&e.s);
279        }
280
281        // Build MSM with expanded equation: per-proof (2+2M) points + 1 shared G.
282        let total_points: usize = items.iter().map(|e| 2 + 2 * e.ios.len()).sum::<usize>() + 1;
283        let mut bases = Vec::with_capacity(total_points);
284        let mut scalars = Vec::with_capacity(total_points);
285        let mut g_scalar = ScalarField::<S>::zero();
286
287        for item in items.iter() {
288            // 128-bit random weights for Schwartz-Zippel soundness.
289            let w = challenge_scalar::<S>(&mut t);
290
291            let wc = w * item.c;
292            let ws = w * item.s;
293
294            // R_j with scalar w_j
295            bases.push(item.r);
296            scalars.push(w);
297
298            // pk_j with scalar w_j*c_j*z0_j
299            bases.push(item.pk.0);
300            scalars.push(wc * item.zs[0]);
301
302            // Accumulate G scalar: -w_j*s_j*z0_j
303            g_scalar -= ws * item.zs[0];
304
305            // Per VRF pair: O_i with w*c*z_i, I_i with -w*s*z_i
306            for (i, io) in item.ios.iter().enumerate() {
307                bases.push(io.output.0);
308                scalars.push(wc * item.zs[i + 1]);
309
310                bases.push(io.input.0);
311                scalars.push(-(ws * item.zs[i + 1]));
312            }
313        }
314
315        // Shared generator base.
316        bases.push(S::generator());
317        scalars.push(g_scalar);
318
319        let result = <S::Affine as AffineRepr>::Group::msm_unchecked(&bases, &scalars);
320        if !result.is_zero() {
321            return Err(Error::VerificationFailure);
322        }
323
324        Ok(())
325    }
326}
327
328#[cfg(test)]
329pub(crate) mod testing {
330    use super::*;
331    use crate::testing::{self as common, SuiteExt, TEST_SEED, random_val};
332
333    pub fn prove_verify<S: ThinSuite>() {
334        use thin::{Prover, Verifier};
335
336        let secret = Secret::<S>::from_seed(TEST_SEED);
337        let public = secret.public();
338        let input = Input::from_affine_unchecked(random_val(None));
339        let io = secret.vrf_io(input);
340
341        let proof = secret.prove(io, b"foo");
342        let result = public.verify(io, b"foo", &proof);
343        assert!(result.is_ok());
344    }
345
346    pub fn batch_verify<S: ThinSuite>() {
347        use thin::{BatchVerifier, Prover, Verifier};
348
349        let secret = Secret::<S>::from_seed(TEST_SEED);
350        let public = secret.public();
351        let input = Input::from_affine_unchecked(random_val(None));
352        let io = secret.vrf_io(input);
353
354        let proof1 = secret.prove(io, b"foo");
355        let proof2 = secret.prove(io, b"bar");
356
357        // Single-proof verification still works.
358        assert!(public.verify(io, b"foo", &proof1).is_ok());
359        assert!(public.verify(io, b"bar", &proof2).is_ok());
360
361        // Batch using push.
362        let mut batch = BatchVerifier::new();
363        batch.push(&public, io, b"foo", &proof1);
364        batch.push(&public, io, b"bar", &proof2);
365        assert!(batch.verify().is_ok());
366
367        // Batch using prepare + push_prepared.
368        let mut batch = BatchVerifier::new();
369        let entry1 = BatchVerifier::prepare(&public, io, b"foo", &proof1);
370        let entry2 = BatchVerifier::prepare(&public, io, b"bar", &proof2);
371        batch.push_prepared(entry1);
372        batch.push_prepared(entry2);
373        assert!(batch.verify().is_ok());
374
375        // Empty batch is ok.
376        let batch = BatchVerifier::<S>::new();
377        assert!(batch.verify().is_ok());
378
379        // Bad additional data should fail.
380        let mut batch = BatchVerifier::new();
381        batch.push(&public, io, b"foo", &proof1);
382        batch.push(&public, io, b"wrong", &proof2);
383        assert!(batch.verify().is_err());
384    }
385
386    /// N=1 slice produces same proof as passing a single `VrfIo`.
387    pub fn prove_verify_multi_single<S: ThinSuite>() {
388        use thin::{Prover, Verifier};
389
390        let secret = Secret::<S>::from_seed(TEST_SEED);
391        let public = secret.public();
392        let input = Input::from_affine_unchecked(random_val(None));
393        let io = secret.vrf_io(input);
394
395        let proof_single = secret.prove(io, b"foo");
396        let proof_slice = secret.prove([io], b"foo");
397
398        // Byte-identical proofs
399        let encode = |p: &thin::Proof<S>| {
400            let mut buf = Vec::new();
401            p.serialize_compressed(&mut buf).unwrap();
402            buf
403        };
404        assert_eq!(encode(&proof_single), encode(&proof_slice));
405
406        // Cross-verification
407        assert!(public.verify(io, b"foo", &proof_slice).is_ok());
408        assert!(public.verify([io], b"foo", &proof_single).is_ok());
409    }
410
411    /// An identity public key must be rejected by both verifiers.
412    ///
413    /// With `Y = 0` the verification equation degenerates to `s * G == R`, which
414    /// anyone can satisfy without knowing any secret: no proving is involved
415    /// below, `s` is picked and `R` derived from it. Verification is handed a raw
416    /// `Public` to make sure the rejection does not depend on the key having gone
417    /// through a checked constructor.
418    pub fn identity_public_key_rejected<S: ThinSuite>() {
419        use thin::{BatchVerifier, Verifier};
420
421        let identity = Public::<S>(AffinePoint::<S>::zero());
422        let s = ScalarField::<S>::from(0x5eed_u64);
423        let forged = Proof::<S> {
424            r: (S::generator() * s).into_affine(),
425            s,
426        };
427
428        assert!(identity.verify([], b"forgery", &forged).is_err());
429
430        let mut batch = BatchVerifier::new();
431        batch.push(&identity, [], b"forgery", &forged);
432        assert!(batch.verify().is_err());
433    }
434
435    /// An I/O pair holding the identity must be rejected by both verifiers.
436    ///
437    /// `(I, O) = (0, 0)` satisfies `O = x * I` for every secret key, so the
438    /// verification equation accepts it and two different keys produce two
439    /// valid proofs for the same pair. The pair therefore binds its VRF output
440    /// to nobody, and only an explicit check keeps it out. The second case
441    /// hides the bad pair behind a good one, where the merged pair alone is not
442    /// enough to catch it.
443    pub fn identity_io_pair_rejected<S: ThinSuite>() {
444        use thin::{BatchVerifier, Prover, Verifier};
445
446        let identity_io = VrfIo::<S> {
447            input: Input(AffinePoint::<S>::zero()),
448            output: Output(AffinePoint::<S>::zero()),
449        };
450
451        for seed in [common::TEST_SEED, [0x11; 32]] {
452            let secret = Secret::<S>::from_seed(seed);
453            let public = secret.public();
454
455            let proof = secret.prove([identity_io], b"forgery");
456            assert!(public.verify([identity_io], b"forgery", &proof).is_err());
457
458            let mut batch = BatchVerifier::new();
459            batch.push(&public, [identity_io], b"forgery", &proof);
460            assert!(batch.verify().is_err());
461
462            let good_io = secret.vrf_io(Input::new(b"good").unwrap());
463            let ios = [good_io, identity_io];
464            let proof = secret.prove(ios, b"forgery");
465            assert!(public.verify(ios, b"forgery", &proof).is_err());
466
467            let mut batch = BatchVerifier::new();
468            batch.push(&public, ios, b"forgery", &proof);
469            assert!(batch.verify().is_err());
470        }
471    }
472
473    /// N=3 VRF pairs: verify succeeds; tampered output/input/ad fails.
474    pub fn prove_verify_multi<S: ThinSuite>() {
475        use thin::{Prover, Verifier};
476
477        let secret = Secret::<S>::from_seed(TEST_SEED);
478        let public = secret.public();
479
480        let ios: Vec<VrfIo<S>> = (0..3u8)
481            .map(|i| {
482                let input = Input::new(&[i + 1]).unwrap();
483                secret.vrf_io(input)
484            })
485            .collect();
486
487        let proof = secret.prove(&ios[..], b"bar");
488        assert!(public.verify(&ios[..], b"bar", &proof).is_ok());
489
490        // Tamper: wrong output on ios[1]
491        let mut bad_ios = ios.clone();
492        bad_ios[1].output = secret.output(ios[0].input);
493        assert!(public.verify(&bad_ios[..], b"bar", &proof).is_err());
494
495        // Tamper: wrong input on ios[0]
496        let mut bad_ios = ios.clone();
497        bad_ios[0].input = ios[1].input;
498        assert!(public.verify(&bad_ios[..], b"bar", &proof).is_err());
499
500        // Tamper: wrong ad
501        assert!(public.verify(&ios[..], b"baz", &proof).is_err());
502    }
503
504    /// N=0 VRF pairs degenerates to Schnorr signature over ad.
505    pub fn prove_verify_multi_empty<S: ThinSuite>() {
506        use thin::{Prover, Verifier};
507
508        let secret = Secret::<S>::from_seed(TEST_SEED);
509        let public = secret.public();
510
511        let proof = secret.prove([], b"bar");
512        assert!(public.verify([], b"bar", &proof).is_ok());
513
514        // Wrong ad should fail
515        assert!(public.verify([], b"baz", &proof).is_err());
516    }
517
518    #[macro_export]
519    macro_rules! thin_suite_tests {
520        ($suite:ty) => {
521            mod thin {
522                use super::*;
523
524                #[test]
525                fn prove_verify() {
526                    $crate::thin::testing::prove_verify::<$suite>();
527                }
528
529                #[test]
530                fn prove_verify_multi_single() {
531                    $crate::thin::testing::prove_verify_multi_single::<$suite>();
532                }
533
534                #[test]
535                fn prove_verify_multi() {
536                    $crate::thin::testing::prove_verify_multi::<$suite>();
537                }
538
539                #[test]
540                fn prove_verify_multi_empty() {
541                    $crate::thin::testing::prove_verify_multi_empty::<$suite>();
542                }
543
544                #[test]
545                fn batch_verify() {
546                    $crate::thin::testing::batch_verify::<$suite>();
547                }
548
549                #[test]
550                fn identity_public_key_rejected() {
551                    $crate::thin::testing::identity_public_key_rejected::<$suite>();
552                }
553
554                #[test]
555                fn identity_io_pair_rejected() {
556                    $crate::thin::testing::identity_io_pair_rejected::<$suite>();
557                }
558
559                $crate::test_vectors!($crate::thin::testing::TestVector<$suite>);
560            }
561        };
562    }
563
564    pub struct TestVector<S: ThinSuite> {
565        pub base: common::TestVector<S>,
566        pub proof_r: AffinePoint<S>,
567        pub proof_s: ScalarField<S>,
568    }
569
570    impl<S: ThinSuite> core::fmt::Debug for TestVector<S> {
571        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
572            let r = hex::encode(common::point_encode::<S>(&self.proof_r));
573            let s = hex::encode(common::scalar_encode::<S>(&self.proof_s));
574            f.debug_struct("TestVector")
575                .field("base", &self.base)
576                .field("proof_r", &r)
577                .field("proof_s", &s)
578                .finish()
579        }
580    }
581
582    impl<S> common::TestVectorTrait for TestVector<S>
583    where
584        S: ThinSuite + SuiteExt + std::fmt::Debug,
585    {
586        fn name() -> String {
587            S::SUITE_NAME.to_string() + "_thin"
588        }
589
590        fn new(comment: &str, seed: &[u8; 32], alpha: &[u8], ad: &[u8]) -> Self {
591            use super::Prover;
592            let base = common::TestVector::new(comment, seed, alpha, ad);
593            let io = VrfIo {
594                input: Input::<S>::from_affine_unchecked(base.h),
595                output: Output::from_affine_unchecked(base.gamma),
596            };
597            let secret = Secret::from_scalar(base.sk);
598            let proof: Proof<S> = secret.prove(io, ad);
599            Self {
600                base,
601                proof_r: proof.r,
602                proof_s: proof.s,
603            }
604        }
605
606        fn from_map(map: &common::TestVectorMap) -> Self {
607            let base = common::TestVector::from_map(map);
608            let proof_r = common::point_decode::<S>(&map.get_bytes("proof_r")).unwrap();
609            let proof_s = common::scalar_decode::<S>(&map.get_bytes("proof_s"));
610            Self {
611                base,
612                proof_r,
613                proof_s,
614            }
615        }
616
617        fn to_map(&self) -> common::TestVectorMap {
618            let items = [
619                (
620                    "proof_r",
621                    hex::encode(common::point_encode::<S>(&self.proof_r)),
622                ),
623                (
624                    "proof_s",
625                    hex::encode(common::scalar_encode::<S>(&self.proof_s)),
626                ),
627            ];
628            let mut map = self.base.to_map();
629            items.into_iter().for_each(|(name, value)| {
630                map.0.insert(name.to_string(), value);
631            });
632            map
633        }
634
635        fn run(&self) {
636            self.base.run();
637            let io = VrfIo {
638                input: Input::<S>::from_affine_unchecked(self.base.h),
639                output: Output::from_affine_unchecked(self.base.gamma),
640            };
641            let sk = Secret::from_scalar(self.base.sk);
642            let proof = sk.prove(io, &self.base.ad);
643            assert_eq!(self.proof_r, proof.r, "Thin VRF proof R mismatch");
644            assert_eq!(self.proof_s, proof.s, "Thin VRF proof s mismatch");
645
646            let pk = Public(self.base.pk);
647            assert!(pk.verify(io, &self.base.ad, &proof).is_ok());
648        }
649    }
650
651    /// Demonstrates that a malicious prover who knows the discrete-log relation
652    /// between the VRF input `I` and the generator `G` (i.e. knows `d` s.t.
653    /// `I = d * G`) can forge a valid Thin-VRF proof for an arbitrary output.
654    ///
655    /// This is why `Input` **must** be constructed via hash-to-curve.
656    #[test]
657    fn known_dlog_input_forgery() {
658        use ark_ff::Field;
659
660        type S = crate::suites::testing::TestSuite;
661        type Sc = ScalarField<S>;
662
663        let g = S::generator();
664
665        // Attacker's key pair.
666        let sk = Sc::from(42);
667        let pk = (g * sk).into_affine();
668
669        // Input with KNOWN discrete log: I = d * G.
670        let d = Sc::from(7);
671        let input_pt = (g * d).into_affine();
672        let input = Input::<S>::from_affine_unchecked(input_pt);
673
674        // Honest output would be O = sk * I.
675        let honest_output = (input_pt * sk).into_affine();
676
677        // Attacker picks a DIFFERENT output: O' = t * G, with t != sk * d.
678        let t_scalar = Sc::from(1234);
679        let fake_output_pt = (g * t_scalar).into_affine();
680        assert_ne!(fake_output_pt, honest_output);
681        let fake_output = Output::<S>::from_affine_unchecked(fake_output_pt);
682
683        let ad: &[u8] = b"attack";
684        let fake_io = VrfIo {
685            input,
686            output: fake_output,
687        };
688
689        // Replicate what prove/verify do.
690        let fake_ios: &[VrfIo<S>] = &[fake_io];
691        let (transcript, zs) = vrf_transcript_scalars::<S>(pk, fake_ios, ad);
692        let (z0, z1) = (zs[0], zs[1]);
693
694        // Compute merged input I_m = z0*G + z1*I for the forgery.
695        let merged_input = (g * z0 + input_pt * z1).into_affine();
696
697        // --- Forge the proof ---
698        //
699        // Because I = d*G, the merged input is I_m = (z0 + z1*d) * G and the
700        // merged output is O_m = (z0*sk + z1*t) * G, both multiples of G.
701        // The effective DLEQ secret is x = (z0*sk + z1*t) / (z0 + z1*d).
702        let x = (z0 * sk + z1 * t_scalar) * (z0 + z1 * d).inverse().unwrap();
703
704        // Standard Schnorr proof with the derived secret.
705        let k = Sc::from(9999);
706        let r = (merged_input * k).into_affine();
707        let c = S::challenge(&[&r], Some(transcript));
708        let s = k + c * x;
709
710        let forged_proof = Proof::<S> { r, s };
711
712        // The forged proof verifies despite O' != sk * I.
713        //
714        // The verifier checks: s * I_m == R + c * O_m
715        //
716        // Expanding with I = d*G (everything collapses to multiples of G):
717        //   I_m = z0*G + z1*I = (z0 + z1*d) * G
718        //   O_m = z0*pk + z1*O' = (z0*sk + z1*t) * G
719        //
720        // LHS: s * I_m = (k + c*x) * (z0 + z1*d) * G
721        // RHS: R + c * O_m = k*(z0 + z1*d)*G + c*(z0*sk + z1*t)*G
722        //
723        // Since x = (z0*sk + z1*t) / (z0 + z1*d):
724        //   LHS = (k + c*x) * (z0 + z1*d) * G
725        //       = k*(z0 + z1*d)*G + c * [(z0*sk + z1*t) / (z0 + z1*d)] * (z0 + z1*d) * G
726        //       = k*(z0 + z1*d)*G + c*(z0*sk + z1*t)*G
727        //       = RHS
728        let public = Public::<S>(pk);
729        assert!(
730            public.verify(fake_io, ad, &forged_proof).is_ok(),
731            "Forged proof must verify when input discrete log is known"
732        );
733    }
734}