1use ff::{Field, PrimeField, PrimeFieldBits};
10use group::{Curve, GroupEncoding};
11use halo2_proofs::circuit::Value;
12use pasta_curves::{
13 arithmetic::{CurveAffine, CurveExt},
14 pallas,
15};
16use rand::{CryptoRng, RngCore};
17use std::{iter, vec::Vec};
18
19use orchard::{
20 constants::{
21 fixed_bases::{COMMIT_IVK_PERSONALIZATION, NOTE_COMMITMENT_PERSONALIZATION},
22 L_ORCHARD_BASE, L_VALUE,
23 },
24 keys::{FullViewingKey, Scope, SpendValidatingKey},
25 note::{commitment::ExtractedNoteCommitment, nullifier::Nullifier, Note, RandomSeed, Rho},
26 spec::NonIdentityPallasPoint,
27 tree::MerklePath,
28 value::NoteValue,
29};
30
31use super::{
32 circuit::{self, rho_binding_hash, van_commitment_hash, NoteSlotWitness},
33 imt::{derive_nullifier_domain, gov_null_hash, ImtProofData, ImtProvider},
34};
35use crate::circuit::elgamal::base_to_scalar;
36use crate::protocol_hash::poseidon_hash_2;
37
38pub(crate) const PADDING_PERSONALIZATION: &str = "shielded-vote/padding-v1";
46
47#[derive(Clone, Debug)]
49pub struct PaddedNoteData {
50 pub rho: [u8; 32],
52 pub rseed: [u8; 32],
54}
55
56#[derive(Clone, Debug)]
60pub struct PrecomputedRandomness {
61 pub padded_notes: Vec<PaddedNoteData>,
63 pub rseed_signed: [u8; 32],
65 pub rseed_output: [u8; 32],
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum PrecomputedRandomnessLocation {
72 PaddedNote(usize),
74 SignedNote,
76 OutputNote,
78}
79
80#[derive(Debug)]
82pub struct RealNoteInput {
83 pub note: Note,
85 pub fvk: FullViewingKey,
87 pub merkle_path: MerklePath,
89 pub imt_proof: ImtProofData,
94 pub scope: Scope,
96}
97
98#[derive(Debug)]
100pub struct DelegationBundle {
101 pub circuit: circuit::Circuit,
103 pub instance: circuit::Instance,
105}
106
107fn point_x(point: &pallas::Point) -> pallas::Base {
111 point_x_opt(point).expect("ExtractP requires a non-identity Pallas point")
112}
113
114fn point_x_opt(point: &pallas::Point) -> Option<pallas::Base> {
115 point
116 .to_affine()
117 .coordinates()
118 .into_option()
119 .map(|coords| *coords.x())
120}
121
122fn byte_bits(bytes: [u8; 32]) -> impl Iterator<Item = bool> {
126 bytes
127 .into_iter()
128 .flat_map(|byte| (0..8).map(move |bit| ((byte >> bit) & 1) == 1))
129}
130
131fn u64_bits(value: u64) -> impl Iterator<Item = bool> {
134 value
135 .to_le_bytes()
136 .into_iter()
137 .flat_map(|byte| (0..8).map(move |bit| ((byte >> bit) & 1) == 1))
138}
139
140pub(crate) fn external_ivk_scalar(fvk: &FullViewingKey, ak: &SpendValidatingKey) -> pallas::Scalar {
148 let ak_point: pallas::Point = ak.into();
149 let ak_x = point_x(&ak_point);
150 let rivk = fvk.rivk(Scope::External).inner();
151 let domain = sinsemilla::CommitDomain::new(COMMIT_IVK_PERSONALIZATION);
152 let ivk = domain
153 .short_commit(
154 iter::empty()
155 .chain(ak_x.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
156 .chain(
157 fvk.nk()
158 .inner()
159 .to_le_bits()
160 .iter()
161 .by_vals()
162 .take(L_ORCHARD_BASE),
163 ),
164 &rivk,
165 )
166 .expect("external ivk must not be bottom");
167 base_to_scalar(ivk).expect("external ivk must fit in the scalar field")
168}
169
170fn non_identity_padding_point(
179 point: pallas::Point,
180 slot_index: usize,
181 component: &'static str,
182) -> Result<NonIdentityPallasPoint, DelegationBuildError> {
183 NonIdentityPallasPoint::from_bytes(&point.to_affine().to_bytes())
184 .into_option()
185 .ok_or(DelegationBuildError::InvalidPaddingPoint {
186 slot_index,
187 component,
188 })
189}
190
191pub(crate) fn padding_points(
199 slot_index: usize,
200 ivk: pallas::Scalar,
201) -> Result<(NonIdentityPallasPoint, NonIdentityPallasPoint), DelegationBuildError> {
202 let slot_index = u32::try_from(slot_index).expect("padding slot index fits in u32");
203 let g_d_pad = pallas::Point::hash_to_curve(PADDING_PERSONALIZATION)(&slot_index.to_le_bytes());
204 let pk_d_pad = g_d_pad * ivk;
205 Ok((
206 non_identity_padding_point(g_d_pad, slot_index as usize, "g_d")?,
207 non_identity_padding_point(pk_d_pad, slot_index as usize, "pk_d")?,
208 ))
209}
210
211fn random_seed_for_rho(rho: &Rho, rng: &mut impl RngCore) -> RandomSeed {
216 loop {
217 let mut rseed = [0u8; 32];
218 rng.fill_bytes(&mut rseed);
219 let rseed = RandomSeed::from_bytes(rseed, rho);
220 if bool::from(rseed.is_some()) {
221 return rseed.unwrap();
222 }
223 }
224}
225
226fn note_commitment_point(
234 g_d: pallas::Point,
235 pk_d: pallas::Point,
236 value: NoteValue,
237 rho: pallas::Base,
238 psi: pallas::Base,
239 rcm: pallas::Scalar,
240) -> Option<pallas::Point> {
241 let domain = sinsemilla::CommitDomain::new(NOTE_COMMITMENT_PERSONALIZATION);
242 domain
244 .commit(
245 iter::empty()
246 .chain(byte_bits(g_d.to_affine().to_bytes()))
247 .chain(byte_bits(pk_d.to_affine().to_bytes()))
248 .chain(u64_bits(value.inner()).take(L_VALUE))
249 .chain(rho.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
250 .chain(psi.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
251 &rcm,
252 )
253 .into_option()
254}
255
256fn derive_note_nullifier(
261 nk: pallas::Base,
262 rho: pallas::Base,
263 psi: pallas::Base,
264 cm: pallas::Point,
265) -> Option<pallas::Base> {
266 let k = pallas::Point::hash_to_curve("z.cash:Orchard")(b"K");
267 let prf_nf = poseidon_hash_2(nk, rho);
268 let scalar = pallas::Scalar::from_repr((prf_nf + psi).to_repr())
270 .expect("Pallas base field is smaller than its scalar field");
271 point_x_opt(&(k * scalar + cm))
272}
273
274struct PaddingSlot {
276 witness: NoteSlotWitness,
277 cmx: pallas::Base,
278 v_raw: u64,
279 gov_null: pallas::Base,
280 #[cfg(test)]
281 real_nf: pallas::Base,
282}
283
284#[allow(clippy::too_many_arguments)]
285fn build_padding_slot(
286 slot_index: usize,
287 pad_idx: usize,
288 nk: pallas::Base,
289 dom: pallas::Base,
290 ivk: pallas::Scalar,
291 imt_provider: &impl ImtProvider,
292 rng: &mut impl RngCore,
293 precomputed: Option<&PrecomputedRandomness>,
294) -> Result<PaddingSlot, DelegationBuildError> {
295 let (g_d_pad, pk_d_pad) = padding_points(slot_index, ivk)?;
296 let location = PrecomputedRandomnessLocation::PaddedNote(pad_idx);
297
298 let (rho, rseed) = if let Some(pre) = precomputed {
299 if pad_idx >= pre.padded_notes.len() {
301 return Err(DelegationBuildError::MissingPrecomputedPaddedNote {
302 index: pad_idx,
303 actual: pre.padded_notes.len(),
304 });
305 }
306 let pd = &pre.padded_notes[pad_idx];
307 let rho = Rho::from_bytes(&pd.rho)
308 .into_option()
309 .ok_or(DelegationBuildError::InvalidPrecomputedRho { index: pad_idx })?;
310 let rseed = RandomSeed::from_bytes(pd.rseed, &rho)
311 .into_option()
312 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
313 (rho, rseed)
314 } else {
315 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
316 let rseed = random_seed_for_rho(&rho, &mut *rng);
317 (rho, rseed)
318 };
319
320 let psi = rseed.psi(&rho);
321 let rcm = rseed.rcm(&rho);
322 let cm = note_commitment_point(
323 *g_d_pad,
324 *pk_d_pad,
325 NoteValue::ZERO,
326 rho.into_inner(),
327 psi,
328 rcm.inner(),
329 )
330 .ok_or(DelegationBuildError::InvalidPaddingNoteCommitment { location })?;
331 let cmx = point_x(&cm);
332
333 let real_nf = derive_note_nullifier(nk, rho.into_inner(), psi, cm)
334 .ok_or(DelegationBuildError::InvalidPaddingNullifier { location })?;
335 let gov_null = gov_null_hash(nk, dom, real_nf);
336 let imt_proof = imt_provider.non_membership_proof(real_nf)?;
337
338 let merkle_path = MerklePath::dummy(&mut *rng);
341 let witness = NoteSlotWitness {
342 g_d: Value::known(g_d_pad),
343 pk_d: Value::known(pk_d_pad),
344 v: Value::known(NoteValue::ZERO),
345 rho: Value::known(rho.into_inner()),
346 psi: Value::known(psi),
347 rcm: Value::known(rcm),
348 cm: Value::known(cm),
349 path: Value::known(merkle_path.auth_path()),
350 pos: Value::known(merkle_path.position()),
351 imt_nf_bounds: Value::known(imt_proof.nf_bounds),
352 imt_leaf_pos: Value::known(imt_proof.leaf_pos),
353 imt_path: Value::known(imt_proof.path),
354 is_internal: Value::known(false),
355 };
356
357 Ok(PaddingSlot {
358 witness,
359 cmx,
360 v_raw: 0,
361 gov_null,
362 #[cfg(test)]
363 real_nf,
364 })
365}
366
367#[cfg(test)]
368pub(crate) struct PaddingSlotForTesting {
369 pub witness: NoteSlotWitness,
370 pub cmx: pallas::Base,
371 pub gov_null: pallas::Base,
372 pub real_nf: pallas::Base,
373}
374
375#[cfg(test)]
376pub(crate) fn build_padding_slot_for_testing(
377 slot_index: usize,
378 pad_idx: usize,
379 fvk: &FullViewingKey,
380 ak: &SpendValidatingKey,
381 dom: pallas::Base,
382 imt_provider: &impl ImtProvider,
383 rng: &mut impl RngCore,
384) -> Result<PaddingSlotForTesting, DelegationBuildError> {
385 let padding = build_padding_slot(
386 slot_index,
387 pad_idx,
388 fvk.nk().inner(),
389 dom,
390 external_ivk_scalar(fvk, ak),
391 imt_provider,
392 rng,
393 None,
394 )?;
395
396 Ok(PaddingSlotForTesting {
397 witness: padding.witness,
398 cmx: padding.cmx,
399 gov_null: padding.gov_null,
400 real_nf: padding.real_nf,
401 })
402}
403
404#[derive(Clone, Debug)]
406pub enum DelegationBuildError {
407 InvalidNoteCount(usize),
409 Instance(circuit::InstanceError),
411 InvalidPaddingPoint {
413 slot_index: usize,
414 component: &'static str,
415 },
416 InvalidPaddingNoteCommitment {
418 location: PrecomputedRandomnessLocation,
419 },
420 InvalidPaddingNullifier {
422 location: PrecomputedRandomnessLocation,
423 },
424 MissingPrecomputedPaddedNote { index: usize, actual: usize },
426 InvalidPrecomputedRho { index: usize },
428 InvalidPrecomputedRseed {
430 location: PrecomputedRandomnessLocation,
431 },
432 InvalidPrecomputedNote {
434 location: PrecomputedRandomnessLocation,
435 },
436 ImtFetchFailed(super::imt::ImtError),
438}
439
440impl From<circuit::InstanceError> for DelegationBuildError {
441 fn from(e: circuit::InstanceError) -> Self {
442 DelegationBuildError::Instance(e)
443 }
444}
445
446impl From<super::imt::ImtError> for DelegationBuildError {
447 fn from(e: super::imt::ImtError) -> Self {
448 DelegationBuildError::ImtFetchFailed(e)
449 }
450}
451
452impl std::fmt::Display for DelegationBuildError {
453 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454 match self {
455 DelegationBuildError::InvalidNoteCount(n) => {
456 write!(
457 f,
458 "invalid note count: {} (expected 1–{})",
459 n,
460 circuit::MAX_REAL_NOTES
461 )
462 }
463 DelegationBuildError::Instance(e) => {
464 write!(f, "instance construction failed: {e}")
465 }
466 DelegationBuildError::InvalidPaddingPoint {
467 slot_index,
468 component,
469 } => {
470 write!(
471 f,
472 "invalid padding point {component} at slot {slot_index}: identity point"
473 )
474 }
475 DelegationBuildError::InvalidPaddingNoteCommitment { location } => {
476 write!(f, "invalid padding note commitment for {location}")
477 }
478 DelegationBuildError::InvalidPaddingNullifier { location } => {
479 write!(f, "invalid padding nullifier for {location}")
480 }
481 DelegationBuildError::MissingPrecomputedPaddedNote { index, actual } => {
482 write!(
483 f,
484 "missing precomputed padded note at index {index} (got {actual} entries)"
485 )
486 }
487 DelegationBuildError::InvalidPrecomputedRho { index } => {
488 write!(f, "invalid precomputed padded note rho at index {index}")
489 }
490 DelegationBuildError::InvalidPrecomputedRseed { location } => {
491 write!(f, "invalid precomputed rseed for {location}")
492 }
493 DelegationBuildError::InvalidPrecomputedNote { location } => {
494 write!(f, "invalid precomputed note components for {location}")
495 }
496 DelegationBuildError::ImtFetchFailed(e) => {
497 write!(f, "IMT proof fetch failed: {e}")
498 }
499 }
500 }
501}
502
503impl std::fmt::Display for PrecomputedRandomnessLocation {
504 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505 match self {
506 PrecomputedRandomnessLocation::PaddedNote(index) => {
507 write!(f, "padded note {index}")
508 }
509 PrecomputedRandomnessLocation::SignedNote => write!(f, "signed note"),
510 PrecomputedRandomnessLocation::OutputNote => write!(f, "output note"),
511 }
512 }
513}
514
515#[allow(clippy::too_many_arguments)]
550pub fn build_delegation_bundle(
551 real_notes: Vec<RealNoteInput>,
552 fvk: &FullViewingKey,
553 alpha: pallas::Scalar,
554 output_recipient: orchard::Address,
555 vote_round_id: pallas::Base,
556 nc_root: pallas::Base,
557 van_comm_rand: pallas::Base,
558 imt_provider: &impl ImtProvider,
559 rng: &mut (impl RngCore + CryptoRng),
560 precomputed: Option<&PrecomputedRandomness>,
561) -> Result<DelegationBundle, DelegationBuildError> {
562 let n_real = real_notes.len();
565 if n_real == 0 || n_real > circuit::MAX_REAL_NOTES {
566 return Err(DelegationBuildError::InvalidNoteCount(n_real));
567 }
568
569 let nf_imt_root = imt_provider.root();
571
572 let nk_val = fvk.nk().inner();
574 let ak: SpendValidatingKey = fvk.clone().into();
575 let ivk = external_ivk_scalar(fvk, &ak);
576
577 let dom = derive_nullifier_domain(vote_round_id);
579
580 let mut note_slots = Vec::with_capacity(circuit::MAX_REAL_NOTES);
582 let mut cmx_values = Vec::with_capacity(circuit::MAX_REAL_NOTES);
583 let mut v_values = Vec::with_capacity(circuit::MAX_REAL_NOTES);
584 let mut gov_nulls = Vec::with_capacity(circuit::MAX_REAL_NOTES);
585
586 for input in &real_notes {
589 let note = &input.note;
590 let rho = note.rho();
591 let psi = note.rseed().psi(&rho);
592 let rcm = note.rseed().rcm(&rho);
593 let cm = note.commitment();
594 let cmx = ExtractedNoteCommitment::from(cm.clone()).inner();
595 let v_raw = note.value().inner();
596 let recipient = note.recipient();
597
598 let real_nf = note.nullifier(fvk);
600 let gov_null = gov_null_hash(nk_val, dom, real_nf.inner());
602
603 let slot = NoteSlotWitness {
604 g_d: Value::known(recipient.g_d()),
605 pk_d: Value::known(recipient.pk_d().inner()),
606 v: Value::known(note.value()),
607 rho: Value::known(rho.into_inner()),
608 psi: Value::known(psi),
609 rcm: Value::known(rcm),
610 cm: Value::known(cm.inner()),
611 path: Value::known(input.merkle_path.auth_path()),
612 pos: Value::known(input.merkle_path.position()),
613 imt_nf_bounds: Value::known(input.imt_proof.nf_bounds),
614 imt_leaf_pos: Value::known(input.imt_proof.leaf_pos),
615 imt_path: Value::known(input.imt_proof.path),
616 is_internal: Value::known(matches!(input.scope, Scope::Internal)),
617 };
618
619 note_slots.push(slot);
620 cmx_values.push(cmx);
621 v_values.push(v_raw);
622 gov_nulls.push(gov_null);
623 }
624
625 for i in n_real..circuit::MAX_REAL_NOTES {
629 let pad_idx = i - n_real; let padding =
631 build_padding_slot(i, pad_idx, nk_val, dom, ivk, imt_provider, rng, precomputed)?;
632
633 note_slots.push(padding.witness);
634 cmx_values.push(padding.cmx);
635 v_values.push(padding.v_raw);
636 gov_nulls.push(padding.gov_null);
637 }
638
639 let notes: [NoteSlotWitness; circuit::MAX_REAL_NOTES] =
640 note_slots.try_into().unwrap_or_else(|_| unreachable!());
641
642 let v_total_u64: u64 = v_values.iter().sum();
645 let num_ballots_u64 = v_total_u64 / circuit::BALLOT_DIVISOR;
646 let remainder_u64 = v_total_u64 % circuit::BALLOT_DIVISOR;
647 let num_ballots_field = pallas::Base::from(num_ballots_u64);
648
649 let g_d_new_x = *output_recipient
655 .g_d()
656 .to_affine()
657 .coordinates()
658 .unwrap()
659 .x();
660 let pk_d_new_x = *output_recipient
661 .pk_d()
662 .inner()
663 .to_affine()
664 .coordinates()
665 .unwrap()
666 .x();
667
668 let van_comm = van_commitment_hash(
669 g_d_new_x,
670 pk_d_new_x,
671 num_ballots_field,
672 vote_round_id,
673 van_comm_rand,
674 );
675
676 let rho = rho_binding_hash(
680 cmx_values[0],
681 cmx_values[1],
682 cmx_values[2],
683 cmx_values[3],
684 cmx_values[4],
685 van_comm,
686 vote_round_id,
687 );
688
689 let sender_address = fvk.address_at(0u32, Scope::External);
693 let signed_rho = Rho::from_nf_old(Nullifier::from_inner(rho));
694 let signed_note = if let Some(pre) = precomputed {
695 let location = PrecomputedRandomnessLocation::SignedNote;
696 let rseed = RandomSeed::from_bytes(pre.rseed_signed, &signed_rho)
697 .into_option()
698 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
699 Note::from_parts(sender_address, NoteValue::from_raw(1), signed_rho, rseed)
700 .into_option()
701 .ok_or(DelegationBuildError::InvalidPrecomputedNote { location })?
702 } else {
703 Note::new(
704 sender_address,
705 NoteValue::from_raw(1),
706 signed_rho,
707 &mut *rng,
708 )
709 };
710
711 let nf_signed = signed_note.nullifier(fvk);
713
714 let output_rho = Rho::from_nf_old(nf_signed);
717 let output_note = if let Some(pre) = precomputed {
718 let location = PrecomputedRandomnessLocation::OutputNote;
719 let rseed = RandomSeed::from_bytes(pre.rseed_output, &output_rho)
720 .into_option()
721 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
722 Note::from_parts(output_recipient, NoteValue::ZERO, output_rho, rseed)
723 .into_option()
724 .ok_or(DelegationBuildError::InvalidPrecomputedNote { location })?
725 } else {
726 Note::new(output_recipient, NoteValue::ZERO, output_rho, &mut *rng)
727 };
728 let cmx_new = ExtractedNoteCommitment::from(output_note.commitment()).inner();
729
730 let rk = ak.randomize(&alpha);
732
733 let circuit = circuit::Circuit::from_note_unchecked(fvk, &signed_note, alpha)
738 .with_output_note(&output_note)
739 .with_notes(notes)
740 .with_van_comm_rand(van_comm_rand)
741 .with_ballot_scaling(
742 pallas::Base::from(num_ballots_u64),
743 pallas::Base::from(remainder_u64),
744 );
745
746 let instance = circuit::Instance::from_parts(
747 nf_signed,
748 rk,
749 cmx_new,
750 van_comm,
751 vote_round_id,
752 nc_root,
753 nf_imt_root,
754 [
755 gov_nulls[0],
756 gov_nulls[1],
757 gov_nulls[2],
758 gov_nulls[3],
759 gov_nulls[4],
760 ],
761 dom,
762 )?;
763
764 Ok(DelegationBundle { circuit, instance })
765}
766
767#[cfg(test)]
772mod tests {
773 use super::*;
774 use crate::delegation::imt::{ImtError, SpacedLeafImtProvider};
775 use ff::Field;
776 use halo2_proofs::dev::MockProver;
777 use incrementalmerkletree::{Hashable, Level};
778 use orchard::{
779 constants::MERKLE_DEPTH_ORCHARD,
780 keys::{FullViewingKey, Scope, SpendingKey},
781 note::{commitment::ExtractedNoteCommitment, Note, Rho},
782 tree::{MerkleHashOrchard, MerklePath},
783 value::NoteValue,
784 };
785 use pasta_curves::pallas;
786 use rand::rngs::OsRng;
787 use std::cell::{Cell, RefCell};
788
789 const K: u32 = 14;
791
792 #[derive(Debug)]
793 struct RecordingImtProvider {
794 proof: ImtProofData,
795 error: Option<ImtError>,
796 requested_nfs: RefCell<Vec<pallas::Base>>,
797 }
798
799 impl RecordingImtProvider {
800 fn returning(proof: ImtProofData) -> Self {
801 Self {
802 proof,
803 error: None,
804 requested_nfs: RefCell::new(Vec::new()),
805 }
806 }
807
808 fn failing(error: ImtError) -> Self {
809 Self {
810 proof: test_imt_proof(),
811 error: Some(error),
812 requested_nfs: RefCell::new(Vec::new()),
813 }
814 }
815 }
816
817 impl ImtProvider for RecordingImtProvider {
818 fn root(&self) -> pallas::Base {
819 self.proof.root
820 }
821
822 fn non_membership_proof(&self, nf: pallas::Base) -> Result<ImtProofData, ImtError> {
823 self.requested_nfs.borrow_mut().push(nf);
824 match &self.error {
825 Some(error) => Err(error.clone()),
826 None => Ok(self.proof.clone()),
827 }
828 }
829 }
830
831 fn test_imt_proof() -> ImtProofData {
832 ImtProofData {
833 root: pallas::Base::from(900u64),
834 nf_bounds: [
835 pallas::Base::from(10u64),
836 pallas::Base::from(20u64),
837 pallas::Base::from(30u64),
838 ],
839 leaf_pos: 7,
840 path: std::array::from_fn(|i| pallas::Base::from(1_000u64 + i as u64)),
841 }
842 }
843
844 fn precomputed_padding_note(rng: &mut impl RngCore) -> (PaddedNoteData, Rho, RandomSeed) {
845 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
846 let rseed = random_seed_for_rho(&rho, &mut *rng);
847 (
848 PaddedNoteData {
849 rho: rho.to_bytes(),
850 rseed: *rseed.as_bytes(),
851 },
852 rho,
853 rseed,
854 )
855 }
856
857 fn assert_known<T>(value: &Value<T>, f: impl FnOnce(&T) -> bool) {
858 let checked = Cell::new(false);
859 value.assert_if_known(|actual| {
860 checked.set(true);
861 f(actual)
862 });
863 assert!(checked.get(), "expected known witness value");
864 }
865
866 fn assert_padding_slot_matches(
867 padding: &PaddingSlot,
868 slot_index: usize,
869 nk: pallas::Base,
870 dom: pallas::Base,
871 ivk: pallas::Scalar,
872 rho: Rho,
873 rseed: RandomSeed,
874 imt_proof: &ImtProofData,
875 requested_nfs: &[pallas::Base],
876 ) {
877 let (g_d_pad, pk_d_pad) =
878 padding_points(slot_index, ivk).expect("test padding points should be valid");
879 let psi = rseed.psi(&rho);
880 let rcm = rseed.rcm(&rho);
881 let cm = note_commitment_point(
882 *g_d_pad,
883 *pk_d_pad,
884 NoteValue::ZERO,
885 rho.into_inner(),
886 psi,
887 rcm.inner(),
888 )
889 .expect("test padding commitment should be valid");
890 let real_nf = derive_note_nullifier(nk, rho.into_inner(), psi, cm)
891 .expect("test padding nullifier should be valid");
892
893 assert_eq!(padding.cmx, point_x(&cm));
894 assert_eq!(padding.v_raw, 0);
895 assert_eq!(padding.gov_null, gov_null_hash(nk, dom, real_nf));
896 assert_eq!(requested_nfs, &[real_nf]);
897
898 assert_known(&padding.witness.g_d, |actual| *actual == g_d_pad);
899 assert_known(&padding.witness.pk_d, |actual| *actual == pk_d_pad);
900 assert_known(&padding.witness.v, |actual| *actual == NoteValue::ZERO);
901 assert_known(&padding.witness.rho, |actual| *actual == rho.into_inner());
902 assert_known(&padding.witness.psi, |actual| *actual == psi);
903 assert_known(&padding.witness.rcm, |actual| actual.inner() == rcm.inner());
904 assert_known(&padding.witness.cm, |actual| *actual == cm);
905 assert_known(&padding.witness.imt_nf_bounds, |actual| {
906 *actual == imt_proof.nf_bounds
907 });
908 assert_known(&padding.witness.imt_leaf_pos, |actual| {
909 *actual == imt_proof.leaf_pos
910 });
911 assert_known(&padding.witness.imt_path, |actual| {
912 *actual == imt_proof.path
913 });
914 assert_known(&padding.witness.is_internal, |actual| !*actual);
915 }
916
917 fn make_real_note_inputs(
924 fvk: &FullViewingKey,
925 values: &[u64],
926 scopes: &[Scope],
927 imt_provider: &impl ImtProvider,
928 rng: &mut impl RngCore,
929 ) -> (Vec<RealNoteInput>, pallas::Base) {
930 let n = values.len();
931 assert!(n >= 1 && n <= circuit::MAX_REAL_NOTES);
932 assert_eq!(n, scopes.len());
933
934 let mut notes = Vec::with_capacity(n);
936 for (idx, &v) in values.iter().enumerate() {
937 let recipient = fvk.address_at(0u32, scopes[idx]);
938 let note_value = NoteValue::from_raw(v);
939 let (_, _, dummy_parent) = Note::dummy(&mut *rng, None);
940 let note = Note::new(
941 recipient,
942 note_value,
943 Rho::from_nf_old(dummy_parent.nullifier(fvk)),
944 &mut *rng,
945 );
946 notes.push(note);
947 }
948
949 let empty_leaf = MerkleHashOrchard::empty_leaf();
951 let mut leaves = [empty_leaf; 8];
952 for (i, note) in notes.iter().enumerate() {
953 let cmx = ExtractedNoteCommitment::from(note.commitment());
954 leaves[i] = MerkleHashOrchard::from_cmx(&cmx);
955 }
956
957 let l1_0 = MerkleHashOrchard::combine(Level::from(0), &leaves[0], &leaves[1]);
959 let l1_1 = MerkleHashOrchard::combine(Level::from(0), &leaves[2], &leaves[3]);
960 let l1_2 = MerkleHashOrchard::combine(Level::from(0), &leaves[4], &leaves[5]);
961 let l1_3 = MerkleHashOrchard::combine(Level::from(0), &leaves[6], &leaves[7]);
962 let l2_0 = MerkleHashOrchard::combine(Level::from(1), &l1_0, &l1_1);
963 let l2_1 = MerkleHashOrchard::combine(Level::from(1), &l1_2, &l1_3);
964 let l3_0 = MerkleHashOrchard::combine(Level::from(2), &l2_0, &l2_1);
965
966 let mut current = l3_0;
968 for level in 3..MERKLE_DEPTH_ORCHARD {
969 let sibling = MerkleHashOrchard::empty_root(Level::from(level as u8));
970 current = MerkleHashOrchard::combine(Level::from(level as u8), ¤t, &sibling);
971 }
972 let nc_root = current.inner();
973
974 let l1 = [l1_0, l1_1, l1_2, l1_3];
976 let l2 = [l2_0, l2_1];
977 let mut inputs = Vec::with_capacity(n);
978 for (i, note) in notes.into_iter().enumerate() {
979 let mut auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
980 auth_path[0] = leaves[i ^ 1];
981 auth_path[1] = l1[(i >> 1) ^ 1];
982 auth_path[2] = l2[1 - (i >> 2)];
983 for level in 3..MERKLE_DEPTH_ORCHARD {
984 auth_path[level] = MerkleHashOrchard::empty_root(Level::from(level as u8));
985 }
986 let merkle_path = MerklePath::from_parts(i as u32, auth_path);
987
988 let real_nf = note.nullifier(fvk);
989 let imt_proof = imt_provider.non_membership_proof(real_nf.inner()).unwrap();
990
991 inputs.push(RealNoteInput {
992 note,
993 fvk: fvk.clone(),
994 merkle_path,
995 imt_proof,
996 scope: scopes[i],
997 });
998 }
999
1000 (inputs, nc_root)
1001 }
1002
1003 fn build_bundle(values: &[u64], scopes: &[Scope]) -> DelegationBundle {
1005 assert_eq!(values.len(), scopes.len());
1006 let mut rng = OsRng;
1007 let sk = SpendingKey::random(&mut rng);
1008 let fvk: FullViewingKey = (&sk).into();
1009 let output_recipient = fvk.address_at(1u32, Scope::External);
1010 let vote_round_id = pallas::Base::random(&mut rng);
1011 let van_comm_rand = pallas::Base::random(&mut rng);
1012 let alpha = pallas::Scalar::random(&mut rng);
1013
1014 let imt = SpacedLeafImtProvider::new();
1015 let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
1016
1017 let bundle = build_delegation_bundle(
1018 inputs,
1019 &fvk,
1020 alpha,
1021 output_recipient,
1022 vote_round_id,
1023 nc_root,
1024 van_comm_rand,
1025 &imt,
1026 &mut rng,
1027 None,
1028 )
1029 .unwrap();
1030
1031 assert_delegation_output_shape(&bundle);
1032 bundle
1033 }
1034
1035 fn build_single_note_bundle_with_precomputed(
1036 precomputed: &PrecomputedRandomness,
1037 ) -> Result<DelegationBundle, DelegationBuildError> {
1038 let mut rng = OsRng;
1039 let sk = SpendingKey::random(&mut rng);
1040 let fvk: FullViewingKey = (&sk).into();
1041
1042 build_single_note_bundle_with_fvk_and_precomputed(&fvk, precomputed, &mut rng)
1043 }
1044
1045 fn build_single_note_bundle_with_fvk_and_precomputed(
1046 fvk: &FullViewingKey,
1047 precomputed: &PrecomputedRandomness,
1048 rng: &mut (impl RngCore + CryptoRng),
1049 ) -> Result<DelegationBundle, DelegationBuildError> {
1050 let output_recipient = fvk.address_at(1u32, Scope::External);
1051 let vote_round_id = pallas::Base::random(&mut *rng);
1052 let van_comm_rand = pallas::Base::random(&mut *rng);
1053 let alpha = pallas::Scalar::random(&mut *rng);
1054
1055 let imt = SpacedLeafImtProvider::new();
1056 let (inputs, nc_root) =
1057 make_real_note_inputs(fvk, &[13_000_000], &[Scope::External], &imt, &mut *rng);
1058
1059 build_delegation_bundle(
1060 inputs,
1061 fvk,
1062 alpha,
1063 output_recipient,
1064 vote_round_id,
1065 nc_root,
1066 van_comm_rand,
1067 &imt,
1068 rng,
1069 Some(precomputed),
1070 )
1071 }
1072
1073 fn make_valid_padded_note_data(rng: &mut impl RngCore) -> PaddedNoteData {
1074 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
1075 let rseed = random_seed_for_rho(&rho, rng);
1076
1077 PaddedNoteData {
1078 rho: rho.to_bytes(),
1079 rseed: *rseed.as_bytes(),
1080 }
1081 }
1082
1083 fn assert_delegation_output_shape(bundle: &DelegationBundle) {
1084 let pi = bundle.instance.to_halo2_instance();
1085 assert_eq!(pi.len(), 14, "delegation public input shape changed");
1086 assert_eq!(bundle.instance.gov_null.len(), 5);
1087 assert_eq!(pi[0], bundle.instance.nf_signed.inner());
1088 assert_eq!(pi[3], bundle.instance.cmx_new);
1089 assert_eq!(pi[4], bundle.instance.van_comm);
1090 assert_eq!(pi[5], bundle.instance.vote_round_id);
1091 assert_eq!(pi[6], bundle.instance.nc_root);
1092 assert_eq!(pi[7], bundle.instance.nf_imt_root);
1093 assert_eq!(&pi[8..13], &bundle.instance.gov_null);
1094 assert_eq!(pi[13], bundle.instance.dom);
1095 }
1096
1097 fn verify_bundle(bundle: &DelegationBundle) {
1098 let pi = bundle.instance.to_halo2_instance();
1100 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
1101 assert_eq!(prover.verify(), Ok(()), "merged circuit failed");
1102 }
1103
1104 #[test]
1105 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1106 fn test_single_real_note() {
1107 let bundle = build_bundle(&[13_000_000], &[Scope::External]);
1108 verify_bundle(&bundle);
1109 }
1110
1111 fn build_bundle_for_inspection(
1115 values: &[u64],
1116 scopes: &[Scope],
1117 ) -> (DelegationBundle, FullViewingKey, SpendValidatingKey) {
1118 let mut rng = OsRng;
1119 let sk = SpendingKey::random(&mut rng);
1120 let fvk: FullViewingKey = (&sk).into();
1121 let ak: SpendValidatingKey = fvk.clone().into();
1122 let output_recipient = fvk.address_at(1u32, Scope::External);
1123 let vote_round_id = pallas::Base::random(&mut rng);
1124 let van_comm_rand = pallas::Base::random(&mut rng);
1125 let alpha = pallas::Scalar::random(&mut rng);
1126 let imt = SpacedLeafImtProvider::new();
1127 let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
1128
1129 let bundle = build_delegation_bundle(
1130 inputs,
1131 &fvk,
1132 alpha,
1133 output_recipient,
1134 vote_round_id,
1135 nc_root,
1136 van_comm_rand,
1137 &imt,
1138 &mut rng,
1139 None,
1140 )
1141 .unwrap();
1142 (bundle, fvk, ak)
1143 }
1144
1145 #[test]
1146 fn test_single_real_note_locks_padding_witnesses() {
1147 let (bundle, fvk, ak) = build_bundle_for_inspection(&[13_000_000], &[Scope::External]);
1153 let ivk = external_ivk_scalar(&fvk, &ak);
1154 let notes = bundle.circuit.notes_for_testing();
1155 for slot_index in 1..5 {
1156 let (expected_g_d, expected_pk_d) =
1157 padding_points(slot_index, ivk).expect("test padding points should be valid");
1158 assert_known(¬es[slot_index].g_d, |actual| *actual == expected_g_d);
1159 assert_known(¬es[slot_index].pk_d, |actual| *actual == expected_pk_d);
1160 }
1161 }
1162
1163 #[test]
1164 fn test_five_real_notes_uses_no_padding() {
1165 let (bundle, fvk, ak) = build_bundle_for_inspection(&[2_500_000; 5], &[Scope::External; 5]);
1172 let ivk = external_ivk_scalar(&fvk, &ak);
1173 let padding_g_ds: Vec<_> = (0..5)
1174 .map(|i| {
1175 padding_points(i, ivk)
1176 .expect("test padding points should be valid")
1177 .0
1178 })
1179 .collect();
1180 let notes = bundle.circuit.notes_for_testing();
1181 for slot_index in 0..5 {
1182 for pad_g_d in &padding_g_ds {
1183 assert_known(¬es[slot_index].g_d, |actual| actual != pad_g_d);
1184 }
1185 }
1186 }
1187
1188 #[test]
1189 fn test_padding_points_are_synthetic_and_ivk_bound() {
1190 let mut rng = OsRng;
1191 let sk = SpendingKey::random(&mut rng);
1192 let fvk: FullViewingKey = (&sk).into();
1193 let ak: SpendValidatingKey = fvk.clone().into();
1194 let ivk = external_ivk_scalar(&fvk, &ak);
1195
1196 for slot_index in 1..5 {
1197 let (g_d_pad, pk_d_pad) =
1198 padding_points(slot_index, ivk).expect("test padding points should be valid");
1199 let real_orchard_addr = fvk.address_at(slot_index as u32, Scope::External);
1200
1201 assert_eq!(*pk_d_pad, *g_d_pad * ivk);
1205 assert_ne!(
1206 g_d_pad.to_affine().to_bytes(),
1207 real_orchard_addr.g_d().to_affine().to_bytes()
1208 );
1209 assert_ne!(
1210 pk_d_pad.to_affine().to_bytes(),
1211 real_orchard_addr.pk_d().to_bytes()
1212 );
1213 }
1214 }
1215
1216 #[test]
1231 fn test_padding_personalization_is_domain_separated_from_orchard() {
1232 use orchard::constants::KEY_DIVERSIFICATION_PERSONALIZATION;
1233
1234 assert_ne!(
1235 PADDING_PERSONALIZATION, KEY_DIVERSIFICATION_PERSONALIZATION,
1236 "padding personalization must be domain-separated from Orchard's \
1237 DiversifyHash personalization; otherwise synthetic padding `g_d_pad` \
1238 can collide with real diversified bases and ZCA-450's fix regresses"
1239 );
1240 }
1241
1242 fn fixture_real_note(
1256 scope: Scope,
1257 rng: &mut impl RngCore,
1258 ) -> (FullViewingKey, SpendValidatingKey, Note) {
1259 let sk = SpendingKey::random(rng);
1260 let fvk: FullViewingKey = (&sk).into();
1261 let ak: SpendValidatingKey = fvk.clone().into();
1262 let recipient = fvk.address_at(0u32, scope);
1263 let (_, _, dummy_parent) = Note::dummy(rng, None);
1264 let note = Note::new(
1265 recipient,
1266 NoteValue::from_raw(12_500_000),
1267 Rho::from_nf_old(dummy_parent.nullifier(&fvk)),
1268 rng,
1269 );
1270 (fvk, ak, note)
1271 }
1272
1273 #[test]
1274 fn test_note_commitment_point_matches_orchard() {
1275 let mut rng = OsRng;
1280 for scope in [Scope::External, Scope::Internal] {
1281 let (_fvk, _ak, note) = fixture_real_note(scope, &mut rng);
1282 let recipient = note.recipient();
1283 let rho = note.rho();
1284 let psi = note.rseed().psi(&rho);
1285 let rcm = note.rseed().rcm(&rho);
1286
1287 let mirrored = note_commitment_point(
1288 *recipient.g_d(),
1289 *recipient.pk_d().inner(),
1290 note.value(),
1291 rho.into_inner(),
1292 psi,
1293 rcm.inner(),
1294 );
1295 let orchard = note.commitment().inner();
1296
1297 assert_eq!(
1298 mirrored,
1299 Some(orchard),
1300 "note_commitment_point drifted from Orchard NoteCommitment::derive ({scope:?})"
1301 );
1302 }
1303 }
1304
1305 #[test]
1306 fn test_derive_note_nullifier_matches_orchard() {
1307 let mut rng = OsRng;
1312 for scope in [Scope::External, Scope::Internal] {
1313 let (fvk, _ak, note) = fixture_real_note(scope, &mut rng);
1314 let nk = fvk.nk().inner();
1315 let rho = note.rho();
1316 let psi = note.rseed().psi(&rho);
1317 let cm = note.commitment().inner();
1318
1319 let mirrored = derive_note_nullifier(nk, rho.into_inner(), psi, cm);
1320 let orchard = note.nullifier(&fvk).inner();
1321
1322 assert_eq!(
1323 mirrored,
1324 Some(orchard),
1325 "derive_note_nullifier drifted from Orchard Nullifier::derive ({scope:?})"
1326 );
1327 }
1328 }
1329
1330 #[test]
1331 fn test_external_ivk_scalar_matches_orchard_address_derivation() {
1332 let mut rng = OsRng;
1339 let sk = SpendingKey::random(&mut rng);
1340 let fvk: FullViewingKey = (&sk).into();
1341 let ak: SpendValidatingKey = fvk.clone().into();
1342 let ivk = external_ivk_scalar(&fvk, &ak);
1343
1344 for idx in [0u32, 1, 7, 1234] {
1347 let addr = fvk.address_at(idx, Scope::External);
1348 assert_eq!(
1349 *addr.g_d() * ivk,
1350 *addr.pk_d().inner(),
1351 "external_ivk_scalar drifted: [ivk] * g_d != pk_d at diversifier index {idx}"
1352 );
1353 }
1354
1355 let internal_addr = fvk.address_at(0u32, Scope::Internal);
1359 assert_ne!(
1360 *internal_addr.g_d() * ivk,
1361 *internal_addr.pk_d().inner(),
1362 "external_ivk_scalar incorrectly validates an internal-scope address"
1363 );
1364 }
1365
1366 #[test]
1367 fn test_build_padding_slot_fresh_randomness_populates_strict_witnesses() {
1368 let mut rng = OsRng;
1369 let sk = SpendingKey::random(&mut rng);
1370 let fvk: FullViewingKey = (&sk).into();
1371 let ak: SpendValidatingKey = fvk.clone().into();
1372 let nk = fvk.nk().inner();
1373 let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
1374 let ivk = external_ivk_scalar(&fvk, &ak);
1375 let imt_proof = test_imt_proof();
1376 let imt = RecordingImtProvider::returning(imt_proof.clone());
1377
1378 let padding = build_padding_slot(3, 0, nk, dom, ivk, &imt, &mut rng, None).unwrap();
1379
1380 let (g_d_pad, pk_d_pad) =
1381 padding_points(3, ivk).expect("test padding points should be valid");
1382 assert_known(&padding.witness.g_d, |actual| *actual == g_d_pad);
1383 assert_known(&padding.witness.pk_d, |actual| *actual == pk_d_pad);
1384 let generated_padding_values = padding
1385 .witness
1386 .rho
1387 .as_ref()
1388 .copied()
1389 .zip(padding.witness.psi.as_ref().copied())
1390 .zip(padding.witness.rcm.as_ref().cloned())
1391 .zip(padding.witness.cm.as_ref().copied());
1392 assert_known(
1393 &generated_padding_values,
1394 |(((rho_inner, psi), rcm), cm_witness)| {
1395 let cm = note_commitment_point(
1396 *g_d_pad,
1397 *pk_d_pad,
1398 NoteValue::ZERO,
1399 *rho_inner,
1400 *psi,
1401 rcm.inner(),
1402 )
1403 .expect("test padding commitment should be valid");
1404 let real_nf = derive_note_nullifier(nk, *rho_inner, *psi, cm)
1405 .expect("test padding nullifier should be valid");
1406
1407 *cm_witness == cm
1408 && padding.cmx == point_x(&cm)
1409 && padding.gov_null == gov_null_hash(nk, dom, real_nf)
1410 && imt.requested_nfs.borrow().as_slice() == [real_nf]
1411 },
1412 );
1413 assert_known(&padding.witness.v, |actual| *actual == NoteValue::ZERO);
1414 assert_known(&padding.witness.is_internal, |actual| !*actual);
1415 assert_known(&padding.witness.imt_nf_bounds, |actual| {
1416 *actual == imt_proof.nf_bounds
1417 });
1418 assert_known(&padding.witness.imt_leaf_pos, |actual| {
1419 *actual == imt_proof.leaf_pos
1420 });
1421 assert_known(&padding.witness.imt_path, |actual| {
1422 *actual == imt_proof.path
1423 });
1424 assert_eq!(padding.v_raw, 0);
1425 assert_eq!(imt.requested_nfs.borrow().len(), 1);
1426 }
1427
1428 #[test]
1429 fn test_build_padding_slot_reuses_selected_precomputed_randomness() {
1430 let mut rng = OsRng;
1431 let sk = SpendingKey::random(&mut rng);
1432 let fvk: FullViewingKey = (&sk).into();
1433 let ak: SpendValidatingKey = fvk.clone().into();
1434 let nk = fvk.nk().inner();
1435 let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
1436 let ivk = external_ivk_scalar(&fvk, &ak);
1437 let imt_proof = test_imt_proof();
1438 let imt = RecordingImtProvider::returning(imt_proof.clone());
1439
1440 let (unused_pd, _, _) = precomputed_padding_note(&mut rng);
1441 let (selected_pd, selected_rho, selected_rseed) = precomputed_padding_note(&mut rng);
1442 let precomputed = PrecomputedRandomness {
1443 padded_notes: vec![unused_pd, selected_pd],
1444 rseed_signed: [0; 32],
1445 rseed_output: [0; 32],
1446 };
1447
1448 let padding =
1449 build_padding_slot(4, 1, nk, dom, ivk, &imt, &mut rng, Some(&precomputed)).unwrap();
1450
1451 let requested_nfs = imt.requested_nfs.borrow();
1452 assert_padding_slot_matches(
1453 &padding,
1454 4,
1455 nk,
1456 dom,
1457 ivk,
1458 selected_rho,
1459 selected_rseed,
1460 &imt_proof,
1461 &requested_nfs,
1462 );
1463 }
1464
1465 #[test]
1466 fn test_build_padding_slot_propagates_imt_errors() {
1467 let mut rng = OsRng;
1468 let sk = SpendingKey::random(&mut rng);
1469 let fvk: FullViewingKey = (&sk).into();
1470 let ak: SpendValidatingKey = fvk.clone().into();
1471 let imt = RecordingImtProvider::failing(ImtError("fixture failure".to_string()));
1472
1473 let result = build_padding_slot(
1474 2,
1475 0,
1476 fvk.nk().inner(),
1477 derive_nullifier_domain(pallas::Base::random(&mut rng)),
1478 external_ivk_scalar(&fvk, &ak),
1479 &imt,
1480 &mut rng,
1481 None,
1482 );
1483
1484 assert!(matches!(
1485 result,
1486 Err(DelegationBuildError::ImtFetchFailed(ImtError(message)))
1487 if message == "fixture failure"
1488 ));
1489 assert_eq!(imt.requested_nfs.borrow().len(), 1);
1490 }
1491
1492 #[test]
1493 fn test_build_padding_slot_rejects_missing_precomputed_padding_entry() {
1494 let mut rng = OsRng;
1495 let sk = SpendingKey::random(&mut rng);
1496 let fvk: FullViewingKey = (&sk).into();
1497 let ak: SpendValidatingKey = fvk.clone().into();
1498 let imt = RecordingImtProvider::returning(test_imt_proof());
1499 let precomputed = PrecomputedRandomness {
1500 padded_notes: vec![],
1501 rseed_signed: [0; 32],
1502 rseed_output: [0; 32],
1503 };
1504
1505 let result = build_padding_slot(
1506 1,
1507 0,
1508 fvk.nk().inner(),
1509 derive_nullifier_domain(pallas::Base::random(&mut rng)),
1510 external_ivk_scalar(&fvk, &ak),
1511 &imt,
1512 &mut rng,
1513 Some(&precomputed),
1514 );
1515
1516 assert!(matches!(
1517 result,
1518 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1519 index: 0,
1520 actual: 0
1521 })
1522 ));
1523 }
1524
1525 #[test]
1526 fn test_four_real_notes_builds_expected_output_shape() {
1527 build_bundle(
1529 &[3_200_000, 3_200_000, 3_200_000, 3_200_000],
1530 &[
1531 Scope::External,
1532 Scope::External,
1533 Scope::External,
1534 Scope::External,
1535 ],
1536 );
1537 }
1538
1539 #[test]
1540 fn test_two_real_notes_builds_expected_output_shape() {
1541 build_bundle(&[7_000_000, 7_000_000], &[Scope::External, Scope::External]);
1542 }
1543
1544 #[test]
1545 fn test_min_weight_boundary_builds_expected_output_shape() {
1546 build_bundle(&[12_500_000], &[Scope::External]);
1548 }
1549
1550 #[test]
1551 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1552 fn test_below_one_ballot() {
1553 let mut rng = OsRng;
1556 let sk = SpendingKey::random(&mut rng);
1557 let fvk: FullViewingKey = (&sk).into();
1558 let output_recipient = fvk.address_at(1u32, Scope::External);
1559 let vote_round_id = pallas::Base::random(&mut rng);
1560 let van_comm_rand = pallas::Base::random(&mut rng);
1561 let alpha = pallas::Scalar::random(&mut rng);
1562
1563 let imt = SpacedLeafImtProvider::new();
1564 let (inputs, nc_root) =
1565 make_real_note_inputs(&fvk, &[12_499_999], &[Scope::External], &imt, &mut rng);
1566
1567 let bundle = build_delegation_bundle(
1568 inputs,
1569 &fvk,
1570 alpha,
1571 output_recipient,
1572 vote_round_id,
1573 nc_root,
1574 van_comm_rand,
1575 &imt,
1576 &mut rng,
1577 None,
1578 )
1579 .unwrap();
1580
1581 let pi = bundle.instance.to_halo2_instance();
1582 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
1583 assert!(prover.verify().is_err(), "below one ballot should fail");
1584 }
1585
1586 #[test]
1587 fn test_three_ballots_builds_expected_output_shape() {
1588 build_bundle(
1590 &[12_500_000, 12_500_000, 12_500_000],
1591 &[Scope::External, Scope::External, Scope::External],
1592 );
1593 }
1594
1595 #[test]
1596 fn test_zero_notes_error() {
1597 let mut rng = OsRng;
1598 let sk = SpendingKey::random(&mut rng);
1599 let fvk: FullViewingKey = (&sk).into();
1600 let output_recipient = fvk.address_at(1u32, Scope::External);
1601 let imt = SpacedLeafImtProvider::new();
1602
1603 let result = build_delegation_bundle(
1604 vec![],
1605 &fvk,
1606 pallas::Scalar::random(&mut rng),
1607 output_recipient,
1608 pallas::Base::random(&mut rng),
1609 pallas::Base::random(&mut rng),
1610 pallas::Base::random(&mut rng),
1611 &imt,
1612 &mut rng,
1613 None,
1614 );
1615
1616 assert!(matches!(
1617 result,
1618 Err(DelegationBuildError::InvalidNoteCount(0))
1619 ));
1620 }
1621
1622 #[test]
1623 fn test_five_real_notes_builds_expected_output_shape() {
1624 build_bundle(
1626 &[2_500_000, 2_500_000, 2_500_000, 2_500_000, 2_500_000],
1627 &[
1628 Scope::External,
1629 Scope::External,
1630 Scope::External,
1631 Scope::External,
1632 Scope::External,
1633 ],
1634 );
1635 }
1636
1637 #[test]
1638 fn test_six_notes_error() {
1639 let mut rng = OsRng;
1640 let sk = SpendingKey::random(&mut rng);
1641 let fvk: FullViewingKey = (&sk).into();
1642 let output_recipient = fvk.address_at(1u32, Scope::External);
1643 let imt = SpacedLeafImtProvider::new();
1644
1645 let (inputs, _) = make_real_note_inputs(
1646 &fvk,
1647 &[3_000_000, 3_000_000, 3_000_000, 3_000_000, 3_000_000],
1648 &[
1649 Scope::External,
1650 Scope::External,
1651 Scope::External,
1652 Scope::External,
1653 Scope::External,
1654 ],
1655 &imt,
1656 &mut rng,
1657 );
1658 let mut inputs = inputs;
1660 let (extra, _) =
1661 make_real_note_inputs(&fvk, &[3_000_000], &[Scope::External], &imt, &mut rng);
1662 inputs.extend(extra);
1663
1664 let result = build_delegation_bundle(
1665 inputs,
1666 &fvk,
1667 pallas::Scalar::random(&mut rng),
1668 output_recipient,
1669 pallas::Base::random(&mut rng),
1670 pallas::Base::random(&mut rng),
1671 pallas::Base::random(&mut rng),
1672 &imt,
1673 &mut rng,
1674 None,
1675 );
1676
1677 assert!(matches!(
1678 result,
1679 Err(DelegationBuildError::InvalidNoteCount(6))
1680 ));
1681 }
1682
1683 #[test]
1684 fn test_missing_precomputed_padded_note_returns_error() {
1685 let precomputed = PrecomputedRandomness {
1686 padded_notes: vec![],
1687 rseed_signed: [0u8; 32],
1688 rseed_output: [0u8; 32],
1689 };
1690
1691 let result = build_single_note_bundle_with_precomputed(&precomputed);
1692
1693 assert!(matches!(
1694 result,
1695 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1696 index: 0,
1697 actual: 0
1698 })
1699 ));
1700 }
1701
1702 #[test]
1703 fn test_partial_precomputed_padded_notes_returns_later_missing_error() {
1704 let mut rng = OsRng;
1705 let sk = SpendingKey::random(&mut rng);
1706 let fvk: FullViewingKey = (&sk).into();
1707 let precomputed = PrecomputedRandomness {
1708 padded_notes: vec![
1709 make_valid_padded_note_data(&mut rng),
1710 make_valid_padded_note_data(&mut rng),
1711 ],
1712 rseed_signed: [0u8; 32],
1713 rseed_output: [0u8; 32],
1714 };
1715
1716 let result =
1717 build_single_note_bundle_with_fvk_and_precomputed(&fvk, &precomputed, &mut rng);
1718
1719 assert!(matches!(
1720 result,
1721 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1722 index: 2,
1723 actual: 2
1724 })
1725 ));
1726 }
1727
1728 #[test]
1729 fn test_invalid_precomputed_padded_rho_returns_error() {
1730 let precomputed = PrecomputedRandomness {
1731 padded_notes: vec![PaddedNoteData {
1732 rho: [0xffu8; 32],
1733 rseed: [0u8; 32],
1734 }],
1735 rseed_signed: [0u8; 32],
1736 rseed_output: [0u8; 32],
1737 };
1738
1739 let result = build_single_note_bundle_with_precomputed(&precomputed);
1740
1741 assert!(matches!(
1742 result,
1743 Err(DelegationBuildError::InvalidPrecomputedRho { index: 0 })
1744 ));
1745 }
1746
1747 #[test]
1748 fn test_single_internal_note_builds_expected_output_shape() {
1749 build_bundle(&[13_000_000], &[Scope::Internal]);
1750 }
1751
1752 #[test]
1753 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1754 fn test_mixed_scope_notes() {
1755 let bundle = build_bundle(
1756 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
1757 &[
1758 Scope::External,
1759 Scope::Internal,
1760 Scope::External,
1761 Scope::Internal,
1762 ],
1763 );
1764 verify_bundle(&bundle);
1765 }
1766
1767 #[test]
1768 fn test_all_internal_notes_builds_expected_output_shape() {
1769 build_bundle(
1770 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
1771 &[
1772 Scope::Internal,
1773 Scope::Internal,
1774 Scope::Internal,
1775 Scope::Internal,
1776 ],
1777 );
1778 }
1779}