1use std::{iter, vec::Vec};
10
11use ff::{Field, PrimeField, PrimeFieldBits};
12use group::{Curve, GroupEncoding};
13use halo2_proofs::circuit::Value;
14use orchard::{
15 constants::{
16 fixed_bases::{COMMIT_IVK_PERSONALIZATION, NOTE_COMMITMENT_PERSONALIZATION},
17 L_ORCHARD_BASE, L_VALUE,
18 },
19 keys::{FullViewingKey, Scope, SpendValidatingKey},
20 note::{commitment::ExtractedNoteCommitment, nullifier::Nullifier, Note, RandomSeed, Rho},
21 spec::NonIdentityPallasPoint,
22 tree::MerklePath,
23 value::NoteValue,
24};
25use pasta_curves::{
26 arithmetic::{CurveAffine, CurveExt},
27 pallas,
28};
29use rand::{CryptoRng, RngCore};
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::{
36 gadgets::elgamal::base_to_scalar, params::BALLOT_DIVISOR, protocol_hash::poseidon_hash_2,
37};
38
39const PADDING_PERSONALIZATION: &str = "shielded-vote/padding-v1";
47
48#[derive(Clone, Debug)]
50pub struct PaddedNoteData {
51 pub rho: [u8; 32],
53 pub rseed: [u8; 32],
55}
56
57#[derive(Clone, Debug)]
61pub struct PrecomputedRandomness {
62 pub padded_notes: Vec<PaddedNoteData>,
64 pub rseed_signed: [u8; 32],
66 pub rseed_output: [u8; 32],
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72#[allow(clippy::enum_variant_names)]
73pub enum PrecomputedRandomnessLocation {
74 PaddedNote(usize),
76 SignedNote,
78 OutputNote,
80}
81
82#[derive(Debug)]
84pub struct RealNoteInput {
85 pub note: Note,
87 pub fvk: FullViewingKey,
89 pub merkle_path: MerklePath,
91 pub imt_proof: ImtProofData,
96 pub scope: Scope,
98}
99
100#[derive(Debug)]
102pub struct DelegationBundle {
103 pub circuit: circuit::Circuit,
105 pub instance: circuit::Instance,
107}
108
109fn point_x(point: &pallas::Point) -> pallas::Base {
113 point_x_opt(point).expect("ExtractP requires a non-identity Pallas point")
114}
115
116fn point_x_opt(point: &pallas::Point) -> Option<pallas::Base> {
117 point
118 .to_affine()
119 .coordinates()
120 .into_option()
121 .map(|coords| *coords.x())
122}
123
124fn byte_bits(bytes: [u8; 32]) -> impl Iterator<Item = bool> {
128 bytes
129 .into_iter()
130 .flat_map(|byte| (0..8).map(move |bit| ((byte >> bit) & 1) == 1))
131}
132
133fn u64_bits(value: u64) -> impl Iterator<Item = bool> {
136 value
137 .to_le_bytes()
138 .into_iter()
139 .flat_map(|byte| (0..8).map(move |bit| ((byte >> bit) & 1) == 1))
140}
141
142fn external_ivk_scalar(fvk: &FullViewingKey, ak: &SpendValidatingKey) -> pallas::Scalar {
150 let ak_point: pallas::Point = ak.into();
151 let ak_x = point_x(&ak_point);
152 let rivk = fvk.rivk(Scope::External).inner();
153 let domain = sinsemilla::CommitDomain::new(COMMIT_IVK_PERSONALIZATION);
154 let ivk = domain
155 .short_commit(
156 iter::empty()
157 .chain(ak_x.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
158 .chain(
159 fvk.nk()
160 .inner()
161 .to_le_bits()
162 .iter()
163 .by_vals()
164 .take(L_ORCHARD_BASE),
165 ),
166 &rivk,
167 )
168 .expect("external ivk must not be bottom");
169 base_to_scalar(ivk).expect("external ivk must fit in the scalar field")
170}
171
172fn non_identity_padding_point(
181 point: pallas::Point,
182 slot_index: usize,
183 component: &'static str,
184) -> Result<NonIdentityPallasPoint, DelegationBuildError> {
185 NonIdentityPallasPoint::from_bytes(&point.to_bytes())
186 .into_option()
187 .ok_or(DelegationBuildError::InvalidPaddingPoint {
188 slot_index,
189 component,
190 })
191}
192
193fn validate_padding_slot_index(slot_index: usize) -> Result<(), DelegationBuildError> {
194 if (1..circuit::MAX_REAL_NOTES).contains(&slot_index) {
195 Ok(())
196 } else {
197 Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index })
198 }
199}
200
201fn padding_points(
209 slot_index: usize,
210 ivk: pallas::Scalar,
211) -> Result<(NonIdentityPallasPoint, NonIdentityPallasPoint), DelegationBuildError> {
212 validate_padding_slot_index(slot_index)?;
213 let slot_index_u32 =
214 u32::try_from(slot_index).expect("validated padding slot index fits in u32");
215 let g_d_pad =
216 pallas::Point::hash_to_curve(PADDING_PERSONALIZATION)(&slot_index_u32.to_le_bytes());
217 let pk_d_pad = g_d_pad * ivk;
218 Ok((
219 non_identity_padding_point(g_d_pad, slot_index, "g_d")?,
220 non_identity_padding_point(pk_d_pad, slot_index, "pk_d")?,
221 ))
222}
223
224fn random_seed_for_rho(rho: &Rho, rng: &mut impl RngCore) -> RandomSeed {
229 loop {
230 let mut rseed = [0u8; 32];
231 rng.fill_bytes(&mut rseed);
232 let rseed = RandomSeed::from_bytes(rseed, rho);
233 if bool::from(rseed.is_some()) {
234 return rseed.unwrap();
235 }
236 }
237}
238
239fn note_commitment_point(
247 g_d: pallas::Point,
248 pk_d: pallas::Point,
249 value: NoteValue,
250 rho: pallas::Base,
251 psi: pallas::Base,
252 rcm: pallas::Scalar,
253) -> Option<pallas::Point> {
254 let domain = sinsemilla::CommitDomain::new(NOTE_COMMITMENT_PERSONALIZATION);
255 domain
257 .commit(
258 iter::empty()
259 .chain(byte_bits(g_d.to_bytes()))
260 .chain(byte_bits(pk_d.to_bytes()))
261 .chain(u64_bits(value.inner()).take(L_VALUE))
262 .chain(rho.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
263 .chain(psi.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
264 &rcm,
265 )
266 .into_option()
267}
268
269fn derive_note_nullifier(
274 nk: pallas::Base,
275 rho: pallas::Base,
276 psi: pallas::Base,
277 cm: pallas::Affine,
278) -> Option<pallas::Base> {
279 let k = pallas::Point::hash_to_curve("z.cash:Orchard")(b"K");
280 let prf_nf = poseidon_hash_2(nk, rho);
281 let scalar = pallas::Scalar::from_repr((prf_nf + psi).to_repr())
283 .expect("Pallas base field is smaller than its scalar field");
284 point_x_opt(&(k * scalar + cm))
285}
286
287#[derive(Clone, Copy, Debug, PartialEq, Eq)]
296pub struct SyntheticPaddingNoteParts {
297 pub cmx: [u8; 32],
299 pub nullifier: [u8; 32],
302}
303
304struct SyntheticPaddingDerivation {
305 g_d_pad: NonIdentityPallasPoint,
306 pk_d_pad: NonIdentityPallasPoint,
307 psi: pallas::Base,
308 rcm: orchard::note::NoteCommitTrapdoor,
309 cm: pallas::Affine,
310 cmx: pallas::Base,
311 real_nf: pallas::Base,
312}
313
314fn derive_synthetic_padding_note(
317 nk: pallas::Base,
318 ivk: pallas::Scalar,
319 slot_index: usize,
320 rho: Rho,
321 rseed: RandomSeed,
322 location: PrecomputedRandomnessLocation,
323) -> Result<SyntheticPaddingDerivation, DelegationBuildError> {
324 let (g_d_pad, pk_d_pad) = padding_points(slot_index, ivk)?;
327 let psi = rseed.psi(&rho);
328 let rcm = rseed.rcm(&rho);
329
330 let cm = note_commitment_point(
333 *g_d_pad,
334 *pk_d_pad,
335 NoteValue::ZERO,
336 rho.into_inner(),
337 psi,
338 rcm.inner(),
339 )
340 .ok_or(DelegationBuildError::InvalidPaddingNoteCommitment { location })?
341 .to_affine();
342 let cmx = *cm
343 .coordinates()
344 .into_option()
345 .ok_or(DelegationBuildError::InvalidPaddingNoteCommitment { location })?
346 .x();
347 let real_nf = derive_note_nullifier(nk, rho.into_inner(), psi, cm)
348 .ok_or(DelegationBuildError::InvalidPaddingNullifier { location })?;
349
350 Ok(SyntheticPaddingDerivation {
351 g_d_pad,
352 pk_d_pad,
353 psi,
354 rcm,
355 cm,
356 cmx,
357 real_nf,
358 })
359}
360
361pub fn synthetic_padding_note_parts(
385 fvk: &FullViewingKey,
386 slot_index: usize,
387 rho: Rho,
388 rseed: RandomSeed,
389) -> Result<SyntheticPaddingNoteParts, DelegationBuildError> {
390 let ak: SpendValidatingKey = fvk.clone().into();
391 let ivk = external_ivk_scalar(fvk, &ak);
392 let location = PrecomputedRandomnessLocation::PaddedNote(slot_index);
393 let padding =
394 derive_synthetic_padding_note(fvk.nk().inner(), ivk, slot_index, rho, rseed, location)?;
395
396 Ok(SyntheticPaddingNoteParts {
397 cmx: padding.cmx.to_repr(),
398 nullifier: padding.real_nf.to_repr(),
399 })
400}
401
402struct PaddingSlot {
404 witness: NoteSlotWitness,
405 cmx: pallas::Base,
406 v_raw: u64,
407 gov_null: pallas::Base,
408 #[cfg(test)]
409 real_nf: pallas::Base,
410}
411
412fn build_padding_slot(
413 slot_index: usize,
414 pad_idx: usize,
415 nk: pallas::Base,
416 dom: pallas::Base,
417 ivk: pallas::Scalar,
418 imt_provider: &impl ImtProvider,
419 rng: &mut impl RngCore,
420 precomputed: Option<&PrecomputedRandomness>,
421) -> Result<PaddingSlot, DelegationBuildError> {
422 let location = PrecomputedRandomnessLocation::PaddedNote(pad_idx);
423
424 let (rho, rseed) = if let Some(pre) = precomputed {
425 if pad_idx >= pre.padded_notes.len() {
427 return Err(DelegationBuildError::MissingPrecomputedPaddedNote {
428 index: pad_idx,
429 actual: pre.padded_notes.len(),
430 });
431 }
432 let pd = &pre.padded_notes[pad_idx];
433 let rho = Rho::from_bytes(&pd.rho)
434 .into_option()
435 .ok_or(DelegationBuildError::InvalidPrecomputedRho { index: pad_idx })?;
436 let rseed = RandomSeed::from_bytes(pd.rseed, &rho)
437 .into_option()
438 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
439 (rho, rseed)
440 } else {
441 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
442 let rseed = random_seed_for_rho(&rho, &mut *rng);
443 (rho, rseed)
444 };
445
446 let padding = derive_synthetic_padding_note(nk, ivk, slot_index, rho, rseed, location)?;
447 let gov_null = gov_null_hash(nk, dom, padding.real_nf);
448 let imt_proof = imt_provider.non_membership_proof(padding.real_nf)?;
449
450 let merkle_path = MerklePath::dummy(&mut *rng);
453 let witness = NoteSlotWitness {
454 g_d: Value::known(padding.g_d_pad),
455 pk_d: Value::known(padding.pk_d_pad),
456 v: Value::known(NoteValue::ZERO),
457 rho: Value::known(rho.into_inner()),
458 psi: Value::known(padding.psi),
459 rcm: Value::known(padding.rcm),
460 cm: Value::known(padding.cm),
461 path: Value::known(merkle_path.auth_path()),
462 pos: Value::known(merkle_path.position()),
463 imt_nf_bounds: Value::known(imt_proof.nf_bounds),
464 imt_leaf_pos: Value::known(imt_proof.leaf_pos),
465 imt_path: Value::known(imt_proof.path),
466 is_internal: Value::known(false),
467 };
468
469 Ok(PaddingSlot {
470 witness,
471 cmx: padding.cmx,
472 v_raw: 0,
473 gov_null,
474 #[cfg(test)]
475 real_nf: padding.real_nf,
476 })
477}
478
479#[cfg(test)]
480pub(super) struct PaddingSlotForTesting {
481 pub witness: NoteSlotWitness,
482 pub cmx: pallas::Base,
483 pub gov_null: pallas::Base,
484 pub real_nf: pallas::Base,
485}
486
487#[cfg(test)]
488pub(super) fn build_padding_slot_for_testing(
489 slot_index: usize,
490 pad_idx: usize,
491 fvk: &FullViewingKey,
492 ak: &SpendValidatingKey,
493 dom: pallas::Base,
494 imt_provider: &impl ImtProvider,
495 rng: &mut impl RngCore,
496) -> Result<PaddingSlotForTesting, DelegationBuildError> {
497 let padding = build_padding_slot(
498 slot_index,
499 pad_idx,
500 fvk.nk().inner(),
501 dom,
502 external_ivk_scalar(fvk, ak),
503 imt_provider,
504 rng,
505 None,
506 )?;
507
508 Ok(PaddingSlotForTesting {
509 witness: padding.witness,
510 cmx: padding.cmx,
511 gov_null: padding.gov_null,
512 real_nf: padding.real_nf,
513 })
514}
515
516#[derive(Clone, Debug)]
518pub enum DelegationBuildError {
519 InvalidNoteCount(usize),
521 Instance(circuit::InstanceError),
523 InvalidPaddingSlotIndex { slot_index: usize },
525 InvalidPaddingPoint {
527 slot_index: usize,
528 component: &'static str,
529 },
530 InvalidPaddingNoteCommitment {
532 location: PrecomputedRandomnessLocation,
533 },
534 InvalidPaddingNullifier {
536 location: PrecomputedRandomnessLocation,
537 },
538 MissingPrecomputedPaddedNote { index: usize, actual: usize },
540 InvalidPrecomputedRho { index: usize },
542 InvalidPrecomputedRseed {
544 location: PrecomputedRandomnessLocation,
545 },
546 InvalidPrecomputedNote {
548 location: PrecomputedRandomnessLocation,
549 },
550 ImtFetchFailed(super::imt::ImtError),
552}
553
554impl From<circuit::InstanceError> for DelegationBuildError {
555 fn from(e: circuit::InstanceError) -> Self {
556 DelegationBuildError::Instance(e)
557 }
558}
559
560impl From<super::imt::ImtError> for DelegationBuildError {
561 fn from(e: super::imt::ImtError) -> Self {
562 DelegationBuildError::ImtFetchFailed(e)
563 }
564}
565
566impl std::fmt::Display for DelegationBuildError {
567 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568 match self {
569 DelegationBuildError::InvalidNoteCount(n) => {
570 write!(
571 f,
572 "invalid note count: {} (expected 1–{})",
573 n,
574 circuit::MAX_REAL_NOTES
575 )
576 }
577 DelegationBuildError::Instance(e) => {
578 write!(f, "instance construction failed: {e}")
579 }
580 DelegationBuildError::InvalidPaddingSlotIndex { slot_index } => {
581 write!(
582 f,
583 "invalid padding slot index {slot_index} (expected 1..={})",
584 circuit::MAX_REAL_NOTES - 1
585 )
586 }
587 DelegationBuildError::InvalidPaddingPoint {
588 slot_index,
589 component,
590 } => {
591 write!(
592 f,
593 "invalid padding point {component} at slot {slot_index}: identity point"
594 )
595 }
596 DelegationBuildError::InvalidPaddingNoteCommitment { location } => {
597 write!(f, "invalid padding note commitment for {location}")
598 }
599 DelegationBuildError::InvalidPaddingNullifier { location } => {
600 write!(f, "invalid padding nullifier for {location}")
601 }
602 DelegationBuildError::MissingPrecomputedPaddedNote { index, actual } => {
603 write!(
604 f,
605 "missing precomputed padded note at index {index} (got {actual} entries)"
606 )
607 }
608 DelegationBuildError::InvalidPrecomputedRho { index } => {
609 write!(f, "invalid precomputed padded note rho at index {index}")
610 }
611 DelegationBuildError::InvalidPrecomputedRseed { location } => {
612 write!(f, "invalid precomputed rseed for {location}")
613 }
614 DelegationBuildError::InvalidPrecomputedNote { location } => {
615 write!(f, "invalid precomputed note components for {location}")
616 }
617 DelegationBuildError::ImtFetchFailed(e) => {
618 write!(f, "IMT proof fetch failed: {e}")
619 }
620 }
621 }
622}
623
624impl std::fmt::Display for PrecomputedRandomnessLocation {
625 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
626 match self {
627 PrecomputedRandomnessLocation::PaddedNote(index) => {
628 write!(f, "padded note {index}")
629 }
630 PrecomputedRandomnessLocation::SignedNote => write!(f, "signed note"),
631 PrecomputedRandomnessLocation::OutputNote => write!(f, "output note"),
632 }
633 }
634}
635
636pub fn build_delegation_bundle(
671 real_notes: Vec<RealNoteInput>,
672 fvk: &FullViewingKey,
673 alpha: pallas::Scalar,
674 output_recipient: orchard::Address,
675 vote_round_id: pallas::Base,
676 nc_root: pallas::Base,
677 van_comm_rand: pallas::Base,
678 imt_provider: &impl ImtProvider,
679 rng: &mut (impl RngCore + CryptoRng),
680 precomputed: Option<&PrecomputedRandomness>,
681) -> Result<DelegationBundle, DelegationBuildError> {
682 let n_real = real_notes.len();
685 if n_real == 0 || n_real > circuit::MAX_REAL_NOTES {
686 return Err(DelegationBuildError::InvalidNoteCount(n_real));
687 }
688
689 let nf_imt_root = imt_provider.root();
691
692 let nk_val = fvk.nk().inner();
694 let ak: SpendValidatingKey = fvk.clone().into();
695 let ivk = external_ivk_scalar(fvk, &ak);
696
697 let dom = derive_nullifier_domain(vote_round_id);
699
700 let mut note_slots = Vec::with_capacity(circuit::MAX_REAL_NOTES);
702 let mut cmx_values = Vec::with_capacity(circuit::MAX_REAL_NOTES);
703 let mut v_values = Vec::with_capacity(circuit::MAX_REAL_NOTES);
704 let mut gov_nulls = Vec::with_capacity(circuit::MAX_REAL_NOTES);
705
706 for input in &real_notes {
709 let note = &input.note;
710 let rho = note.rho();
711 let psi = note.rseed().psi(&rho);
712 let rcm = note.rseed().rcm(&rho);
713 let cm = note.commitment();
714 let cmx = ExtractedNoteCommitment::from(cm.clone()).inner();
715 let v_raw = note.value().inner();
716 let recipient = note.recipient();
717
718 let real_nf = note.nullifier(fvk);
720 let gov_null = gov_null_hash(nk_val, dom, real_nf.inner());
722
723 let slot = NoteSlotWitness {
724 g_d: Value::known(recipient.g_d()),
725 pk_d: Value::known(recipient.pk_d().inner()),
726 v: Value::known(note.value()),
727 rho: Value::known(rho.into_inner()),
728 psi: Value::known(psi),
729 rcm: Value::known(rcm),
730 cm: Value::known(cm.inner().to_affine()),
731 path: Value::known(input.merkle_path.auth_path()),
732 pos: Value::known(input.merkle_path.position()),
733 imt_nf_bounds: Value::known(input.imt_proof.nf_bounds),
734 imt_leaf_pos: Value::known(input.imt_proof.leaf_pos),
735 imt_path: Value::known(input.imt_proof.path),
736 is_internal: Value::known(matches!(input.scope, Scope::Internal)),
737 };
738
739 note_slots.push(slot);
740 cmx_values.push(cmx);
741 v_values.push(v_raw);
742 gov_nulls.push(gov_null);
743 }
744
745 for i in n_real..circuit::MAX_REAL_NOTES {
749 let pad_idx = i - n_real; let padding =
751 build_padding_slot(i, pad_idx, nk_val, dom, ivk, imt_provider, rng, precomputed)?;
752
753 note_slots.push(padding.witness);
754 cmx_values.push(padding.cmx);
755 v_values.push(padding.v_raw);
756 gov_nulls.push(padding.gov_null);
757 }
758
759 let notes: [NoteSlotWitness; circuit::MAX_REAL_NOTES] =
760 note_slots.try_into().unwrap_or_else(|_| unreachable!());
761
762 let v_total_u64: u64 = v_values.iter().sum();
765 let num_ballots_u64 = v_total_u64 / BALLOT_DIVISOR;
766 let remainder_u64 = v_total_u64 % BALLOT_DIVISOR;
767 let num_ballots_field = pallas::Base::from(num_ballots_u64);
768
769 let g_d_new_x = *output_recipient
775 .g_d()
776 .to_affine()
777 .coordinates()
778 .unwrap()
779 .x();
780 let pk_d_new_x = *output_recipient
781 .pk_d()
782 .inner()
783 .to_affine()
784 .coordinates()
785 .unwrap()
786 .x();
787
788 let van_comm = van_commitment_hash(
789 g_d_new_x,
790 pk_d_new_x,
791 num_ballots_field,
792 vote_round_id,
793 van_comm_rand,
794 );
795
796 let rho = rho_binding_hash(
800 cmx_values[0],
801 cmx_values[1],
802 cmx_values[2],
803 cmx_values[3],
804 cmx_values[4],
805 van_comm,
806 vote_round_id,
807 );
808
809 let sender_address = fvk.address_at(0u32, Scope::External);
813 let signed_rho = Rho::from_nf_old(Nullifier::from_inner(rho));
814 let signed_note = if let Some(pre) = precomputed {
815 let location = PrecomputedRandomnessLocation::SignedNote;
816 let rseed = RandomSeed::from_bytes(pre.rseed_signed, &signed_rho)
817 .into_option()
818 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
819 Note::from_parts(sender_address, NoteValue::from_raw(1), signed_rho, rseed)
820 .into_option()
821 .ok_or(DelegationBuildError::InvalidPrecomputedNote { location })?
822 } else {
823 Note::new(
824 sender_address,
825 NoteValue::from_raw(1),
826 signed_rho,
827 &mut *rng,
828 )
829 };
830
831 let nf_signed = signed_note.nullifier(fvk);
833
834 let output_rho = Rho::from_nf_old(nf_signed);
837 let output_note = if let Some(pre) = precomputed {
838 let location = PrecomputedRandomnessLocation::OutputNote;
839 let rseed = RandomSeed::from_bytes(pre.rseed_output, &output_rho)
840 .into_option()
841 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
842 Note::from_parts(output_recipient, NoteValue::ZERO, output_rho, rseed)
843 .into_option()
844 .ok_or(DelegationBuildError::InvalidPrecomputedNote { location })?
845 } else {
846 Note::new(output_recipient, NoteValue::ZERO, output_rho, &mut *rng)
847 };
848 let cmx_new = ExtractedNoteCommitment::from(output_note.commitment()).inner();
849
850 let rk = ak.randomize(&alpha);
852
853 let circuit = circuit::Circuit::from_note_unchecked(fvk, &signed_note, alpha)
858 .with_output_note(&output_note)
859 .with_notes(notes)
860 .with_van_comm_rand(van_comm_rand)
861 .with_ballot_scaling(
862 pallas::Base::from(num_ballots_u64),
863 pallas::Base::from(remainder_u64),
864 );
865
866 let instance = circuit::Instance::from_parts(
867 nf_signed,
868 rk,
869 cmx_new,
870 van_comm,
871 vote_round_id,
872 nc_root,
873 nf_imt_root,
874 [
875 gov_nulls[0],
876 gov_nulls[1],
877 gov_nulls[2],
878 gov_nulls[3],
879 gov_nulls[4],
880 ],
881 dom,
882 )?;
883
884 Ok(DelegationBundle { circuit, instance })
885}
886
887#[cfg(test)]
892mod tests {
893 use super::*;
894 use crate::delegation::imt::{ImtError, SpacedLeafImtProvider};
895 use ff::Field;
896 use halo2_proofs::dev::MockProver;
897 use incrementalmerkletree::{Hashable, Level};
898 use orchard::{
899 constants::MERKLE_DEPTH_ORCHARD,
900 keys::{FullViewingKey, Scope, SpendingKey},
901 note::{commitment::ExtractedNoteCommitment, Note, Rho},
902 tree::{MerkleHashOrchard, MerklePath},
903 value::NoteValue,
904 };
905 use pasta_curves::pallas;
906 use rand::rngs::OsRng;
907 use std::cell::{Cell, RefCell};
908
909 const K: u32 = 14;
911
912 #[derive(Debug)]
913 struct RecordingImtProvider {
914 proof: ImtProofData,
915 error: Option<ImtError>,
916 requested_nfs: RefCell<Vec<pallas::Base>>,
917 }
918
919 impl RecordingImtProvider {
920 fn returning(proof: ImtProofData) -> Self {
921 Self {
922 proof,
923 error: None,
924 requested_nfs: RefCell::new(Vec::new()),
925 }
926 }
927
928 fn failing(error: ImtError) -> Self {
929 Self {
930 proof: test_imt_proof(),
931 error: Some(error),
932 requested_nfs: RefCell::new(Vec::new()),
933 }
934 }
935 }
936
937 impl ImtProvider for RecordingImtProvider {
938 fn root(&self) -> pallas::Base {
939 self.proof.root
940 }
941
942 fn non_membership_proof(&self, nf: pallas::Base) -> Result<ImtProofData, ImtError> {
943 self.requested_nfs.borrow_mut().push(nf);
944 match &self.error {
945 Some(error) => Err(error.clone()),
946 None => Ok(self.proof.clone()),
947 }
948 }
949 }
950
951 fn test_imt_proof() -> ImtProofData {
952 ImtProofData {
953 root: pallas::Base::from(900u64),
954 nf_bounds: [
955 pallas::Base::from(10u64),
956 pallas::Base::from(20u64),
957 pallas::Base::from(30u64),
958 ],
959 leaf_pos: 7,
960 path: std::array::from_fn(|i| pallas::Base::from(1_000u64 + i as u64)),
961 }
962 }
963
964 fn precomputed_padding_note(rng: &mut impl RngCore) -> (PaddedNoteData, Rho, RandomSeed) {
965 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
966 let rseed = random_seed_for_rho(&rho, &mut *rng);
967 (
968 PaddedNoteData {
969 rho: rho.to_bytes(),
970 rseed: *rseed.as_bytes(),
971 },
972 rho,
973 rseed,
974 )
975 }
976
977 fn assert_known<T>(value: &Value<T>, f: impl FnOnce(&T) -> bool) {
978 let checked = Cell::new(false);
979 value.assert_if_known(|actual| {
980 checked.set(true);
981 f(actual)
982 });
983 assert!(checked.get(), "expected known witness value");
984 }
985
986 fn assert_padding_slot_matches(
987 padding: &PaddingSlot,
988 slot_index: usize,
989 nk: pallas::Base,
990 dom: pallas::Base,
991 ivk: pallas::Scalar,
992 rho: Rho,
993 rseed: RandomSeed,
994 imt_proof: &ImtProofData,
995 requested_nfs: &[pallas::Base],
996 ) {
997 let (g_d_pad, pk_d_pad) =
998 padding_points(slot_index, ivk).expect("test padding points should be valid");
999 let psi = rseed.psi(&rho);
1000 let rcm = rseed.rcm(&rho);
1001 let cm = note_commitment_point(
1002 *g_d_pad,
1003 *pk_d_pad,
1004 NoteValue::ZERO,
1005 rho.into_inner(),
1006 psi,
1007 rcm.inner(),
1008 )
1009 .expect("test padding commitment should be valid")
1010 .to_affine();
1011 let real_nf = derive_note_nullifier(nk, rho.into_inner(), psi, cm)
1012 .expect("test padding nullifier should be valid");
1013
1014 assert_eq!(padding.cmx, *cm.coordinates().unwrap().x());
1015 assert_eq!(padding.v_raw, 0);
1016 assert_eq!(padding.gov_null, gov_null_hash(nk, dom, real_nf));
1017 assert_eq!(requested_nfs, &[real_nf]);
1018
1019 assert_known(&padding.witness.g_d, |actual| *actual == g_d_pad);
1020 assert_known(&padding.witness.pk_d, |actual| *actual == pk_d_pad);
1021 assert_known(&padding.witness.v, |actual| *actual == NoteValue::ZERO);
1022 assert_known(&padding.witness.rho, |actual| *actual == rho.into_inner());
1023 assert_known(&padding.witness.psi, |actual| *actual == psi);
1024 assert_known(&padding.witness.rcm, |actual| actual.inner() == rcm.inner());
1025 assert_known(&padding.witness.cm, |actual| *actual == cm);
1026 assert_known(&padding.witness.imt_nf_bounds, |actual| {
1027 *actual == imt_proof.nf_bounds
1028 });
1029 assert_known(&padding.witness.imt_leaf_pos, |actual| {
1030 *actual == imt_proof.leaf_pos
1031 });
1032 assert_known(&padding.witness.imt_path, |actual| {
1033 *actual == imt_proof.path
1034 });
1035 assert_known(&padding.witness.is_internal, |actual| !*actual);
1036 }
1037
1038 fn make_real_note_inputs(
1045 fvk: &FullViewingKey,
1046 values: &[u64],
1047 scopes: &[Scope],
1048 imt_provider: &impl ImtProvider,
1049 rng: &mut impl RngCore,
1050 ) -> (Vec<RealNoteInput>, pallas::Base) {
1051 let n = values.len();
1052 assert!((1..=circuit::MAX_REAL_NOTES).contains(&n));
1053 assert_eq!(n, scopes.len());
1054
1055 let mut notes = Vec::with_capacity(n);
1057 for (idx, &v) in values.iter().enumerate() {
1058 let recipient = fvk.address_at(0u32, scopes[idx]);
1059 let note_value = NoteValue::from_raw(v);
1060 let (_, _, dummy_parent) = Note::dummy(&mut *rng, None);
1061 let note = Note::new(
1062 recipient,
1063 note_value,
1064 Rho::from_nf_old(dummy_parent.nullifier(fvk)),
1065 &mut *rng,
1066 );
1067 notes.push(note);
1068 }
1069
1070 let empty_leaf = MerkleHashOrchard::empty_leaf();
1072 let mut leaves = [empty_leaf; 8];
1073 for (i, note) in notes.iter().enumerate() {
1074 let cmx = ExtractedNoteCommitment::from(note.commitment());
1075 leaves[i] = MerkleHashOrchard::from_cmx(&cmx);
1076 }
1077
1078 let l1_0 = MerkleHashOrchard::combine(Level::from(0), &leaves[0], &leaves[1]);
1080 let l1_1 = MerkleHashOrchard::combine(Level::from(0), &leaves[2], &leaves[3]);
1081 let l1_2 = MerkleHashOrchard::combine(Level::from(0), &leaves[4], &leaves[5]);
1082 let l1_3 = MerkleHashOrchard::combine(Level::from(0), &leaves[6], &leaves[7]);
1083 let l2_0 = MerkleHashOrchard::combine(Level::from(1), &l1_0, &l1_1);
1084 let l2_1 = MerkleHashOrchard::combine(Level::from(1), &l1_2, &l1_3);
1085 let l3_0 = MerkleHashOrchard::combine(Level::from(2), &l2_0, &l2_1);
1086
1087 let mut current = l3_0;
1089 for level in 3..MERKLE_DEPTH_ORCHARD {
1090 let sibling = MerkleHashOrchard::empty_root(Level::from(level as u8));
1091 current = MerkleHashOrchard::combine(Level::from(level as u8), ¤t, &sibling);
1092 }
1093 let nc_root = current.inner();
1094
1095 let l1 = [l1_0, l1_1, l1_2, l1_3];
1097 let l2 = [l2_0, l2_1];
1098 let mut inputs = Vec::with_capacity(n);
1099 for (i, note) in notes.into_iter().enumerate() {
1100 let mut auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
1101 auth_path[0] = leaves[i ^ 1];
1102 auth_path[1] = l1[(i >> 1) ^ 1];
1103 auth_path[2] = l2[1 - (i >> 2)];
1104 for level in 3..MERKLE_DEPTH_ORCHARD {
1105 auth_path[level] = MerkleHashOrchard::empty_root(Level::from(level as u8));
1106 }
1107 let merkle_path = MerklePath::from_parts(i as u32, auth_path);
1108
1109 let real_nf = note.nullifier(fvk);
1110 let imt_proof = imt_provider.non_membership_proof(real_nf.inner()).unwrap();
1111
1112 inputs.push(RealNoteInput {
1113 note,
1114 fvk: fvk.clone(),
1115 merkle_path,
1116 imt_proof,
1117 scope: scopes[i],
1118 });
1119 }
1120
1121 (inputs, nc_root)
1122 }
1123
1124 fn build_bundle(values: &[u64], scopes: &[Scope]) -> DelegationBundle {
1126 assert_eq!(values.len(), scopes.len());
1127 let mut rng = OsRng;
1128 let sk = SpendingKey::random(&mut rng);
1129 let fvk: FullViewingKey = (&sk).into();
1130 let output_recipient = fvk.address_at(1u32, Scope::External);
1131 let vote_round_id = pallas::Base::random(&mut rng);
1132 let van_comm_rand = pallas::Base::random(&mut rng);
1133 let alpha = pallas::Scalar::random(&mut rng);
1134
1135 let imt = SpacedLeafImtProvider::new();
1136 let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
1137
1138 let bundle = build_delegation_bundle(
1139 inputs,
1140 &fvk,
1141 alpha,
1142 output_recipient,
1143 vote_round_id,
1144 nc_root,
1145 van_comm_rand,
1146 &imt,
1147 &mut rng,
1148 None,
1149 )
1150 .unwrap();
1151
1152 assert_delegation_output_shape(&bundle);
1153 bundle
1154 }
1155
1156 fn build_single_note_bundle_with_precomputed(
1157 precomputed: &PrecomputedRandomness,
1158 ) -> Result<DelegationBundle, DelegationBuildError> {
1159 let mut rng = OsRng;
1160 let sk = SpendingKey::random(&mut rng);
1161 let fvk: FullViewingKey = (&sk).into();
1162
1163 build_single_note_bundle_with_fvk_and_precomputed(&fvk, precomputed, &mut rng)
1164 }
1165
1166 fn build_single_note_bundle_with_fvk_and_precomputed(
1167 fvk: &FullViewingKey,
1168 precomputed: &PrecomputedRandomness,
1169 rng: &mut (impl RngCore + CryptoRng),
1170 ) -> Result<DelegationBundle, DelegationBuildError> {
1171 let output_recipient = fvk.address_at(1u32, Scope::External);
1172 let vote_round_id = pallas::Base::random(&mut *rng);
1173 let van_comm_rand = pallas::Base::random(&mut *rng);
1174 let alpha = pallas::Scalar::random(&mut *rng);
1175
1176 let imt = SpacedLeafImtProvider::new();
1177 let (inputs, nc_root) =
1178 make_real_note_inputs(fvk, &[13_000_000], &[Scope::External], &imt, &mut *rng);
1179
1180 build_delegation_bundle(
1181 inputs,
1182 fvk,
1183 alpha,
1184 output_recipient,
1185 vote_round_id,
1186 nc_root,
1187 van_comm_rand,
1188 &imt,
1189 rng,
1190 Some(precomputed),
1191 )
1192 }
1193
1194 fn make_valid_padded_note_data(rng: &mut impl RngCore) -> PaddedNoteData {
1195 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
1196 let rseed = random_seed_for_rho(&rho, rng);
1197
1198 PaddedNoteData {
1199 rho: rho.to_bytes(),
1200 rseed: *rseed.as_bytes(),
1201 }
1202 }
1203
1204 fn assert_delegation_output_shape(bundle: &DelegationBundle) {
1205 let pi = bundle.instance.to_halo2_instance();
1206 assert_eq!(pi.len(), 14, "delegation public input shape changed");
1207 assert_eq!(bundle.instance.gov_null.len(), 5);
1208 assert_eq!(pi[0], bundle.instance.nf_signed.inner());
1209 assert_eq!(pi[3], bundle.instance.cmx_new);
1210 assert_eq!(pi[4], bundle.instance.van_comm);
1211 assert_eq!(pi[5], bundle.instance.vote_round_id);
1212 assert_eq!(pi[6], bundle.instance.nc_root);
1213 assert_eq!(pi[7], bundle.instance.nf_imt_root);
1214 assert_eq!(&pi[8..13], &bundle.instance.gov_null);
1215 assert_eq!(pi[13], bundle.instance.dom);
1216 }
1217
1218 fn verify_bundle(bundle: &DelegationBundle) {
1219 let pi = bundle.instance.to_halo2_instance();
1221 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
1222 assert_eq!(prover.verify(), Ok(()), "merged circuit failed");
1223 }
1224
1225 #[test]
1226 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1227 fn test_single_real_note() {
1228 let bundle = build_bundle(&[13_000_000], &[Scope::External]);
1229 verify_bundle(&bundle);
1230 }
1231
1232 fn build_bundle_for_inspection(
1236 values: &[u64],
1237 scopes: &[Scope],
1238 ) -> (DelegationBundle, FullViewingKey, SpendValidatingKey) {
1239 let mut rng = OsRng;
1240 let sk = SpendingKey::random(&mut rng);
1241 let fvk: FullViewingKey = (&sk).into();
1242 let ak: SpendValidatingKey = fvk.clone().into();
1243 let output_recipient = fvk.address_at(1u32, Scope::External);
1244 let vote_round_id = pallas::Base::random(&mut rng);
1245 let van_comm_rand = pallas::Base::random(&mut rng);
1246 let alpha = pallas::Scalar::random(&mut rng);
1247 let imt = SpacedLeafImtProvider::new();
1248 let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
1249
1250 let bundle = build_delegation_bundle(
1251 inputs,
1252 &fvk,
1253 alpha,
1254 output_recipient,
1255 vote_round_id,
1256 nc_root,
1257 van_comm_rand,
1258 &imt,
1259 &mut rng,
1260 None,
1261 )
1262 .unwrap();
1263 (bundle, fvk, ak)
1264 }
1265
1266 #[test]
1267 fn test_single_real_note_locks_padding_witnesses() {
1268 let (bundle, fvk, ak) = build_bundle_for_inspection(&[13_000_000], &[Scope::External]);
1274 let ivk = external_ivk_scalar(&fvk, &ak);
1275 let notes = bundle.circuit.notes_for_testing();
1276 for slot_index in 1..5 {
1277 let (expected_g_d, expected_pk_d) =
1278 padding_points(slot_index, ivk).expect("test padding points should be valid");
1279 assert_known(¬es[slot_index].g_d, |actual| *actual == expected_g_d);
1280 assert_known(¬es[slot_index].pk_d, |actual| *actual == expected_pk_d);
1281 }
1282 }
1283
1284 #[test]
1285 fn test_five_real_notes_uses_no_padding() {
1286 let (bundle, fvk, ak) = build_bundle_for_inspection(&[2_500_000; 5], &[Scope::External; 5]);
1293 let ivk = external_ivk_scalar(&fvk, &ak);
1294 let padding_g_ds: Vec<_> = (1..circuit::MAX_REAL_NOTES)
1295 .map(|i| {
1296 padding_points(i, ivk)
1297 .expect("test padding points should be valid")
1298 .0
1299 })
1300 .collect();
1301 let notes = bundle.circuit.notes_for_testing();
1302 for slot_index in 0..5 {
1303 for pad_g_d in &padding_g_ds {
1304 assert_known(¬es[slot_index].g_d, |actual| actual != pad_g_d);
1305 }
1306 }
1307 }
1308
1309 #[test]
1310 fn test_padding_points_are_synthetic_and_ivk_bound() {
1311 let mut rng = OsRng;
1312 let sk = SpendingKey::random(&mut rng);
1313 let fvk: FullViewingKey = (&sk).into();
1314 let ak: SpendValidatingKey = fvk.clone().into();
1315 let ivk = external_ivk_scalar(&fvk, &ak);
1316
1317 for slot_index in 1..5 {
1318 let (g_d_pad, pk_d_pad) =
1319 padding_points(slot_index, ivk).expect("test padding points should be valid");
1320 let real_orchard_addr = fvk.address_at(slot_index as u32, Scope::External);
1321
1322 assert_eq!(*pk_d_pad, *g_d_pad * ivk);
1326 assert_ne!(*g_d_pad, *real_orchard_addr.g_d());
1327 assert_ne!(*pk_d_pad, *real_orchard_addr.pk_d().inner());
1328 }
1329 }
1330
1331 #[test]
1332 fn test_padding_points_reject_impossible_slot_indices() {
1333 let mut rng = OsRng;
1334 let sk = SpendingKey::random(&mut rng);
1335 let fvk: FullViewingKey = (&sk).into();
1336 let ak: SpendValidatingKey = fvk.clone().into();
1337 let ivk = external_ivk_scalar(&fvk, &ak);
1338
1339 for slot_index in [0, circuit::MAX_REAL_NOTES, usize::MAX] {
1340 assert!(matches!(
1341 padding_points(slot_index, ivk),
1342 Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index: actual })
1343 if actual == slot_index
1344 ));
1345 }
1346 }
1347
1348 #[test]
1363 fn test_padding_personalization_is_domain_separated_from_orchard() {
1364 use orchard::constants::KEY_DIVERSIFICATION_PERSONALIZATION;
1365
1366 assert_ne!(
1367 PADDING_PERSONALIZATION, KEY_DIVERSIFICATION_PERSONALIZATION,
1368 "padding personalization must be domain-separated from Orchard's \
1369 DiversifyHash personalization; otherwise synthetic padding `g_d_pad` \
1370 can collide with real diversified bases and ZCA-450's fix regresses"
1371 );
1372 }
1373
1374 fn fixture_real_note(
1388 scope: Scope,
1389 rng: &mut impl RngCore,
1390 ) -> (FullViewingKey, SpendValidatingKey, Note) {
1391 let sk = SpendingKey::random(rng);
1392 let fvk: FullViewingKey = (&sk).into();
1393 let ak: SpendValidatingKey = fvk.clone().into();
1394 let recipient = fvk.address_at(0u32, scope);
1395 let (_, _, dummy_parent) = Note::dummy(rng, None);
1396 let note = Note::new(
1397 recipient,
1398 NoteValue::from_raw(12_500_000),
1399 Rho::from_nf_old(dummy_parent.nullifier(&fvk)),
1400 rng,
1401 );
1402 (fvk, ak, note)
1403 }
1404
1405 #[test]
1406 fn test_note_commitment_point_matches_orchard() {
1407 let mut rng = OsRng;
1412 for scope in [Scope::External, Scope::Internal] {
1413 let (_fvk, _ak, note) = fixture_real_note(scope, &mut rng);
1414 let recipient = note.recipient();
1415 let rho = note.rho();
1416 let psi = note.rseed().psi(&rho);
1417 let rcm = note.rseed().rcm(&rho);
1418
1419 let mirrored = note_commitment_point(
1420 *recipient.g_d(),
1421 *recipient.pk_d().inner(),
1422 note.value(),
1423 rho.into_inner(),
1424 psi,
1425 rcm.inner(),
1426 );
1427 let orchard = note.commitment().inner();
1428
1429 assert_eq!(
1430 mirrored,
1431 Some(orchard),
1432 "note_commitment_point drifted from Orchard NoteCommitment::derive ({scope:?})"
1433 );
1434 }
1435 }
1436
1437 #[test]
1438 fn test_derive_note_nullifier_matches_orchard() {
1439 let mut rng = OsRng;
1444 for scope in [Scope::External, Scope::Internal] {
1445 let (fvk, _ak, note) = fixture_real_note(scope, &mut rng);
1446 let nk = fvk.nk().inner();
1447 let rho = note.rho();
1448 let psi = note.rseed().psi(&rho);
1449 let cm = note.commitment().inner().to_affine();
1450
1451 let mirrored = derive_note_nullifier(nk, rho.into_inner(), psi, cm);
1452 let orchard = note.nullifier(&fvk).inner();
1453
1454 assert_eq!(
1455 mirrored,
1456 Some(orchard),
1457 "derive_note_nullifier drifted from Orchard Nullifier::derive ({scope:?})"
1458 );
1459 }
1460 }
1461
1462 #[test]
1463 fn test_external_ivk_scalar_matches_orchard_address_derivation() {
1464 let mut rng = OsRng;
1471 let sk = SpendingKey::random(&mut rng);
1472 let fvk: FullViewingKey = (&sk).into();
1473 let ak: SpendValidatingKey = fvk.clone().into();
1474 let ivk = external_ivk_scalar(&fvk, &ak);
1475
1476 for idx in [0u32, 1, 7, 1234] {
1479 let addr = fvk.address_at(idx, Scope::External);
1480 assert_eq!(
1481 *addr.g_d() * ivk,
1482 *addr.pk_d().inner(),
1483 "external_ivk_scalar drifted: [ivk] * g_d != pk_d at diversifier index {idx}"
1484 );
1485 }
1486
1487 let internal_addr = fvk.address_at(0u32, Scope::Internal);
1491 assert_ne!(
1492 *internal_addr.g_d() * ivk,
1493 *internal_addr.pk_d().inner(),
1494 "external_ivk_scalar incorrectly validates an internal-scope address"
1495 );
1496 }
1497
1498 #[test]
1499 fn test_build_padding_slot_fresh_randomness_populates_strict_witnesses() {
1500 let mut rng = OsRng;
1501 let sk = SpendingKey::random(&mut rng);
1502 let fvk: FullViewingKey = (&sk).into();
1503 let ak: SpendValidatingKey = fvk.clone().into();
1504 let nk = fvk.nk().inner();
1505 let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
1506 let ivk = external_ivk_scalar(&fvk, &ak);
1507 let imt_proof = test_imt_proof();
1508 let imt = RecordingImtProvider::returning(imt_proof.clone());
1509
1510 let padding = build_padding_slot(3, 0, nk, dom, ivk, &imt, &mut rng, None).unwrap();
1511
1512 let (g_d_pad, pk_d_pad) =
1513 padding_points(3, ivk).expect("test padding points should be valid");
1514 assert_known(&padding.witness.g_d, |actual| *actual == g_d_pad);
1515 assert_known(&padding.witness.pk_d, |actual| *actual == pk_d_pad);
1516 let generated_padding_values = padding
1517 .witness
1518 .rho
1519 .as_ref()
1520 .copied()
1521 .zip(padding.witness.psi.as_ref().copied())
1522 .zip(padding.witness.rcm.as_ref().cloned())
1523 .zip(padding.witness.cm.as_ref().copied());
1524 assert_known(
1525 &generated_padding_values,
1526 |(((rho_inner, psi), rcm), cm_witness)| {
1527 let cm = note_commitment_point(
1528 *g_d_pad,
1529 *pk_d_pad,
1530 NoteValue::ZERO,
1531 *rho_inner,
1532 *psi,
1533 rcm.inner(),
1534 )
1535 .expect("test padding commitment should be valid")
1536 .to_affine();
1537 let real_nf = derive_note_nullifier(nk, *rho_inner, *psi, cm)
1538 .expect("test padding nullifier should be valid");
1539
1540 *cm_witness == cm
1541 && padding.cmx == *cm.coordinates().unwrap().x()
1542 && padding.gov_null == gov_null_hash(nk, dom, real_nf)
1543 && imt.requested_nfs.borrow().as_slice() == [real_nf]
1544 },
1545 );
1546 assert_known(&padding.witness.v, |actual| *actual == NoteValue::ZERO);
1547 assert_known(&padding.witness.is_internal, |actual| !*actual);
1548 assert_known(&padding.witness.imt_nf_bounds, |actual| {
1549 *actual == imt_proof.nf_bounds
1550 });
1551 assert_known(&padding.witness.imt_leaf_pos, |actual| {
1552 *actual == imt_proof.leaf_pos
1553 });
1554 assert_known(&padding.witness.imt_path, |actual| {
1555 *actual == imt_proof.path
1556 });
1557 assert_eq!(padding.v_raw, 0);
1558 assert_eq!(imt.requested_nfs.borrow().len(), 1);
1559 }
1560
1561 #[test]
1562 fn test_build_padding_slot_reuses_selected_precomputed_randomness() {
1563 let mut rng = OsRng;
1564 let sk = SpendingKey::random(&mut rng);
1565 let fvk: FullViewingKey = (&sk).into();
1566 let ak: SpendValidatingKey = fvk.clone().into();
1567 let nk = fvk.nk().inner();
1568 let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
1569 let ivk = external_ivk_scalar(&fvk, &ak);
1570 let imt_proof = test_imt_proof();
1571 let imt = RecordingImtProvider::returning(imt_proof.clone());
1572
1573 let (unused_pd, _, _) = precomputed_padding_note(&mut rng);
1574 let (selected_pd, selected_rho, selected_rseed) = precomputed_padding_note(&mut rng);
1575 let precomputed = PrecomputedRandomness {
1576 padded_notes: vec![unused_pd, selected_pd],
1577 rseed_signed: [0; 32],
1578 rseed_output: [0; 32],
1579 };
1580
1581 let padding =
1582 build_padding_slot(4, 1, nk, dom, ivk, &imt, &mut rng, Some(&precomputed)).unwrap();
1583
1584 let requested_nfs = imt.requested_nfs.borrow();
1585 assert_padding_slot_matches(
1586 &padding,
1587 4,
1588 nk,
1589 dom,
1590 ivk,
1591 selected_rho,
1592 selected_rseed,
1593 &imt_proof,
1594 &requested_nfs,
1595 );
1596 }
1597
1598 #[test]
1599 fn test_derive_synthetic_padding_note_matches_manual_derivation() {
1600 let mut rng = OsRng;
1601 let sk = SpendingKey::random(&mut rng);
1602 let fvk: FullViewingKey = (&sk).into();
1603 let ak: SpendValidatingKey = fvk.clone().into();
1604 let nk = fvk.nk().inner();
1605 let ivk = external_ivk_scalar(&fvk, &ak);
1606 let slot_index = 3;
1607 let (_padded_note, rho, rseed) = precomputed_padding_note(&mut rng);
1608
1609 let derived = derive_synthetic_padding_note(
1610 nk,
1611 ivk,
1612 slot_index,
1613 rho,
1614 rseed,
1615 PrecomputedRandomnessLocation::PaddedNote(0),
1616 )
1617 .unwrap();
1618
1619 let (expected_g_d, expected_pk_d) =
1620 padding_points(slot_index, ivk).expect("test padding points should be valid");
1621 let expected_psi = rseed.psi(&rho);
1622 let expected_rcm = rseed.rcm(&rho);
1623 let expected_cm = note_commitment_point(
1624 *expected_g_d,
1625 *expected_pk_d,
1626 NoteValue::ZERO,
1627 rho.into_inner(),
1628 expected_psi,
1629 expected_rcm.inner(),
1630 )
1631 .expect("test padding commitment should be valid")
1632 .to_affine();
1633 let expected_nf = derive_note_nullifier(nk, rho.into_inner(), expected_psi, expected_cm)
1634 .expect("test padding nullifier should be valid");
1635
1636 assert_eq!(derived.g_d_pad, expected_g_d);
1637 assert_eq!(derived.pk_d_pad, expected_pk_d);
1638 assert_eq!(derived.psi, expected_psi);
1639 assert_eq!(derived.rcm.inner(), expected_rcm.inner());
1640 assert_eq!(derived.cm, expected_cm);
1641 assert_eq!(derived.cmx, *expected_cm.coordinates().unwrap().x());
1642 assert_eq!(derived.real_nf, expected_nf);
1643 }
1644
1645 #[test]
1646 fn test_synthetic_padding_note_parts_matches_padding_slot_derivation() {
1647 let mut rng = OsRng;
1648 let sk = SpendingKey::random(&mut rng);
1649 let fvk: FullViewingKey = (&sk).into();
1650 let ak: SpendValidatingKey = fvk.clone().into();
1651 let nk = fvk.nk().inner();
1652 let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
1653 let ivk = external_ivk_scalar(&fvk, &ak);
1654 let imt = RecordingImtProvider::returning(test_imt_proof());
1655
1656 let (padded_note, rho, rseed) = precomputed_padding_note(&mut rng);
1657 let precomputed = PrecomputedRandomness {
1658 padded_notes: vec![padded_note],
1659 rseed_signed: [0; 32],
1660 rseed_output: [0; 32],
1661 };
1662
1663 let padding =
1664 build_padding_slot(3, 0, nk, dom, ivk, &imt, &mut rng, Some(&precomputed)).unwrap();
1665 let parts = crate::delegation::synthetic_padding_note_parts(&fvk, 3, rho, rseed).unwrap();
1666
1667 assert_eq!(
1668 parts,
1669 SyntheticPaddingNoteParts {
1670 cmx: padding.cmx.to_repr(),
1671 nullifier: padding.real_nf.to_repr(),
1672 }
1673 );
1674 assert_eq!(imt.requested_nfs.borrow().as_slice(), &[padding.real_nf]);
1675 }
1676
1677 #[test]
1678 fn test_synthetic_padding_note_parts_rejects_impossible_slot_indices() {
1679 let mut rng = OsRng;
1680 let sk = SpendingKey::random(&mut rng);
1681 let fvk: FullViewingKey = (&sk).into();
1682 let (_padded_note, rho, rseed) = precomputed_padding_note(&mut rng);
1683
1684 for slot_index in [0, circuit::MAX_REAL_NOTES, usize::MAX] {
1685 assert!(matches!(
1686 crate::delegation::synthetic_padding_note_parts(&fvk, slot_index, rho, rseed),
1687 Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index: actual })
1688 if actual == slot_index
1689 ));
1690 }
1691 }
1692
1693 #[test]
1694 fn test_build_padding_slot_propagates_imt_errors() {
1695 let mut rng = OsRng;
1696 let sk = SpendingKey::random(&mut rng);
1697 let fvk: FullViewingKey = (&sk).into();
1698 let ak: SpendValidatingKey = fvk.clone().into();
1699 let imt = RecordingImtProvider::failing(ImtError("fixture failure".to_string()));
1700
1701 let result = build_padding_slot(
1702 2,
1703 0,
1704 fvk.nk().inner(),
1705 derive_nullifier_domain(pallas::Base::random(&mut rng)),
1706 external_ivk_scalar(&fvk, &ak),
1707 &imt,
1708 &mut rng,
1709 None,
1710 );
1711
1712 assert!(matches!(
1713 result,
1714 Err(DelegationBuildError::ImtFetchFailed(ImtError(message)))
1715 if message == "fixture failure"
1716 ));
1717 assert_eq!(imt.requested_nfs.borrow().len(), 1);
1718 }
1719
1720 #[test]
1721 fn test_build_padding_slot_rejects_missing_precomputed_padding_entry() {
1722 let mut rng = OsRng;
1723 let sk = SpendingKey::random(&mut rng);
1724 let fvk: FullViewingKey = (&sk).into();
1725 let ak: SpendValidatingKey = fvk.clone().into();
1726 let imt = RecordingImtProvider::returning(test_imt_proof());
1727 let precomputed = PrecomputedRandomness {
1728 padded_notes: vec![],
1729 rseed_signed: [0; 32],
1730 rseed_output: [0; 32],
1731 };
1732
1733 let result = build_padding_slot(
1734 1,
1735 0,
1736 fvk.nk().inner(),
1737 derive_nullifier_domain(pallas::Base::random(&mut rng)),
1738 external_ivk_scalar(&fvk, &ak),
1739 &imt,
1740 &mut rng,
1741 Some(&precomputed),
1742 );
1743
1744 assert!(matches!(
1745 result,
1746 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1747 index: 0,
1748 actual: 0
1749 })
1750 ));
1751 }
1752
1753 #[test]
1754 fn test_four_real_notes_builds_expected_output_shape() {
1755 build_bundle(
1757 &[3_200_000, 3_200_000, 3_200_000, 3_200_000],
1758 &[
1759 Scope::External,
1760 Scope::External,
1761 Scope::External,
1762 Scope::External,
1763 ],
1764 );
1765 }
1766
1767 #[test]
1768 fn test_two_real_notes_builds_expected_output_shape() {
1769 build_bundle(&[7_000_000, 7_000_000], &[Scope::External, Scope::External]);
1770 }
1771
1772 #[test]
1773 fn test_min_weight_boundary_builds_expected_output_shape() {
1774 build_bundle(&[12_500_000], &[Scope::External]);
1776 }
1777
1778 #[test]
1779 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1780 fn test_below_one_ballot() {
1781 let mut rng = OsRng;
1784 let sk = SpendingKey::random(&mut rng);
1785 let fvk: FullViewingKey = (&sk).into();
1786 let output_recipient = fvk.address_at(1u32, Scope::External);
1787 let vote_round_id = pallas::Base::random(&mut rng);
1788 let van_comm_rand = pallas::Base::random(&mut rng);
1789 let alpha = pallas::Scalar::random(&mut rng);
1790
1791 let imt = SpacedLeafImtProvider::new();
1792 let (inputs, nc_root) =
1793 make_real_note_inputs(&fvk, &[12_499_999], &[Scope::External], &imt, &mut rng);
1794
1795 let bundle = build_delegation_bundle(
1796 inputs,
1797 &fvk,
1798 alpha,
1799 output_recipient,
1800 vote_round_id,
1801 nc_root,
1802 van_comm_rand,
1803 &imt,
1804 &mut rng,
1805 None,
1806 )
1807 .unwrap();
1808
1809 let pi = bundle.instance.to_halo2_instance();
1810 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
1811 assert!(prover.verify().is_err(), "below one ballot should fail");
1812 }
1813
1814 #[test]
1815 fn test_three_ballots_builds_expected_output_shape() {
1816 build_bundle(
1818 &[12_500_000, 12_500_000, 12_500_000],
1819 &[Scope::External, Scope::External, Scope::External],
1820 );
1821 }
1822
1823 #[test]
1824 fn test_zero_notes_error() {
1825 let mut rng = OsRng;
1826 let sk = SpendingKey::random(&mut rng);
1827 let fvk: FullViewingKey = (&sk).into();
1828 let output_recipient = fvk.address_at(1u32, Scope::External);
1829 let imt = SpacedLeafImtProvider::new();
1830
1831 let result = build_delegation_bundle(
1832 vec![],
1833 &fvk,
1834 pallas::Scalar::random(&mut rng),
1835 output_recipient,
1836 pallas::Base::random(&mut rng),
1837 pallas::Base::random(&mut rng),
1838 pallas::Base::random(&mut rng),
1839 &imt,
1840 &mut rng,
1841 None,
1842 );
1843
1844 assert!(matches!(
1845 result,
1846 Err(DelegationBuildError::InvalidNoteCount(0))
1847 ));
1848 }
1849
1850 #[test]
1851 fn test_five_real_notes_builds_expected_output_shape() {
1852 build_bundle(
1854 &[2_500_000, 2_500_000, 2_500_000, 2_500_000, 2_500_000],
1855 &[
1856 Scope::External,
1857 Scope::External,
1858 Scope::External,
1859 Scope::External,
1860 Scope::External,
1861 ],
1862 );
1863 }
1864
1865 #[test]
1866 fn test_six_notes_error() {
1867 let mut rng = OsRng;
1868 let sk = SpendingKey::random(&mut rng);
1869 let fvk: FullViewingKey = (&sk).into();
1870 let output_recipient = fvk.address_at(1u32, Scope::External);
1871 let imt = SpacedLeafImtProvider::new();
1872
1873 let (inputs, _) = make_real_note_inputs(
1874 &fvk,
1875 &[3_000_000, 3_000_000, 3_000_000, 3_000_000, 3_000_000],
1876 &[
1877 Scope::External,
1878 Scope::External,
1879 Scope::External,
1880 Scope::External,
1881 Scope::External,
1882 ],
1883 &imt,
1884 &mut rng,
1885 );
1886 let mut inputs = inputs;
1888 let (extra, _) =
1889 make_real_note_inputs(&fvk, &[3_000_000], &[Scope::External], &imt, &mut rng);
1890 inputs.extend(extra);
1891
1892 let result = build_delegation_bundle(
1893 inputs,
1894 &fvk,
1895 pallas::Scalar::random(&mut rng),
1896 output_recipient,
1897 pallas::Base::random(&mut rng),
1898 pallas::Base::random(&mut rng),
1899 pallas::Base::random(&mut rng),
1900 &imt,
1901 &mut rng,
1902 None,
1903 );
1904
1905 assert!(matches!(
1906 result,
1907 Err(DelegationBuildError::InvalidNoteCount(6))
1908 ));
1909 }
1910
1911 #[test]
1912 fn test_missing_precomputed_padded_note_returns_error() {
1913 let precomputed = PrecomputedRandomness {
1914 padded_notes: vec![],
1915 rseed_signed: [0u8; 32],
1916 rseed_output: [0u8; 32],
1917 };
1918
1919 let result = build_single_note_bundle_with_precomputed(&precomputed);
1920
1921 assert!(matches!(
1922 result,
1923 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1924 index: 0,
1925 actual: 0
1926 })
1927 ));
1928 }
1929
1930 #[test]
1931 fn test_partial_precomputed_padded_notes_returns_later_missing_error() {
1932 let mut rng = OsRng;
1933 let sk = SpendingKey::random(&mut rng);
1934 let fvk: FullViewingKey = (&sk).into();
1935 let precomputed = PrecomputedRandomness {
1936 padded_notes: vec![
1937 make_valid_padded_note_data(&mut rng),
1938 make_valid_padded_note_data(&mut rng),
1939 ],
1940 rseed_signed: [0u8; 32],
1941 rseed_output: [0u8; 32],
1942 };
1943
1944 let result =
1945 build_single_note_bundle_with_fvk_and_precomputed(&fvk, &precomputed, &mut rng);
1946
1947 assert!(matches!(
1948 result,
1949 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1950 index: 2,
1951 actual: 2
1952 })
1953 ));
1954 }
1955
1956 #[test]
1957 fn test_invalid_precomputed_padded_rho_returns_error() {
1958 let precomputed = PrecomputedRandomness {
1959 padded_notes: vec![PaddedNoteData {
1960 rho: [0xffu8; 32],
1961 rseed: [0u8; 32],
1962 }],
1963 rseed_signed: [0u8; 32],
1964 rseed_output: [0u8; 32],
1965 };
1966
1967 let result = build_single_note_bundle_with_precomputed(&precomputed);
1968
1969 assert!(matches!(
1970 result,
1971 Err(DelegationBuildError::InvalidPrecomputedRho { index: 0 })
1972 ));
1973 }
1974
1975 #[test]
1976 fn test_single_internal_note_builds_expected_output_shape() {
1977 build_bundle(&[13_000_000], &[Scope::Internal]);
1978 }
1979
1980 #[test]
1981 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1982 fn test_mixed_scope_notes() {
1983 let bundle = build_bundle(
1984 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
1985 &[
1986 Scope::External,
1987 Scope::Internal,
1988 Scope::External,
1989 Scope::Internal,
1990 ],
1991 );
1992 verify_bundle(&bundle);
1993 }
1994
1995 #[test]
1996 fn test_all_internal_notes_builds_expected_output_shape() {
1997 build_bundle(
1998 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
1999 &[
2000 Scope::Internal,
2001 Scope::Internal,
2002 Scope::Internal,
2003 Scope::Internal,
2004 ],
2005 );
2006 }
2007}