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
191fn validate_padding_slot_index(slot_index: usize) -> Result<(), DelegationBuildError> {
192 if (1..circuit::MAX_REAL_NOTES).contains(&slot_index) {
193 Ok(())
194 } else {
195 Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index })
196 }
197}
198
199pub(crate) fn padding_points(
207 slot_index: usize,
208 ivk: pallas::Scalar,
209) -> Result<(NonIdentityPallasPoint, NonIdentityPallasPoint), DelegationBuildError> {
210 validate_padding_slot_index(slot_index)?;
211 let slot_index_u32 =
212 u32::try_from(slot_index).expect("validated padding slot index fits in u32");
213 let g_d_pad =
214 pallas::Point::hash_to_curve(PADDING_PERSONALIZATION)(&slot_index_u32.to_le_bytes());
215 let pk_d_pad = g_d_pad * ivk;
216 Ok((
217 non_identity_padding_point(g_d_pad, slot_index, "g_d")?,
218 non_identity_padding_point(pk_d_pad, slot_index, "pk_d")?,
219 ))
220}
221
222fn random_seed_for_rho(rho: &Rho, rng: &mut impl RngCore) -> RandomSeed {
227 loop {
228 let mut rseed = [0u8; 32];
229 rng.fill_bytes(&mut rseed);
230 let rseed = RandomSeed::from_bytes(rseed, rho);
231 if bool::from(rseed.is_some()) {
232 return rseed.unwrap();
233 }
234 }
235}
236
237fn note_commitment_point(
245 g_d: pallas::Point,
246 pk_d: pallas::Point,
247 value: NoteValue,
248 rho: pallas::Base,
249 psi: pallas::Base,
250 rcm: pallas::Scalar,
251) -> Option<pallas::Point> {
252 let domain = sinsemilla::CommitDomain::new(NOTE_COMMITMENT_PERSONALIZATION);
253 domain
255 .commit(
256 iter::empty()
257 .chain(byte_bits(g_d.to_affine().to_bytes()))
258 .chain(byte_bits(pk_d.to_affine().to_bytes()))
259 .chain(u64_bits(value.inner()).take(L_VALUE))
260 .chain(rho.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
261 .chain(psi.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
262 &rcm,
263 )
264 .into_option()
265}
266
267fn derive_note_nullifier(
272 nk: pallas::Base,
273 rho: pallas::Base,
274 psi: pallas::Base,
275 cm: pallas::Point,
276) -> Option<pallas::Base> {
277 let k = pallas::Point::hash_to_curve("z.cash:Orchard")(b"K");
278 let prf_nf = poseidon_hash_2(nk, rho);
279 let scalar = pallas::Scalar::from_repr((prf_nf + psi).to_repr())
281 .expect("Pallas base field is smaller than its scalar field");
282 point_x_opt(&(k * scalar + cm))
283}
284
285#[derive(Clone, Copy, Debug, PartialEq, Eq)]
294pub struct SyntheticPaddingNoteParts {
295 pub cmx: [u8; 32],
297 pub nullifier: [u8; 32],
300}
301
302struct SyntheticPaddingDerivation {
303 g_d_pad: NonIdentityPallasPoint,
304 pk_d_pad: NonIdentityPallasPoint,
305 psi: pallas::Base,
306 rcm: orchard::note::NoteCommitTrapdoor,
307 cm: pallas::Point,
308 cmx: pallas::Base,
309 real_nf: pallas::Base,
310}
311
312fn derive_synthetic_padding_note(
315 nk: pallas::Base,
316 ivk: pallas::Scalar,
317 slot_index: usize,
318 rho: Rho,
319 rseed: RandomSeed,
320 location: PrecomputedRandomnessLocation,
321) -> Result<SyntheticPaddingDerivation, DelegationBuildError> {
322 let (g_d_pad, pk_d_pad) = padding_points(slot_index, ivk)?;
325 let psi = rseed.psi(&rho);
326 let rcm = rseed.rcm(&rho);
327
328 let cm = note_commitment_point(
331 *g_d_pad,
332 *pk_d_pad,
333 NoteValue::ZERO,
334 rho.into_inner(),
335 psi,
336 rcm.inner(),
337 )
338 .ok_or(DelegationBuildError::InvalidPaddingNoteCommitment { location })?;
339 let cmx =
340 point_x_opt(&cm).ok_or(DelegationBuildError::InvalidPaddingNoteCommitment { location })?;
341 let real_nf = derive_note_nullifier(nk, rho.into_inner(), psi, cm)
342 .ok_or(DelegationBuildError::InvalidPaddingNullifier { location })?;
343
344 Ok(SyntheticPaddingDerivation {
345 g_d_pad,
346 pk_d_pad,
347 psi,
348 rcm,
349 cm,
350 cmx,
351 real_nf,
352 })
353}
354
355pub fn synthetic_padding_note_parts(
379 fvk: &FullViewingKey,
380 slot_index: usize,
381 rho: Rho,
382 rseed: RandomSeed,
383) -> Result<SyntheticPaddingNoteParts, DelegationBuildError> {
384 let ak: SpendValidatingKey = fvk.clone().into();
385 let ivk = external_ivk_scalar(fvk, &ak);
386 let location = PrecomputedRandomnessLocation::PaddedNote(slot_index);
387 let padding =
388 derive_synthetic_padding_note(fvk.nk().inner(), ivk, slot_index, rho, rseed, location)?;
389
390 Ok(SyntheticPaddingNoteParts {
391 cmx: padding.cmx.to_repr(),
392 nullifier: padding.real_nf.to_repr(),
393 })
394}
395
396struct PaddingSlot {
398 witness: NoteSlotWitness,
399 cmx: pallas::Base,
400 v_raw: u64,
401 gov_null: pallas::Base,
402 #[cfg(test)]
403 real_nf: pallas::Base,
404}
405
406#[allow(clippy::too_many_arguments)]
407fn build_padding_slot(
408 slot_index: usize,
409 pad_idx: usize,
410 nk: pallas::Base,
411 dom: pallas::Base,
412 ivk: pallas::Scalar,
413 imt_provider: &impl ImtProvider,
414 rng: &mut impl RngCore,
415 precomputed: Option<&PrecomputedRandomness>,
416) -> Result<PaddingSlot, DelegationBuildError> {
417 let location = PrecomputedRandomnessLocation::PaddedNote(pad_idx);
418
419 let (rho, rseed) = if let Some(pre) = precomputed {
420 if pad_idx >= pre.padded_notes.len() {
422 return Err(DelegationBuildError::MissingPrecomputedPaddedNote {
423 index: pad_idx,
424 actual: pre.padded_notes.len(),
425 });
426 }
427 let pd = &pre.padded_notes[pad_idx];
428 let rho = Rho::from_bytes(&pd.rho)
429 .into_option()
430 .ok_or(DelegationBuildError::InvalidPrecomputedRho { index: pad_idx })?;
431 let rseed = RandomSeed::from_bytes(pd.rseed, &rho)
432 .into_option()
433 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
434 (rho, rseed)
435 } else {
436 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
437 let rseed = random_seed_for_rho(&rho, &mut *rng);
438 (rho, rseed)
439 };
440
441 let padding = derive_synthetic_padding_note(nk, ivk, slot_index, rho, rseed, location)?;
442 let gov_null = gov_null_hash(nk, dom, padding.real_nf);
443 let imt_proof = imt_provider.non_membership_proof(padding.real_nf)?;
444
445 let merkle_path = MerklePath::dummy(&mut *rng);
448 let witness = NoteSlotWitness {
449 g_d: Value::known(padding.g_d_pad),
450 pk_d: Value::known(padding.pk_d_pad),
451 v: Value::known(NoteValue::ZERO),
452 rho: Value::known(rho.into_inner()),
453 psi: Value::known(padding.psi),
454 rcm: Value::known(padding.rcm),
455 cm: Value::known(padding.cm),
456 path: Value::known(merkle_path.auth_path()),
457 pos: Value::known(merkle_path.position()),
458 imt_nf_bounds: Value::known(imt_proof.nf_bounds),
459 imt_leaf_pos: Value::known(imt_proof.leaf_pos),
460 imt_path: Value::known(imt_proof.path),
461 is_internal: Value::known(false),
462 };
463
464 Ok(PaddingSlot {
465 witness,
466 cmx: padding.cmx,
467 v_raw: 0,
468 gov_null,
469 #[cfg(test)]
470 real_nf: padding.real_nf,
471 })
472}
473
474#[cfg(test)]
475pub(crate) struct PaddingSlotForTesting {
476 pub witness: NoteSlotWitness,
477 pub cmx: pallas::Base,
478 pub gov_null: pallas::Base,
479 pub real_nf: pallas::Base,
480}
481
482#[cfg(test)]
483pub(crate) fn build_padding_slot_for_testing(
484 slot_index: usize,
485 pad_idx: usize,
486 fvk: &FullViewingKey,
487 ak: &SpendValidatingKey,
488 dom: pallas::Base,
489 imt_provider: &impl ImtProvider,
490 rng: &mut impl RngCore,
491) -> Result<PaddingSlotForTesting, DelegationBuildError> {
492 let padding = build_padding_slot(
493 slot_index,
494 pad_idx,
495 fvk.nk().inner(),
496 dom,
497 external_ivk_scalar(fvk, ak),
498 imt_provider,
499 rng,
500 None,
501 )?;
502
503 Ok(PaddingSlotForTesting {
504 witness: padding.witness,
505 cmx: padding.cmx,
506 gov_null: padding.gov_null,
507 real_nf: padding.real_nf,
508 })
509}
510
511#[derive(Clone, Debug)]
513pub enum DelegationBuildError {
514 InvalidNoteCount(usize),
516 Instance(circuit::InstanceError),
518 InvalidPaddingSlotIndex { slot_index: usize },
520 InvalidPaddingPoint {
522 slot_index: usize,
523 component: &'static str,
524 },
525 InvalidPaddingNoteCommitment {
527 location: PrecomputedRandomnessLocation,
528 },
529 InvalidPaddingNullifier {
531 location: PrecomputedRandomnessLocation,
532 },
533 MissingPrecomputedPaddedNote { index: usize, actual: usize },
535 InvalidPrecomputedRho { index: usize },
537 InvalidPrecomputedRseed {
539 location: PrecomputedRandomnessLocation,
540 },
541 InvalidPrecomputedNote {
543 location: PrecomputedRandomnessLocation,
544 },
545 ImtFetchFailed(super::imt::ImtError),
547}
548
549impl From<circuit::InstanceError> for DelegationBuildError {
550 fn from(e: circuit::InstanceError) -> Self {
551 DelegationBuildError::Instance(e)
552 }
553}
554
555impl From<super::imt::ImtError> for DelegationBuildError {
556 fn from(e: super::imt::ImtError) -> Self {
557 DelegationBuildError::ImtFetchFailed(e)
558 }
559}
560
561impl std::fmt::Display for DelegationBuildError {
562 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563 match self {
564 DelegationBuildError::InvalidNoteCount(n) => {
565 write!(
566 f,
567 "invalid note count: {} (expected 1–{})",
568 n,
569 circuit::MAX_REAL_NOTES
570 )
571 }
572 DelegationBuildError::Instance(e) => {
573 write!(f, "instance construction failed: {e}")
574 }
575 DelegationBuildError::InvalidPaddingSlotIndex { slot_index } => {
576 write!(
577 f,
578 "invalid padding slot index {slot_index} (expected 1..={})",
579 circuit::MAX_REAL_NOTES - 1
580 )
581 }
582 DelegationBuildError::InvalidPaddingPoint {
583 slot_index,
584 component,
585 } => {
586 write!(
587 f,
588 "invalid padding point {component} at slot {slot_index}: identity point"
589 )
590 }
591 DelegationBuildError::InvalidPaddingNoteCommitment { location } => {
592 write!(f, "invalid padding note commitment for {location}")
593 }
594 DelegationBuildError::InvalidPaddingNullifier { location } => {
595 write!(f, "invalid padding nullifier for {location}")
596 }
597 DelegationBuildError::MissingPrecomputedPaddedNote { index, actual } => {
598 write!(
599 f,
600 "missing precomputed padded note at index {index} (got {actual} entries)"
601 )
602 }
603 DelegationBuildError::InvalidPrecomputedRho { index } => {
604 write!(f, "invalid precomputed padded note rho at index {index}")
605 }
606 DelegationBuildError::InvalidPrecomputedRseed { location } => {
607 write!(f, "invalid precomputed rseed for {location}")
608 }
609 DelegationBuildError::InvalidPrecomputedNote { location } => {
610 write!(f, "invalid precomputed note components for {location}")
611 }
612 DelegationBuildError::ImtFetchFailed(e) => {
613 write!(f, "IMT proof fetch failed: {e}")
614 }
615 }
616 }
617}
618
619impl std::fmt::Display for PrecomputedRandomnessLocation {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 match self {
622 PrecomputedRandomnessLocation::PaddedNote(index) => {
623 write!(f, "padded note {index}")
624 }
625 PrecomputedRandomnessLocation::SignedNote => write!(f, "signed note"),
626 PrecomputedRandomnessLocation::OutputNote => write!(f, "output note"),
627 }
628 }
629}
630
631#[allow(clippy::too_many_arguments)]
666pub fn build_delegation_bundle(
667 real_notes: Vec<RealNoteInput>,
668 fvk: &FullViewingKey,
669 alpha: pallas::Scalar,
670 output_recipient: orchard::Address,
671 vote_round_id: pallas::Base,
672 nc_root: pallas::Base,
673 van_comm_rand: pallas::Base,
674 imt_provider: &impl ImtProvider,
675 rng: &mut (impl RngCore + CryptoRng),
676 precomputed: Option<&PrecomputedRandomness>,
677) -> Result<DelegationBundle, DelegationBuildError> {
678 let n_real = real_notes.len();
681 if n_real == 0 || n_real > circuit::MAX_REAL_NOTES {
682 return Err(DelegationBuildError::InvalidNoteCount(n_real));
683 }
684
685 let nf_imt_root = imt_provider.root();
687
688 let nk_val = fvk.nk().inner();
690 let ak: SpendValidatingKey = fvk.clone().into();
691 let ivk = external_ivk_scalar(fvk, &ak);
692
693 let dom = derive_nullifier_domain(vote_round_id);
695
696 let mut note_slots = Vec::with_capacity(circuit::MAX_REAL_NOTES);
698 let mut cmx_values = Vec::with_capacity(circuit::MAX_REAL_NOTES);
699 let mut v_values = Vec::with_capacity(circuit::MAX_REAL_NOTES);
700 let mut gov_nulls = Vec::with_capacity(circuit::MAX_REAL_NOTES);
701
702 for input in &real_notes {
705 let note = &input.note;
706 let rho = note.rho();
707 let psi = note.rseed().psi(&rho);
708 let rcm = note.rseed().rcm(&rho);
709 let cm = note.commitment();
710 let cmx = ExtractedNoteCommitment::from(cm.clone()).inner();
711 let v_raw = note.value().inner();
712 let recipient = note.recipient();
713
714 let real_nf = note.nullifier(fvk);
716 let gov_null = gov_null_hash(nk_val, dom, real_nf.inner());
718
719 let slot = NoteSlotWitness {
720 g_d: Value::known(recipient.g_d()),
721 pk_d: Value::known(recipient.pk_d().inner()),
722 v: Value::known(note.value()),
723 rho: Value::known(rho.into_inner()),
724 psi: Value::known(psi),
725 rcm: Value::known(rcm),
726 cm: Value::known(cm.inner()),
727 path: Value::known(input.merkle_path.auth_path()),
728 pos: Value::known(input.merkle_path.position()),
729 imt_nf_bounds: Value::known(input.imt_proof.nf_bounds),
730 imt_leaf_pos: Value::known(input.imt_proof.leaf_pos),
731 imt_path: Value::known(input.imt_proof.path),
732 is_internal: Value::known(matches!(input.scope, Scope::Internal)),
733 };
734
735 note_slots.push(slot);
736 cmx_values.push(cmx);
737 v_values.push(v_raw);
738 gov_nulls.push(gov_null);
739 }
740
741 for i in n_real..circuit::MAX_REAL_NOTES {
745 let pad_idx = i - n_real; let padding =
747 build_padding_slot(i, pad_idx, nk_val, dom, ivk, imt_provider, rng, precomputed)?;
748
749 note_slots.push(padding.witness);
750 cmx_values.push(padding.cmx);
751 v_values.push(padding.v_raw);
752 gov_nulls.push(padding.gov_null);
753 }
754
755 let notes: [NoteSlotWitness; circuit::MAX_REAL_NOTES] =
756 note_slots.try_into().unwrap_or_else(|_| unreachable!());
757
758 let v_total_u64: u64 = v_values.iter().sum();
761 let num_ballots_u64 = v_total_u64 / circuit::BALLOT_DIVISOR;
762 let remainder_u64 = v_total_u64 % circuit::BALLOT_DIVISOR;
763 let num_ballots_field = pallas::Base::from(num_ballots_u64);
764
765 let g_d_new_x = *output_recipient
771 .g_d()
772 .to_affine()
773 .coordinates()
774 .unwrap()
775 .x();
776 let pk_d_new_x = *output_recipient
777 .pk_d()
778 .inner()
779 .to_affine()
780 .coordinates()
781 .unwrap()
782 .x();
783
784 let van_comm = van_commitment_hash(
785 g_d_new_x,
786 pk_d_new_x,
787 num_ballots_field,
788 vote_round_id,
789 van_comm_rand,
790 );
791
792 let rho = rho_binding_hash(
796 cmx_values[0],
797 cmx_values[1],
798 cmx_values[2],
799 cmx_values[3],
800 cmx_values[4],
801 van_comm,
802 vote_round_id,
803 );
804
805 let sender_address = fvk.address_at(0u32, Scope::External);
809 let signed_rho = Rho::from_nf_old(Nullifier::from_inner(rho));
810 let signed_note = if let Some(pre) = precomputed {
811 let location = PrecomputedRandomnessLocation::SignedNote;
812 let rseed = RandomSeed::from_bytes(pre.rseed_signed, &signed_rho)
813 .into_option()
814 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
815 Note::from_parts(sender_address, NoteValue::from_raw(1), signed_rho, rseed)
816 .into_option()
817 .ok_or(DelegationBuildError::InvalidPrecomputedNote { location })?
818 } else {
819 Note::new(
820 sender_address,
821 NoteValue::from_raw(1),
822 signed_rho,
823 &mut *rng,
824 )
825 };
826
827 let nf_signed = signed_note.nullifier(fvk);
829
830 let output_rho = Rho::from_nf_old(nf_signed);
833 let output_note = if let Some(pre) = precomputed {
834 let location = PrecomputedRandomnessLocation::OutputNote;
835 let rseed = RandomSeed::from_bytes(pre.rseed_output, &output_rho)
836 .into_option()
837 .ok_or(DelegationBuildError::InvalidPrecomputedRseed { location })?;
838 Note::from_parts(output_recipient, NoteValue::ZERO, output_rho, rseed)
839 .into_option()
840 .ok_or(DelegationBuildError::InvalidPrecomputedNote { location })?
841 } else {
842 Note::new(output_recipient, NoteValue::ZERO, output_rho, &mut *rng)
843 };
844 let cmx_new = ExtractedNoteCommitment::from(output_note.commitment()).inner();
845
846 let rk = ak.randomize(&alpha);
848
849 let circuit = circuit::Circuit::from_note_unchecked(fvk, &signed_note, alpha)
854 .with_output_note(&output_note)
855 .with_notes(notes)
856 .with_van_comm_rand(van_comm_rand)
857 .with_ballot_scaling(
858 pallas::Base::from(num_ballots_u64),
859 pallas::Base::from(remainder_u64),
860 );
861
862 let instance = circuit::Instance::from_parts(
863 nf_signed,
864 rk,
865 cmx_new,
866 van_comm,
867 vote_round_id,
868 nc_root,
869 nf_imt_root,
870 [
871 gov_nulls[0],
872 gov_nulls[1],
873 gov_nulls[2],
874 gov_nulls[3],
875 gov_nulls[4],
876 ],
877 dom,
878 )?;
879
880 Ok(DelegationBundle { circuit, instance })
881}
882
883#[cfg(test)]
888mod tests {
889 use super::*;
890 use crate::delegation::imt::{ImtError, SpacedLeafImtProvider};
891 use ff::Field;
892 use halo2_proofs::dev::MockProver;
893 use incrementalmerkletree::{Hashable, Level};
894 use orchard::{
895 constants::MERKLE_DEPTH_ORCHARD,
896 keys::{FullViewingKey, Scope, SpendingKey},
897 note::{commitment::ExtractedNoteCommitment, Note, Rho},
898 tree::{MerkleHashOrchard, MerklePath},
899 value::NoteValue,
900 };
901 use pasta_curves::pallas;
902 use rand::rngs::OsRng;
903 use std::cell::{Cell, RefCell};
904
905 const K: u32 = 14;
907
908 #[derive(Debug)]
909 struct RecordingImtProvider {
910 proof: ImtProofData,
911 error: Option<ImtError>,
912 requested_nfs: RefCell<Vec<pallas::Base>>,
913 }
914
915 impl RecordingImtProvider {
916 fn returning(proof: ImtProofData) -> Self {
917 Self {
918 proof,
919 error: None,
920 requested_nfs: RefCell::new(Vec::new()),
921 }
922 }
923
924 fn failing(error: ImtError) -> Self {
925 Self {
926 proof: test_imt_proof(),
927 error: Some(error),
928 requested_nfs: RefCell::new(Vec::new()),
929 }
930 }
931 }
932
933 impl ImtProvider for RecordingImtProvider {
934 fn root(&self) -> pallas::Base {
935 self.proof.root
936 }
937
938 fn non_membership_proof(&self, nf: pallas::Base) -> Result<ImtProofData, ImtError> {
939 self.requested_nfs.borrow_mut().push(nf);
940 match &self.error {
941 Some(error) => Err(error.clone()),
942 None => Ok(self.proof.clone()),
943 }
944 }
945 }
946
947 fn test_imt_proof() -> ImtProofData {
948 ImtProofData {
949 root: pallas::Base::from(900u64),
950 nf_bounds: [
951 pallas::Base::from(10u64),
952 pallas::Base::from(20u64),
953 pallas::Base::from(30u64),
954 ],
955 leaf_pos: 7,
956 path: std::array::from_fn(|i| pallas::Base::from(1_000u64 + i as u64)),
957 }
958 }
959
960 fn precomputed_padding_note(rng: &mut impl RngCore) -> (PaddedNoteData, Rho, RandomSeed) {
961 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
962 let rseed = random_seed_for_rho(&rho, &mut *rng);
963 (
964 PaddedNoteData {
965 rho: rho.to_bytes(),
966 rseed: *rseed.as_bytes(),
967 },
968 rho,
969 rseed,
970 )
971 }
972
973 fn assert_known<T>(value: &Value<T>, f: impl FnOnce(&T) -> bool) {
974 let checked = Cell::new(false);
975 value.assert_if_known(|actual| {
976 checked.set(true);
977 f(actual)
978 });
979 assert!(checked.get(), "expected known witness value");
980 }
981
982 fn assert_padding_slot_matches(
983 padding: &PaddingSlot,
984 slot_index: usize,
985 nk: pallas::Base,
986 dom: pallas::Base,
987 ivk: pallas::Scalar,
988 rho: Rho,
989 rseed: RandomSeed,
990 imt_proof: &ImtProofData,
991 requested_nfs: &[pallas::Base],
992 ) {
993 let (g_d_pad, pk_d_pad) =
994 padding_points(slot_index, ivk).expect("test padding points should be valid");
995 let psi = rseed.psi(&rho);
996 let rcm = rseed.rcm(&rho);
997 let cm = note_commitment_point(
998 *g_d_pad,
999 *pk_d_pad,
1000 NoteValue::ZERO,
1001 rho.into_inner(),
1002 psi,
1003 rcm.inner(),
1004 )
1005 .expect("test padding commitment should be valid");
1006 let real_nf = derive_note_nullifier(nk, rho.into_inner(), psi, cm)
1007 .expect("test padding nullifier should be valid");
1008
1009 assert_eq!(padding.cmx, point_x(&cm));
1010 assert_eq!(padding.v_raw, 0);
1011 assert_eq!(padding.gov_null, gov_null_hash(nk, dom, real_nf));
1012 assert_eq!(requested_nfs, &[real_nf]);
1013
1014 assert_known(&padding.witness.g_d, |actual| *actual == g_d_pad);
1015 assert_known(&padding.witness.pk_d, |actual| *actual == pk_d_pad);
1016 assert_known(&padding.witness.v, |actual| *actual == NoteValue::ZERO);
1017 assert_known(&padding.witness.rho, |actual| *actual == rho.into_inner());
1018 assert_known(&padding.witness.psi, |actual| *actual == psi);
1019 assert_known(&padding.witness.rcm, |actual| actual.inner() == rcm.inner());
1020 assert_known(&padding.witness.cm, |actual| *actual == cm);
1021 assert_known(&padding.witness.imt_nf_bounds, |actual| {
1022 *actual == imt_proof.nf_bounds
1023 });
1024 assert_known(&padding.witness.imt_leaf_pos, |actual| {
1025 *actual == imt_proof.leaf_pos
1026 });
1027 assert_known(&padding.witness.imt_path, |actual| {
1028 *actual == imt_proof.path
1029 });
1030 assert_known(&padding.witness.is_internal, |actual| !*actual);
1031 }
1032
1033 fn make_real_note_inputs(
1040 fvk: &FullViewingKey,
1041 values: &[u64],
1042 scopes: &[Scope],
1043 imt_provider: &impl ImtProvider,
1044 rng: &mut impl RngCore,
1045 ) -> (Vec<RealNoteInput>, pallas::Base) {
1046 let n = values.len();
1047 assert!(n >= 1 && n <= circuit::MAX_REAL_NOTES);
1048 assert_eq!(n, scopes.len());
1049
1050 let mut notes = Vec::with_capacity(n);
1052 for (idx, &v) in values.iter().enumerate() {
1053 let recipient = fvk.address_at(0u32, scopes[idx]);
1054 let note_value = NoteValue::from_raw(v);
1055 let (_, _, dummy_parent) = Note::dummy(&mut *rng, None);
1056 let note = Note::new(
1057 recipient,
1058 note_value,
1059 Rho::from_nf_old(dummy_parent.nullifier(fvk)),
1060 &mut *rng,
1061 );
1062 notes.push(note);
1063 }
1064
1065 let empty_leaf = MerkleHashOrchard::empty_leaf();
1067 let mut leaves = [empty_leaf; 8];
1068 for (i, note) in notes.iter().enumerate() {
1069 let cmx = ExtractedNoteCommitment::from(note.commitment());
1070 leaves[i] = MerkleHashOrchard::from_cmx(&cmx);
1071 }
1072
1073 let l1_0 = MerkleHashOrchard::combine(Level::from(0), &leaves[0], &leaves[1]);
1075 let l1_1 = MerkleHashOrchard::combine(Level::from(0), &leaves[2], &leaves[3]);
1076 let l1_2 = MerkleHashOrchard::combine(Level::from(0), &leaves[4], &leaves[5]);
1077 let l1_3 = MerkleHashOrchard::combine(Level::from(0), &leaves[6], &leaves[7]);
1078 let l2_0 = MerkleHashOrchard::combine(Level::from(1), &l1_0, &l1_1);
1079 let l2_1 = MerkleHashOrchard::combine(Level::from(1), &l1_2, &l1_3);
1080 let l3_0 = MerkleHashOrchard::combine(Level::from(2), &l2_0, &l2_1);
1081
1082 let mut current = l3_0;
1084 for level in 3..MERKLE_DEPTH_ORCHARD {
1085 let sibling = MerkleHashOrchard::empty_root(Level::from(level as u8));
1086 current = MerkleHashOrchard::combine(Level::from(level as u8), ¤t, &sibling);
1087 }
1088 let nc_root = current.inner();
1089
1090 let l1 = [l1_0, l1_1, l1_2, l1_3];
1092 let l2 = [l2_0, l2_1];
1093 let mut inputs = Vec::with_capacity(n);
1094 for (i, note) in notes.into_iter().enumerate() {
1095 let mut auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
1096 auth_path[0] = leaves[i ^ 1];
1097 auth_path[1] = l1[(i >> 1) ^ 1];
1098 auth_path[2] = l2[1 - (i >> 2)];
1099 for level in 3..MERKLE_DEPTH_ORCHARD {
1100 auth_path[level] = MerkleHashOrchard::empty_root(Level::from(level as u8));
1101 }
1102 let merkle_path = MerklePath::from_parts(i as u32, auth_path);
1103
1104 let real_nf = note.nullifier(fvk);
1105 let imt_proof = imt_provider.non_membership_proof(real_nf.inner()).unwrap();
1106
1107 inputs.push(RealNoteInput {
1108 note,
1109 fvk: fvk.clone(),
1110 merkle_path,
1111 imt_proof,
1112 scope: scopes[i],
1113 });
1114 }
1115
1116 (inputs, nc_root)
1117 }
1118
1119 fn build_bundle(values: &[u64], scopes: &[Scope]) -> DelegationBundle {
1121 assert_eq!(values.len(), scopes.len());
1122 let mut rng = OsRng;
1123 let sk = SpendingKey::random(&mut rng);
1124 let fvk: FullViewingKey = (&sk).into();
1125 let output_recipient = fvk.address_at(1u32, Scope::External);
1126 let vote_round_id = pallas::Base::random(&mut rng);
1127 let van_comm_rand = pallas::Base::random(&mut rng);
1128 let alpha = pallas::Scalar::random(&mut rng);
1129
1130 let imt = SpacedLeafImtProvider::new();
1131 let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
1132
1133 let bundle = build_delegation_bundle(
1134 inputs,
1135 &fvk,
1136 alpha,
1137 output_recipient,
1138 vote_round_id,
1139 nc_root,
1140 van_comm_rand,
1141 &imt,
1142 &mut rng,
1143 None,
1144 )
1145 .unwrap();
1146
1147 assert_delegation_output_shape(&bundle);
1148 bundle
1149 }
1150
1151 fn build_single_note_bundle_with_precomputed(
1152 precomputed: &PrecomputedRandomness,
1153 ) -> Result<DelegationBundle, DelegationBuildError> {
1154 let mut rng = OsRng;
1155 let sk = SpendingKey::random(&mut rng);
1156 let fvk: FullViewingKey = (&sk).into();
1157
1158 build_single_note_bundle_with_fvk_and_precomputed(&fvk, precomputed, &mut rng)
1159 }
1160
1161 fn build_single_note_bundle_with_fvk_and_precomputed(
1162 fvk: &FullViewingKey,
1163 precomputed: &PrecomputedRandomness,
1164 rng: &mut (impl RngCore + CryptoRng),
1165 ) -> Result<DelegationBundle, DelegationBuildError> {
1166 let output_recipient = fvk.address_at(1u32, Scope::External);
1167 let vote_round_id = pallas::Base::random(&mut *rng);
1168 let van_comm_rand = pallas::Base::random(&mut *rng);
1169 let alpha = pallas::Scalar::random(&mut *rng);
1170
1171 let imt = SpacedLeafImtProvider::new();
1172 let (inputs, nc_root) =
1173 make_real_note_inputs(fvk, &[13_000_000], &[Scope::External], &imt, &mut *rng);
1174
1175 build_delegation_bundle(
1176 inputs,
1177 fvk,
1178 alpha,
1179 output_recipient,
1180 vote_round_id,
1181 nc_root,
1182 van_comm_rand,
1183 &imt,
1184 rng,
1185 Some(precomputed),
1186 )
1187 }
1188
1189 fn make_valid_padded_note_data(rng: &mut impl RngCore) -> PaddedNoteData {
1190 let rho = Rho::from_nf_old(Nullifier::from_inner(pallas::Base::random(&mut *rng)));
1191 let rseed = random_seed_for_rho(&rho, rng);
1192
1193 PaddedNoteData {
1194 rho: rho.to_bytes(),
1195 rseed: *rseed.as_bytes(),
1196 }
1197 }
1198
1199 fn assert_delegation_output_shape(bundle: &DelegationBundle) {
1200 let pi = bundle.instance.to_halo2_instance();
1201 assert_eq!(pi.len(), 14, "delegation public input shape changed");
1202 assert_eq!(bundle.instance.gov_null.len(), 5);
1203 assert_eq!(pi[0], bundle.instance.nf_signed.inner());
1204 assert_eq!(pi[3], bundle.instance.cmx_new);
1205 assert_eq!(pi[4], bundle.instance.van_comm);
1206 assert_eq!(pi[5], bundle.instance.vote_round_id);
1207 assert_eq!(pi[6], bundle.instance.nc_root);
1208 assert_eq!(pi[7], bundle.instance.nf_imt_root);
1209 assert_eq!(&pi[8..13], &bundle.instance.gov_null);
1210 assert_eq!(pi[13], bundle.instance.dom);
1211 }
1212
1213 fn verify_bundle(bundle: &DelegationBundle) {
1214 let pi = bundle.instance.to_halo2_instance();
1216 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
1217 assert_eq!(prover.verify(), Ok(()), "merged circuit failed");
1218 }
1219
1220 #[test]
1221 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1222 fn test_single_real_note() {
1223 let bundle = build_bundle(&[13_000_000], &[Scope::External]);
1224 verify_bundle(&bundle);
1225 }
1226
1227 fn build_bundle_for_inspection(
1231 values: &[u64],
1232 scopes: &[Scope],
1233 ) -> (DelegationBundle, FullViewingKey, SpendValidatingKey) {
1234 let mut rng = OsRng;
1235 let sk = SpendingKey::random(&mut rng);
1236 let fvk: FullViewingKey = (&sk).into();
1237 let ak: SpendValidatingKey = fvk.clone().into();
1238 let output_recipient = fvk.address_at(1u32, Scope::External);
1239 let vote_round_id = pallas::Base::random(&mut rng);
1240 let van_comm_rand = pallas::Base::random(&mut rng);
1241 let alpha = pallas::Scalar::random(&mut rng);
1242 let imt = SpacedLeafImtProvider::new();
1243 let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
1244
1245 let bundle = build_delegation_bundle(
1246 inputs,
1247 &fvk,
1248 alpha,
1249 output_recipient,
1250 vote_round_id,
1251 nc_root,
1252 van_comm_rand,
1253 &imt,
1254 &mut rng,
1255 None,
1256 )
1257 .unwrap();
1258 (bundle, fvk, ak)
1259 }
1260
1261 #[test]
1262 fn test_single_real_note_locks_padding_witnesses() {
1263 let (bundle, fvk, ak) = build_bundle_for_inspection(&[13_000_000], &[Scope::External]);
1269 let ivk = external_ivk_scalar(&fvk, &ak);
1270 let notes = bundle.circuit.notes_for_testing();
1271 for slot_index in 1..5 {
1272 let (expected_g_d, expected_pk_d) =
1273 padding_points(slot_index, ivk).expect("test padding points should be valid");
1274 assert_known(¬es[slot_index].g_d, |actual| *actual == expected_g_d);
1275 assert_known(¬es[slot_index].pk_d, |actual| *actual == expected_pk_d);
1276 }
1277 }
1278
1279 #[test]
1280 fn test_five_real_notes_uses_no_padding() {
1281 let (bundle, fvk, ak) = build_bundle_for_inspection(&[2_500_000; 5], &[Scope::External; 5]);
1288 let ivk = external_ivk_scalar(&fvk, &ak);
1289 let padding_g_ds: Vec<_> = (1..circuit::MAX_REAL_NOTES)
1290 .map(|i| {
1291 padding_points(i, ivk)
1292 .expect("test padding points should be valid")
1293 .0
1294 })
1295 .collect();
1296 let notes = bundle.circuit.notes_for_testing();
1297 for slot_index in 0..5 {
1298 for pad_g_d in &padding_g_ds {
1299 assert_known(¬es[slot_index].g_d, |actual| actual != pad_g_d);
1300 }
1301 }
1302 }
1303
1304 #[test]
1305 fn test_padding_points_are_synthetic_and_ivk_bound() {
1306 let mut rng = OsRng;
1307 let sk = SpendingKey::random(&mut rng);
1308 let fvk: FullViewingKey = (&sk).into();
1309 let ak: SpendValidatingKey = fvk.clone().into();
1310 let ivk = external_ivk_scalar(&fvk, &ak);
1311
1312 for slot_index in 1..5 {
1313 let (g_d_pad, pk_d_pad) =
1314 padding_points(slot_index, ivk).expect("test padding points should be valid");
1315 let real_orchard_addr = fvk.address_at(slot_index as u32, Scope::External);
1316
1317 assert_eq!(*pk_d_pad, *g_d_pad * ivk);
1321 assert_ne!(
1322 g_d_pad.to_affine().to_bytes(),
1323 real_orchard_addr.g_d().to_affine().to_bytes()
1324 );
1325 assert_ne!(
1326 pk_d_pad.to_affine().to_bytes(),
1327 real_orchard_addr.pk_d().to_bytes()
1328 );
1329 }
1330 }
1331
1332 #[test]
1333 fn test_padding_points_reject_impossible_slot_indices() {
1334 let mut rng = OsRng;
1335 let sk = SpendingKey::random(&mut rng);
1336 let fvk: FullViewingKey = (&sk).into();
1337 let ak: SpendValidatingKey = fvk.clone().into();
1338 let ivk = external_ivk_scalar(&fvk, &ak);
1339
1340 for slot_index in [0, circuit::MAX_REAL_NOTES, usize::MAX] {
1341 assert!(matches!(
1342 padding_points(slot_index, ivk),
1343 Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index: actual })
1344 if actual == slot_index
1345 ));
1346 }
1347 }
1348
1349 #[test]
1364 fn test_padding_personalization_is_domain_separated_from_orchard() {
1365 use orchard::constants::KEY_DIVERSIFICATION_PERSONALIZATION;
1366
1367 assert_ne!(
1368 PADDING_PERSONALIZATION, KEY_DIVERSIFICATION_PERSONALIZATION,
1369 "padding personalization must be domain-separated from Orchard's \
1370 DiversifyHash personalization; otherwise synthetic padding `g_d_pad` \
1371 can collide with real diversified bases and ZCA-450's fix regresses"
1372 );
1373 }
1374
1375 fn fixture_real_note(
1389 scope: Scope,
1390 rng: &mut impl RngCore,
1391 ) -> (FullViewingKey, SpendValidatingKey, Note) {
1392 let sk = SpendingKey::random(rng);
1393 let fvk: FullViewingKey = (&sk).into();
1394 let ak: SpendValidatingKey = fvk.clone().into();
1395 let recipient = fvk.address_at(0u32, scope);
1396 let (_, _, dummy_parent) = Note::dummy(rng, None);
1397 let note = Note::new(
1398 recipient,
1399 NoteValue::from_raw(12_500_000),
1400 Rho::from_nf_old(dummy_parent.nullifier(&fvk)),
1401 rng,
1402 );
1403 (fvk, ak, note)
1404 }
1405
1406 #[test]
1407 fn test_note_commitment_point_matches_orchard() {
1408 let mut rng = OsRng;
1413 for scope in [Scope::External, Scope::Internal] {
1414 let (_fvk, _ak, note) = fixture_real_note(scope, &mut rng);
1415 let recipient = note.recipient();
1416 let rho = note.rho();
1417 let psi = note.rseed().psi(&rho);
1418 let rcm = note.rseed().rcm(&rho);
1419
1420 let mirrored = note_commitment_point(
1421 *recipient.g_d(),
1422 *recipient.pk_d().inner(),
1423 note.value(),
1424 rho.into_inner(),
1425 psi,
1426 rcm.inner(),
1427 );
1428 let orchard = note.commitment().inner();
1429
1430 assert_eq!(
1431 mirrored,
1432 Some(orchard),
1433 "note_commitment_point drifted from Orchard NoteCommitment::derive ({scope:?})"
1434 );
1435 }
1436 }
1437
1438 #[test]
1439 fn test_derive_note_nullifier_matches_orchard() {
1440 let mut rng = OsRng;
1445 for scope in [Scope::External, Scope::Internal] {
1446 let (fvk, _ak, note) = fixture_real_note(scope, &mut rng);
1447 let nk = fvk.nk().inner();
1448 let rho = note.rho();
1449 let psi = note.rseed().psi(&rho);
1450 let cm = note.commitment().inner();
1451
1452 let mirrored = derive_note_nullifier(nk, rho.into_inner(), psi, cm);
1453 let orchard = note.nullifier(&fvk).inner();
1454
1455 assert_eq!(
1456 mirrored,
1457 Some(orchard),
1458 "derive_note_nullifier drifted from Orchard Nullifier::derive ({scope:?})"
1459 );
1460 }
1461 }
1462
1463 #[test]
1464 fn test_external_ivk_scalar_matches_orchard_address_derivation() {
1465 let mut rng = OsRng;
1472 let sk = SpendingKey::random(&mut rng);
1473 let fvk: FullViewingKey = (&sk).into();
1474 let ak: SpendValidatingKey = fvk.clone().into();
1475 let ivk = external_ivk_scalar(&fvk, &ak);
1476
1477 for idx in [0u32, 1, 7, 1234] {
1480 let addr = fvk.address_at(idx, Scope::External);
1481 assert_eq!(
1482 *addr.g_d() * ivk,
1483 *addr.pk_d().inner(),
1484 "external_ivk_scalar drifted: [ivk] * g_d != pk_d at diversifier index {idx}"
1485 );
1486 }
1487
1488 let internal_addr = fvk.address_at(0u32, Scope::Internal);
1492 assert_ne!(
1493 *internal_addr.g_d() * ivk,
1494 *internal_addr.pk_d().inner(),
1495 "external_ivk_scalar incorrectly validates an internal-scope address"
1496 );
1497 }
1498
1499 #[test]
1500 fn test_build_padding_slot_fresh_randomness_populates_strict_witnesses() {
1501 let mut rng = OsRng;
1502 let sk = SpendingKey::random(&mut rng);
1503 let fvk: FullViewingKey = (&sk).into();
1504 let ak: SpendValidatingKey = fvk.clone().into();
1505 let nk = fvk.nk().inner();
1506 let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
1507 let ivk = external_ivk_scalar(&fvk, &ak);
1508 let imt_proof = test_imt_proof();
1509 let imt = RecordingImtProvider::returning(imt_proof.clone());
1510
1511 let padding = build_padding_slot(3, 0, nk, dom, ivk, &imt, &mut rng, None).unwrap();
1512
1513 let (g_d_pad, pk_d_pad) =
1514 padding_points(3, ivk).expect("test padding points should be valid");
1515 assert_known(&padding.witness.g_d, |actual| *actual == g_d_pad);
1516 assert_known(&padding.witness.pk_d, |actual| *actual == pk_d_pad);
1517 let generated_padding_values = padding
1518 .witness
1519 .rho
1520 .as_ref()
1521 .copied()
1522 .zip(padding.witness.psi.as_ref().copied())
1523 .zip(padding.witness.rcm.as_ref().cloned())
1524 .zip(padding.witness.cm.as_ref().copied());
1525 assert_known(
1526 &generated_padding_values,
1527 |(((rho_inner, psi), rcm), cm_witness)| {
1528 let cm = note_commitment_point(
1529 *g_d_pad,
1530 *pk_d_pad,
1531 NoteValue::ZERO,
1532 *rho_inner,
1533 *psi,
1534 rcm.inner(),
1535 )
1536 .expect("test padding commitment should be valid");
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 == point_x(&cm)
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 let expected_nf = derive_note_nullifier(nk, rho.into_inner(), expected_psi, expected_cm)
1633 .expect("test padding nullifier should be valid");
1634
1635 assert_eq!(derived.g_d_pad, expected_g_d);
1636 assert_eq!(derived.pk_d_pad, expected_pk_d);
1637 assert_eq!(derived.psi, expected_psi);
1638 assert_eq!(derived.rcm.inner(), expected_rcm.inner());
1639 assert_eq!(derived.cm, expected_cm);
1640 assert_eq!(derived.cmx, point_x(&expected_cm));
1641 assert_eq!(derived.real_nf, expected_nf);
1642 }
1643
1644 #[test]
1645 fn test_synthetic_padding_note_parts_matches_padding_slot_derivation() {
1646 let mut rng = OsRng;
1647 let sk = SpendingKey::random(&mut rng);
1648 let fvk: FullViewingKey = (&sk).into();
1649 let ak: SpendValidatingKey = fvk.clone().into();
1650 let nk = fvk.nk().inner();
1651 let dom = derive_nullifier_domain(pallas::Base::random(&mut rng));
1652 let ivk = external_ivk_scalar(&fvk, &ak);
1653 let imt = RecordingImtProvider::returning(test_imt_proof());
1654
1655 let (padded_note, rho, rseed) = precomputed_padding_note(&mut rng);
1656 let precomputed = PrecomputedRandomness {
1657 padded_notes: vec![padded_note],
1658 rseed_signed: [0; 32],
1659 rseed_output: [0; 32],
1660 };
1661
1662 let padding =
1663 build_padding_slot(3, 0, nk, dom, ivk, &imt, &mut rng, Some(&precomputed)).unwrap();
1664 let parts = crate::delegation::synthetic_padding_note_parts(&fvk, 3, rho, rseed).unwrap();
1665
1666 assert_eq!(
1667 parts,
1668 crate::delegation::SyntheticPaddingNoteParts {
1669 cmx: padding.cmx.to_repr(),
1670 nullifier: padding.real_nf.to_repr(),
1671 }
1672 );
1673 assert_eq!(imt.requested_nfs.borrow().as_slice(), &[padding.real_nf]);
1674 }
1675
1676 #[test]
1677 fn test_synthetic_padding_note_parts_rejects_impossible_slot_indices() {
1678 let mut rng = OsRng;
1679 let sk = SpendingKey::random(&mut rng);
1680 let fvk: FullViewingKey = (&sk).into();
1681 let (_padded_note, rho, rseed) = precomputed_padding_note(&mut rng);
1682
1683 for slot_index in [0, circuit::MAX_REAL_NOTES, usize::MAX] {
1684 assert!(matches!(
1685 crate::delegation::synthetic_padding_note_parts(&fvk, slot_index, rho, rseed),
1686 Err(DelegationBuildError::InvalidPaddingSlotIndex { slot_index: actual })
1687 if actual == slot_index
1688 ));
1689 }
1690 }
1691
1692 #[test]
1693 fn test_build_padding_slot_propagates_imt_errors() {
1694 let mut rng = OsRng;
1695 let sk = SpendingKey::random(&mut rng);
1696 let fvk: FullViewingKey = (&sk).into();
1697 let ak: SpendValidatingKey = fvk.clone().into();
1698 let imt = RecordingImtProvider::failing(ImtError("fixture failure".to_string()));
1699
1700 let result = build_padding_slot(
1701 2,
1702 0,
1703 fvk.nk().inner(),
1704 derive_nullifier_domain(pallas::Base::random(&mut rng)),
1705 external_ivk_scalar(&fvk, &ak),
1706 &imt,
1707 &mut rng,
1708 None,
1709 );
1710
1711 assert!(matches!(
1712 result,
1713 Err(DelegationBuildError::ImtFetchFailed(ImtError(message)))
1714 if message == "fixture failure"
1715 ));
1716 assert_eq!(imt.requested_nfs.borrow().len(), 1);
1717 }
1718
1719 #[test]
1720 fn test_build_padding_slot_rejects_missing_precomputed_padding_entry() {
1721 let mut rng = OsRng;
1722 let sk = SpendingKey::random(&mut rng);
1723 let fvk: FullViewingKey = (&sk).into();
1724 let ak: SpendValidatingKey = fvk.clone().into();
1725 let imt = RecordingImtProvider::returning(test_imt_proof());
1726 let precomputed = PrecomputedRandomness {
1727 padded_notes: vec![],
1728 rseed_signed: [0; 32],
1729 rseed_output: [0; 32],
1730 };
1731
1732 let result = build_padding_slot(
1733 1,
1734 0,
1735 fvk.nk().inner(),
1736 derive_nullifier_domain(pallas::Base::random(&mut rng)),
1737 external_ivk_scalar(&fvk, &ak),
1738 &imt,
1739 &mut rng,
1740 Some(&precomputed),
1741 );
1742
1743 assert!(matches!(
1744 result,
1745 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1746 index: 0,
1747 actual: 0
1748 })
1749 ));
1750 }
1751
1752 #[test]
1753 fn test_four_real_notes_builds_expected_output_shape() {
1754 build_bundle(
1756 &[3_200_000, 3_200_000, 3_200_000, 3_200_000],
1757 &[
1758 Scope::External,
1759 Scope::External,
1760 Scope::External,
1761 Scope::External,
1762 ],
1763 );
1764 }
1765
1766 #[test]
1767 fn test_two_real_notes_builds_expected_output_shape() {
1768 build_bundle(&[7_000_000, 7_000_000], &[Scope::External, Scope::External]);
1769 }
1770
1771 #[test]
1772 fn test_min_weight_boundary_builds_expected_output_shape() {
1773 build_bundle(&[12_500_000], &[Scope::External]);
1775 }
1776
1777 #[test]
1778 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1779 fn test_below_one_ballot() {
1780 let mut rng = OsRng;
1783 let sk = SpendingKey::random(&mut rng);
1784 let fvk: FullViewingKey = (&sk).into();
1785 let output_recipient = fvk.address_at(1u32, Scope::External);
1786 let vote_round_id = pallas::Base::random(&mut rng);
1787 let van_comm_rand = pallas::Base::random(&mut rng);
1788 let alpha = pallas::Scalar::random(&mut rng);
1789
1790 let imt = SpacedLeafImtProvider::new();
1791 let (inputs, nc_root) =
1792 make_real_note_inputs(&fvk, &[12_499_999], &[Scope::External], &imt, &mut rng);
1793
1794 let bundle = build_delegation_bundle(
1795 inputs,
1796 &fvk,
1797 alpha,
1798 output_recipient,
1799 vote_round_id,
1800 nc_root,
1801 van_comm_rand,
1802 &imt,
1803 &mut rng,
1804 None,
1805 )
1806 .unwrap();
1807
1808 let pi = bundle.instance.to_halo2_instance();
1809 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
1810 assert!(prover.verify().is_err(), "below one ballot should fail");
1811 }
1812
1813 #[test]
1814 fn test_three_ballots_builds_expected_output_shape() {
1815 build_bundle(
1817 &[12_500_000, 12_500_000, 12_500_000],
1818 &[Scope::External, Scope::External, Scope::External],
1819 );
1820 }
1821
1822 #[test]
1823 fn test_zero_notes_error() {
1824 let mut rng = OsRng;
1825 let sk = SpendingKey::random(&mut rng);
1826 let fvk: FullViewingKey = (&sk).into();
1827 let output_recipient = fvk.address_at(1u32, Scope::External);
1828 let imt = SpacedLeafImtProvider::new();
1829
1830 let result = build_delegation_bundle(
1831 vec![],
1832 &fvk,
1833 pallas::Scalar::random(&mut rng),
1834 output_recipient,
1835 pallas::Base::random(&mut rng),
1836 pallas::Base::random(&mut rng),
1837 pallas::Base::random(&mut rng),
1838 &imt,
1839 &mut rng,
1840 None,
1841 );
1842
1843 assert!(matches!(
1844 result,
1845 Err(DelegationBuildError::InvalidNoteCount(0))
1846 ));
1847 }
1848
1849 #[test]
1850 fn test_five_real_notes_builds_expected_output_shape() {
1851 build_bundle(
1853 &[2_500_000, 2_500_000, 2_500_000, 2_500_000, 2_500_000],
1854 &[
1855 Scope::External,
1856 Scope::External,
1857 Scope::External,
1858 Scope::External,
1859 Scope::External,
1860 ],
1861 );
1862 }
1863
1864 #[test]
1865 fn test_six_notes_error() {
1866 let mut rng = OsRng;
1867 let sk = SpendingKey::random(&mut rng);
1868 let fvk: FullViewingKey = (&sk).into();
1869 let output_recipient = fvk.address_at(1u32, Scope::External);
1870 let imt = SpacedLeafImtProvider::new();
1871
1872 let (inputs, _) = make_real_note_inputs(
1873 &fvk,
1874 &[3_000_000, 3_000_000, 3_000_000, 3_000_000, 3_000_000],
1875 &[
1876 Scope::External,
1877 Scope::External,
1878 Scope::External,
1879 Scope::External,
1880 Scope::External,
1881 ],
1882 &imt,
1883 &mut rng,
1884 );
1885 let mut inputs = inputs;
1887 let (extra, _) =
1888 make_real_note_inputs(&fvk, &[3_000_000], &[Scope::External], &imt, &mut rng);
1889 inputs.extend(extra);
1890
1891 let result = build_delegation_bundle(
1892 inputs,
1893 &fvk,
1894 pallas::Scalar::random(&mut rng),
1895 output_recipient,
1896 pallas::Base::random(&mut rng),
1897 pallas::Base::random(&mut rng),
1898 pallas::Base::random(&mut rng),
1899 &imt,
1900 &mut rng,
1901 None,
1902 );
1903
1904 assert!(matches!(
1905 result,
1906 Err(DelegationBuildError::InvalidNoteCount(6))
1907 ));
1908 }
1909
1910 #[test]
1911 fn test_missing_precomputed_padded_note_returns_error() {
1912 let precomputed = PrecomputedRandomness {
1913 padded_notes: vec![],
1914 rseed_signed: [0u8; 32],
1915 rseed_output: [0u8; 32],
1916 };
1917
1918 let result = build_single_note_bundle_with_precomputed(&precomputed);
1919
1920 assert!(matches!(
1921 result,
1922 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1923 index: 0,
1924 actual: 0
1925 })
1926 ));
1927 }
1928
1929 #[test]
1930 fn test_partial_precomputed_padded_notes_returns_later_missing_error() {
1931 let mut rng = OsRng;
1932 let sk = SpendingKey::random(&mut rng);
1933 let fvk: FullViewingKey = (&sk).into();
1934 let precomputed = PrecomputedRandomness {
1935 padded_notes: vec![
1936 make_valid_padded_note_data(&mut rng),
1937 make_valid_padded_note_data(&mut rng),
1938 ],
1939 rseed_signed: [0u8; 32],
1940 rseed_output: [0u8; 32],
1941 };
1942
1943 let result =
1944 build_single_note_bundle_with_fvk_and_precomputed(&fvk, &precomputed, &mut rng);
1945
1946 assert!(matches!(
1947 result,
1948 Err(DelegationBuildError::MissingPrecomputedPaddedNote {
1949 index: 2,
1950 actual: 2
1951 })
1952 ));
1953 }
1954
1955 #[test]
1956 fn test_invalid_precomputed_padded_rho_returns_error() {
1957 let precomputed = PrecomputedRandomness {
1958 padded_notes: vec![PaddedNoteData {
1959 rho: [0xffu8; 32],
1960 rseed: [0u8; 32],
1961 }],
1962 rseed_signed: [0u8; 32],
1963 rseed_output: [0u8; 32],
1964 };
1965
1966 let result = build_single_note_bundle_with_precomputed(&precomputed);
1967
1968 assert!(matches!(
1969 result,
1970 Err(DelegationBuildError::InvalidPrecomputedRho { index: 0 })
1971 ));
1972 }
1973
1974 #[test]
1975 fn test_single_internal_note_builds_expected_output_shape() {
1976 build_bundle(&[13_000_000], &[Scope::Internal]);
1977 }
1978
1979 #[test]
1980 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
1981 fn test_mixed_scope_notes() {
1982 let bundle = build_bundle(
1983 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
1984 &[
1985 Scope::External,
1986 Scope::Internal,
1987 Scope::External,
1988 Scope::Internal,
1989 ],
1990 );
1991 verify_bundle(&bundle);
1992 }
1993
1994 #[test]
1995 fn test_all_internal_notes_builds_expected_output_shape() {
1996 build_bundle(
1997 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
1998 &[
1999 Scope::Internal,
2000 Scope::Internal,
2001 Scope::Internal,
2002 Scope::Internal,
2003 ],
2004 );
2005 }
2006}