1use group::{Curve, GroupEncoding};
32use halo2_proofs::{
33 circuit::{floor_planner, AssignedCell, Layouter, Value},
34 plonk::{self, Advice, Column, Constraints, Instance as InstanceColumn, Selector},
35 poly::Rotation,
36};
37use pasta_curves::{arithmetic::CurveAffine, pallas, vesta};
38use std::vec::Vec;
39
40use super::imt::{gov_auth_domain_tag, gov_null_domain_tag, IMT_DEPTH};
41use super::imt_circuit::{synthesize_imt_non_membership, ImtNonMembershipConfig};
42use crate::circuit::address_ownership::prove_address_ownership;
43use crate::circuit::gadget::assign_constant;
44use crate::circuit::mul_chip::{MulChip, MulConfig, MulInstruction};
45use crate::circuit::van_integrity;
46use crate::domain_tags;
47use crate::protocol_hash::poseidon_hash_in_circuit;
48use halo2_gadgets::{
49 ecc::{
50 chip::{EccChip, EccConfig},
51 NonIdentityPoint, Point, ScalarFixed, ScalarVar,
52 },
53 poseidon::{
54 primitives::{self as poseidon, ConstantLength},
55 Hash as PoseidonHash, Pow5Chip as PoseidonChip, Pow5Config as PoseidonConfig,
56 },
57 sinsemilla::{
58 chip::{SinsemillaChip, SinsemillaConfig},
59 merkle::{
60 chip::{MerkleChip, MerkleConfig},
61 MerklePath as GadgetMerklePath,
62 },
63 },
64 utilities::{
65 bool_check,
66 lookup_range_check::{LookupRangeCheck, LookupRangeCheckConfig},
67 },
68};
69use orchard::constants::MERKLE_DEPTH_ORCHARD;
70use orchard::{
71 circuit::{
72 commit_ivk::{CommitIvkChip, CommitIvkConfig},
73 gadget::{
74 add_chip::{AddChip, AddConfig},
75 assign_free_advice, derive_nullifier, note_commit, AddInstruction,
76 },
77 note_commit::{NoteCommitChip, NoteCommitConfig},
78 },
79 constants::{OrchardCommitDomains, OrchardFixedBases, OrchardHashDomains},
80 keys::{
81 CommitIvkRandomness, DiversifiedTransmissionKey, FullViewingKey, NullifierDerivingKey,
82 Scope, SpendValidatingKey,
83 },
84 note::{
85 commitment::{NoteCommitTrapdoor, NoteCommitment},
86 nullifier::Nullifier,
87 Note,
88 },
89 primitives::redpallas::{SpendAuth, VerificationKey},
90 spec::NonIdentityPallasPoint,
91 tree::MerkleHashOrchard,
92 value::NoteValue,
93};
94
95pub const K: u32 = 14;
105
106pub const NF_SIGNED_PUBLIC_OFFSET: usize = 0;
112pub const RK_X_PUBLIC_OFFSET: usize = 1;
114pub const RK_Y_PUBLIC_OFFSET: usize = 2;
116pub const CMX_NEW_PUBLIC_OFFSET: usize = 3;
118pub const VAN_COMM_PUBLIC_OFFSET: usize = 4;
120pub const VOTE_ROUND_ID_PUBLIC_OFFSET: usize = 5;
122pub const NC_ROOT_PUBLIC_OFFSET: usize = 6;
124pub const NF_IMT_ROOT_PUBLIC_OFFSET: usize = 7;
126pub const GOV_NULL_1_PUBLIC_OFFSET: usize = 8;
128pub const GOV_NULL_2_PUBLIC_OFFSET: usize = 9;
129pub const GOV_NULL_3_PUBLIC_OFFSET: usize = 10;
130pub const GOV_NULL_4_PUBLIC_OFFSET: usize = 11;
131pub const GOV_NULL_5_PUBLIC_OFFSET: usize = 12;
132
133pub const GOV_NULL_PUBLIC_OFFSETS: [usize; 5] = [
135 GOV_NULL_1_PUBLIC_OFFSET,
136 GOV_NULL_2_PUBLIC_OFFSET,
137 GOV_NULL_3_PUBLIC_OFFSET,
138 GOV_NULL_4_PUBLIC_OFFSET,
139 GOV_NULL_5_PUBLIC_OFFSET,
140];
141pub const DOM_PUBLIC_OFFSET: usize = 13;
143
144pub(crate) const MAX_PROPOSAL_AUTHORITY: u64 = 65535; pub(crate) const MAX_REAL_NOTES: usize = 5;
164
165pub(crate) fn rho_binding_hash(
169 cmx_1: pallas::Base,
170 cmx_2: pallas::Base,
171 cmx_3: pallas::Base,
172 cmx_4: pallas::Base,
173 cmx_5: pallas::Base,
174 van_comm: pallas::Base,
175 vote_round_id: pallas::Base,
176) -> pallas::Base {
177 poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<8>, 3, 2>::init().hash([
178 domain_tags::delegation_rho_binding(),
179 cmx_1,
180 cmx_2,
181 cmx_3,
182 cmx_4,
183 cmx_5,
184 van_comm,
185 vote_round_id,
186 ])
187}
188
189pub(crate) const BALLOT_DIVISOR: u64 = 12_500_000;
193
194pub(crate) fn van_commitment_hash(
201 g_d_new_x: pallas::Base,
202 pk_d_new_x: pallas::Base,
203 num_ballots: pallas::Base,
204 vote_round_id: pallas::Base,
205 van_comm_rand: pallas::Base,
206) -> pallas::Base {
207 van_integrity::van_integrity_hash(
208 g_d_new_x,
209 pk_d_new_x,
210 num_ballots,
211 vote_round_id,
212 pallas::Base::from(MAX_PROPOSAL_AUTHORITY),
213 van_comm_rand,
214 )
215}
216
217#[derive(Clone, Debug)]
223pub struct Config {
224 primary: Column<InstanceColumn>,
226 advices: [Column<Advice>; 10],
233 add_config: AddConfig,
236 mul_config: MulConfig,
239 ecc_config: EccConfig<OrchardFixedBases>,
243 poseidon_config: PoseidonConfig<pallas::Base, 3, 2>,
245 sinsemilla_config_1:
249 SinsemillaConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
250 sinsemilla_config_2:
254 SinsemillaConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
255 commit_ivk_config: CommitIvkConfig,
257 signed_note_commit_config: NoteCommitConfig,
259 new_note_commit_config: NoteCommitConfig,
261 range_check: LookupRangeCheckConfig<pallas::Base, 10>,
265 merkle_config_1: MerkleConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
268 merkle_config_2: MerkleConfig<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases>,
272 q_per_note: Selector,
276 q_scope_select: Selector,
279 imt_config: ImtNonMembershipConfig,
281}
282
283impl Config {
284 fn add_chip(&self) -> AddChip {
285 AddChip::construct(self.add_config.clone())
286 }
287
288 fn mul_chip(&self) -> MulChip {
289 MulChip::construct(self.mul_config.clone())
290 }
291
292 fn ecc_chip(&self) -> EccChip<OrchardFixedBases> {
293 EccChip::construct(self.ecc_config.clone())
294 }
295
296 fn poseidon_chip(&self) -> PoseidonChip<pallas::Base, 3, 2> {
301 PoseidonChip::construct(self.poseidon_config.clone())
302 }
303
304 fn commit_ivk_chip(&self) -> CommitIvkChip {
305 CommitIvkChip::construct(self.commit_ivk_config.clone())
306 }
307
308 fn sinsemilla_chip_1(
309 &self,
310 ) -> SinsemillaChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
311 SinsemillaChip::construct(self.sinsemilla_config_1.clone())
312 }
313
314 fn sinsemilla_chip_2(
315 &self,
316 ) -> SinsemillaChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
317 SinsemillaChip::construct(self.sinsemilla_config_2.clone())
318 }
319
320 fn note_commit_chip_signed(&self) -> NoteCommitChip {
321 NoteCommitChip::construct(self.signed_note_commit_config.clone())
322 }
323
324 fn note_commit_chip_new(&self) -> NoteCommitChip {
325 NoteCommitChip::construct(self.new_note_commit_config.clone())
326 }
327
328 fn merkle_chip_1(
329 &self,
330 ) -> MerkleChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
331 MerkleChip::construct(self.merkle_config_1.clone())
332 }
333
334 fn merkle_chip_2(
335 &self,
336 ) -> MerkleChip<OrchardHashDomains, OrchardCommitDomains, OrchardFixedBases> {
337 MerkleChip::construct(self.merkle_config_2.clone())
338 }
339
340 fn range_check_config(&self) -> LookupRangeCheckConfig<pallas::Base, 10> {
341 self.range_check
342 }
343}
344
345#[derive(Clone, Debug, Default)]
351pub struct NoteSlotWitness {
352 pub(crate) g_d: Value<NonIdentityPallasPoint>,
353 pub(crate) pk_d: Value<NonIdentityPallasPoint>,
354 pub(crate) v: Value<NoteValue>,
355 pub(crate) rho: Value<pallas::Base>,
356 pub(crate) psi: Value<pallas::Base>,
357 pub(crate) rcm: Value<NoteCommitTrapdoor>,
358 pub(crate) cm: Value<pallas::Point>,
359 pub(crate) path: Value<[MerkleHashOrchard; MERKLE_DEPTH_ORCHARD]>,
360 pub(crate) pos: Value<u32>,
361 pub(crate) imt_nf_bounds: Value<[pallas::Base; 3]>,
362 pub(crate) imt_leaf_pos: Value<u32>,
363 pub(crate) imt_path: Value<[pallas::Base; IMT_DEPTH]>,
364 pub(crate) is_internal: Value<bool>,
367}
368
369#[derive(Clone, Debug, Default)]
377pub struct Circuit {
378 nk: Value<NullifierDerivingKey>,
380 rho_signed: Value<pallas::Base>,
381 psi_signed: Value<pallas::Base>,
382 cm_signed: Value<NoteCommitment>,
383 ak: Value<SpendValidatingKey>,
384 alpha: Value<pallas::Scalar>,
385 rivk: Value<CommitIvkRandomness>,
386 rivk_internal: Value<CommitIvkRandomness>,
387 rcm_signed: Value<NoteCommitTrapdoor>,
388 g_d_signed: Value<NonIdentityPallasPoint>,
389 pk_d_signed: Value<DiversifiedTransmissionKey>,
390 g_d_new: Value<NonIdentityPallasPoint>,
393 pk_d_new: Value<DiversifiedTransmissionKey>,
394 psi_new: Value<pallas::Base>,
395 rcm_new: Value<NoteCommitTrapdoor>,
396 notes: [NoteSlotWitness; 5],
398 van_comm_rand: Value<pallas::Base>,
400 num_ballots: Value<pallas::Base>,
403 remainder: Value<pallas::Base>,
404}
405
406impl Circuit {
407 pub fn from_note_unchecked(fvk: &FullViewingKey, note: &Note, alpha: pallas::Scalar) -> Self {
409 let sender_address = note.recipient();
410 let rho_signed = note.rho();
411 let psi_signed = note.rseed().psi(&rho_signed);
412 let rcm_signed = note.rseed().rcm(&rho_signed);
413 Circuit {
414 nk: Value::known(*fvk.nk()),
415 rho_signed: Value::known(rho_signed.into_inner()),
416 psi_signed: Value::known(psi_signed),
417 cm_signed: Value::known(note.commitment()),
418 ak: Value::known(fvk.clone().into()),
419 alpha: Value::known(alpha),
420 rivk: Value::known(fvk.rivk(Scope::External)),
421 rivk_internal: Value::known(fvk.rivk(Scope::Internal)),
422 rcm_signed: Value::known(rcm_signed),
423 g_d_signed: Value::known(sender_address.g_d()),
424 pk_d_signed: Value::known(*sender_address.pk_d()),
425 ..Default::default()
426 }
427 }
428
429 pub fn with_output_note(mut self, output_note: &Note) -> Self {
431 let rho_new = output_note.rho();
432 let psi_new = output_note.rseed().psi(&rho_new);
433 let rcm_new = output_note.rseed().rcm(&rho_new);
434 self.g_d_new = Value::known(output_note.recipient().g_d());
435 self.pk_d_new = Value::known(*output_note.recipient().pk_d());
436 self.psi_new = Value::known(psi_new);
437 self.rcm_new = Value::known(rcm_new);
438 self
439 }
440
441 pub fn with_notes(mut self, notes: [NoteSlotWitness; 5]) -> Self {
443 self.notes = notes;
444 self
445 }
446
447 #[cfg(test)]
452 pub(crate) fn notes_for_testing(&self) -> &[NoteSlotWitness; 5] {
453 &self.notes
454 }
455
456 pub fn with_van_comm_rand(mut self, van_comm_rand: pallas::Base) -> Self {
458 self.van_comm_rand = Value::known(van_comm_rand);
459 self
460 }
461
462 pub fn with_ballot_scaling(
464 mut self,
465 num_ballots: pallas::Base,
466 remainder: pallas::Base,
467 ) -> Self {
468 self.num_ballots = Value::known(num_ballots);
469 self.remainder = Value::known(remainder);
470 self
471 }
472}
473
474impl plonk::Circuit<pallas::Base> for Circuit {
479 type Config = Config;
480 type FloorPlanner = floor_planner::V1;
481
482 fn without_witnesses(&self) -> Self {
483 Self::default()
484 }
485
486 fn configure(meta: &mut plonk::ConstraintSystem<pallas::Base>) -> Self::Config {
487 let advices = [
505 meta.advice_column(),
506 meta.advice_column(),
507 meta.advice_column(),
508 meta.advice_column(),
509 meta.advice_column(),
510 meta.advice_column(),
511 meta.advice_column(),
512 meta.advice_column(),
513 meta.advice_column(),
514 meta.advice_column(),
515 ];
516
517 let primary = meta.instance_column();
519
520 let table_idx = meta.lookup_table_column();
522 let lookup = (
523 table_idx,
524 meta.lookup_table_column(),
525 meta.lookup_table_column(),
526 );
527
528 let lagrange_coeffs = [
531 meta.fixed_column(),
532 meta.fixed_column(),
533 meta.fixed_column(),
534 meta.fixed_column(),
535 meta.fixed_column(),
536 meta.fixed_column(),
537 meta.fixed_column(),
538 meta.fixed_column(),
539 ];
540 let rc_a = lagrange_coeffs[2..5].try_into().unwrap();
541 let rc_b = lagrange_coeffs[5..8].try_into().unwrap();
542
543 meta.enable_equality(primary);
548 for advice in advices.iter() {
549 meta.enable_equality(*advice);
550 }
551
552 meta.enable_constant(lagrange_coeffs[0]);
554
555 let q_per_note = meta.selector();
562 meta.create_gate("Per-note checks", |meta| {
563 let q_per_note = meta.query_selector(q_per_note);
564 let v = meta.query_advice(advices[0], Rotation::cur());
565 let root = meta.query_advice(advices[1], Rotation::cur());
566 let anchor = meta.query_advice(advices[2], Rotation::cur());
567 let imt_root = meta.query_advice(advices[3], Rotation::cur());
568 let nf_imt_root = meta.query_advice(advices[4], Rotation::cur());
569
570 Constraints::with_selector(
571 q_per_note,
572 [
573 ("v * (root - anchor) = 0", v * (root - anchor)),
577 ("imt_root = nf_imt_root", imt_root - nf_imt_root),
580 ],
581 )
582 });
583
584 let q_scope_select = meta.selector();
589 meta.create_gate("scope ivk select", |meta| {
590 let q = meta.query_selector(q_scope_select);
591 let is_internal = meta.query_advice(advices[0], Rotation::cur());
592 let ivk = meta.query_advice(advices[1], Rotation::cur());
593 let ivk_internal = meta.query_advice(advices[2], Rotation::cur());
594 let selected_ivk = meta.query_advice(advices[3], Rotation::cur());
595 let expected = ivk.clone() + is_internal.clone() * (ivk_internal - ivk);
597 Constraints::with_selector(
598 q,
599 [
600 ("bool_check is_internal", bool_check(is_internal)),
601 ("scope select", selected_ivk - expected),
602 ],
603 )
604 });
605
606 let imt_config = ImtNonMembershipConfig::configure(meta, &advices);
608
609 let add_config = AddChip::configure(meta, advices[7], advices[8], advices[6]);
612 let mul_config = MulChip::configure(meta, advices[7], advices[8], advices[6]);
613
614 let range_check = LookupRangeCheckConfig::configure(meta, advices[9], table_idx);
616
617 let ecc_config =
618 EccChip::<OrchardFixedBases>::configure(meta, advices, lagrange_coeffs, range_check);
619
620 let poseidon_config = PoseidonChip::configure::<poseidon::P128Pow5T3>(
621 meta,
622 advices[6..9].try_into().unwrap(),
623 advices[5],
624 rc_a,
625 rc_b,
626 );
627
628 let configure_sinsemilla_merkle =
642 |meta: &mut plonk::ConstraintSystem<pallas::Base>,
643 advice_cols: [Column<Advice>; 5],
644 witness_col: Column<Advice>,
645 lagrange_col: Column<plonk::Fixed>| {
646 let sinsemilla = SinsemillaChip::configure(
647 meta,
648 advice_cols,
649 witness_col,
650 lagrange_col,
651 lookup,
652 range_check,
653 false,
654 );
655 let merkle = MerkleChip::configure(meta, sinsemilla.clone());
656 (sinsemilla, merkle)
657 };
658
659 let (sinsemilla_config_1, merkle_config_1) = configure_sinsemilla_merkle(
660 meta,
661 advices[..5].try_into().unwrap(),
662 advices[6],
663 lagrange_coeffs[0],
664 );
665 let (sinsemilla_config_2, merkle_config_2) = configure_sinsemilla_merkle(
666 meta,
667 advices[5..].try_into().unwrap(),
668 advices[7],
669 lagrange_coeffs[1],
670 );
671
672 let commit_ivk_config = CommitIvkChip::configure(meta, advices);
674
675 let signed_note_commit_config =
677 NoteCommitChip::configure(meta, advices, sinsemilla_config_1.clone());
678
679 let new_note_commit_config =
681 NoteCommitChip::configure(meta, advices, sinsemilla_config_2.clone());
682
683 Config {
684 primary,
685 advices,
686 add_config,
687 mul_config,
688 ecc_config,
689 poseidon_config,
690 sinsemilla_config_1,
691 sinsemilla_config_2,
692 commit_ivk_config,
693 signed_note_commit_config,
694 new_note_commit_config,
695 range_check,
696 merkle_config_1,
697 merkle_config_2,
698 q_per_note,
699 q_scope_select,
700 imt_config,
701 }
702 }
703
704 #[allow(non_snake_case)]
705 fn synthesize(
706 &self,
707 config: Self::Config,
708 mut layouter: impl Layouter<pallas::Base>,
709 ) -> Result<(), plonk::Error> {
710 SinsemillaChip::load(config.sinsemilla_config_1.clone(), &mut layouter)?;
712
713 let ecc_chip = config.ecc_chip();
716
717 let ak_P: Value<pallas::Point> = self.ak.as_ref().map(|ak| ak.into());
724 let ak_P = NonIdentityPoint::new(
725 ecc_chip.clone(),
726 layouter.namespace(|| "witness ak_P"),
727 ak_P.map(|ak_P| ak_P.to_affine()),
728 )?;
729
730 let g_d_signed = NonIdentityPoint::new(
733 ecc_chip.clone(),
734 layouter.namespace(|| "witness g_d_signed"),
735 self.g_d_signed.as_ref().map(|gd| gd.to_affine()),
736 )?;
737
738 let pk_d_signed = NonIdentityPoint::new(
741 ecc_chip.clone(),
742 layouter.namespace(|| "witness pk_d_signed"),
743 self.pk_d_signed
744 .as_ref()
745 .map(|pk_d_signed| pk_d_signed.inner().to_affine()),
746 )?;
747
748 let nk = assign_free_advice(
750 layouter.namespace(|| "witness nk"),
751 config.advices[0],
752 self.nk.map(|nk| nk.inner()),
753 )?;
754
755 let rho_signed = assign_free_advice(
766 layouter.namespace(|| "witness rho_signed"),
767 config.advices[0],
768 self.rho_signed,
769 )?;
770
771 let psi_signed = assign_free_advice(
787 layouter.namespace(|| "witness psi_signed"),
788 config.advices[0],
789 self.psi_signed,
790 )?;
791
792 let cm_signed = Point::new(
794 ecc_chip.clone(),
795 layouter.namespace(|| "witness cm_signed"),
796 self.cm_signed.as_ref().map(|cm| cm.inner().to_affine()),
797 )?;
798
799 let nf_signed = derive_nullifier(
806 layouter
807 .namespace(|| "nf_signed = DeriveNullifier_nk(rho_signed, psi_signed, cm_signed)"),
808 config.poseidon_chip(),
809 config.add_chip(),
810 ecc_chip.clone(),
811 rho_signed.clone(), &psi_signed,
813 &cm_signed,
814 nk.clone(), )?;
816
817 layouter.constrain_instance(
821 nf_signed.inner().cell(),
822 config.primary,
823 NF_SIGNED_PUBLIC_OFFSET,
824 )?;
825
826 crate::circuit::spend_authority::prove_spend_authority(
840 ecc_chip.clone(),
841 layouter.namespace(|| "cond4 spend authority"),
842 self.alpha,
843 &ak_P.clone().into(),
844 config.primary,
845 RK_X_PUBLIC_OFFSET,
846 RK_Y_PUBLIC_OFFSET,
847 )?;
848
849 let ak = ak_P.extract_p().inner().clone();
861 let ak_for_internal = ak.clone();
862 let rivk = ScalarFixed::new(
863 ecc_chip.clone(),
864 layouter.namespace(|| "rivk"),
865 self.rivk.map(|rivk| rivk.inner()),
866 )?;
867 let ivk_cell = prove_address_ownership(
868 config.sinsemilla_chip_1(),
869 ecc_chip.clone(),
870 config.commit_ivk_chip(),
871 layouter.namespace(|| "cond5"),
872 "cond5",
873 ak,
874 nk.clone(),
875 rivk,
876 &g_d_signed,
877 &pk_d_signed,
878 )?;
879
880 let ivk_internal_cell = {
885 use orchard::circuit::commit_ivk::gadgets::commit_ivk;
886 let rivk_internal = ScalarFixed::new(
887 ecc_chip.clone(),
888 layouter.namespace(|| "rivk_internal"),
889 self.rivk_internal.map(|rivk| rivk.inner()),
890 )?;
891 let ivk_internal = commit_ivk(
892 config.sinsemilla_chip_1(),
893 ecc_chip.clone(),
894 config.commit_ivk_chip(),
895 layouter.namespace(|| "commit_ivk_internal"),
896 ak_for_internal,
897 nk.clone(),
898 rivk_internal,
899 )?;
900 ivk_internal.inner().clone()
901 };
902
903 {
913 let pk_d_signed_for_nc = NonIdentityPoint::new(
915 ecc_chip.clone(),
916 layouter.namespace(|| "pk_d_signed for note_commit"),
917 self.pk_d_signed
918 .map(|pk_d_signed| pk_d_signed.inner().to_affine()),
919 )?;
920 pk_d_signed_for_nc.constrain_equal(
925 layouter.namespace(|| "pk_d_signed_for_nc == pk_d_signed"),
926 &pk_d_signed,
927 )?;
928
929 let rcm_signed = ScalarFixed::new(
930 ecc_chip.clone(),
931 layouter.namespace(|| "rcm_signed"),
932 self.rcm_signed.as_ref().map(|rcm| rcm.inner()),
933 )?;
934
935 let v_signed = assign_free_advice(
943 layouter.namespace(|| "v_signed = 1"),
944 config.advices[0],
945 Value::known(NoteValue::from_raw(1)),
946 )?;
947
948 let derived_cm_signed = note_commit(
950 layouter.namespace(|| "NoteCommit_rcm_signed(g_d, pk_d, 1, rho, psi)"),
951 config.sinsemilla_chip_1(),
952 config.ecc_chip(),
953 config.note_commit_chip_signed(),
954 g_d_signed.inner(),
955 pk_d_signed_for_nc.inner(),
956 v_signed,
957 rho_signed.clone(),
958 psi_signed,
959 rcm_signed,
960 )?;
961
962 derived_cm_signed
964 .constrain_equal(layouter.namespace(|| "cm_signed integrity"), &cm_signed)?;
965 }
966
967 let van_comm_cell = layouter.assign_region(
984 || "copy van_comm from instance",
985 |mut region| {
986 region.assign_advice_from_instance(
987 || "van_comm",
988 config.primary,
989 VAN_COMM_PUBLIC_OFFSET,
990 config.advices[0],
991 0,
992 )
993 },
994 )?;
995
996 let vote_round_id_cell = layouter.assign_region(
999 || "copy vote_round_id from instance",
1000 |mut region| {
1001 region.assign_advice_from_instance(
1002 || "vote_round_id",
1003 config.primary,
1004 VOTE_ROUND_ID_PUBLIC_OFFSET,
1005 config.advices[0],
1006 0,
1007 )
1008 },
1009 )?;
1010
1011 let dom_cell = layouter.assign_region(
1016 || "copy dom from instance",
1017 |mut region| {
1018 region.assign_advice_from_instance(
1019 || "dom",
1020 config.primary,
1021 DOM_PUBLIC_OFFSET,
1022 config.advices[0],
1023 0,
1024 )
1025 },
1026 )?;
1027
1028 let gov_auth_tag_cell = assign_constant(
1029 layouter.namespace(|| "gov_auth_domain_tag constant"),
1030 config.advices[0],
1031 gov_auth_domain_tag(),
1032 )?;
1033 let derived_dom = poseidon_hash_in_circuit(
1034 config.poseidon_chip(),
1035 layouter.namespace(|| "derive dom"),
1036 "Poseidon(gov_auth_domain_tag, vote_round_id)",
1037 [gov_auth_tag_cell, vote_round_id_cell.clone()],
1038 )?;
1039 layouter.assign_region(
1040 || "dom binding",
1041 |mut region| region.constrain_equal(derived_dom.cell(), dom_cell.cell()),
1042 )?;
1043
1044 let nc_root_cell = layouter.assign_region(
1047 || "copy nc_root from instance",
1048 |mut region| {
1049 region.assign_advice_from_instance(
1050 || "nc_root",
1051 config.primary,
1052 NC_ROOT_PUBLIC_OFFSET,
1053 config.advices[0],
1054 0,
1055 )
1056 },
1057 )?;
1058
1059 let nf_imt_root_cell = layouter.assign_region(
1063 || "copy nf_imt_root from instance",
1064 |mut region| {
1065 region.assign_advice_from_instance(
1066 || "nf_imt_root",
1067 config.primary,
1068 NF_IMT_ROOT_PUBLIC_OFFSET,
1069 config.advices[0],
1070 0,
1071 )
1072 },
1073 )?;
1074
1075 let mut cmx_cells = Vec::with_capacity(5);
1093 let mut v_cells = Vec::with_capacity(5);
1094 let mut gov_null_cells = Vec::with_capacity(5);
1095
1096 for i in 0..5 {
1097 let (cmx_i, v_i, gov_null_i) = synthesize_note_slot(
1098 &config,
1099 &mut layouter,
1100 ecc_chip.clone(),
1101 &ivk_cell,
1102 &ivk_internal_cell,
1103 &nk,
1104 &dom_cell,
1105 &nc_root_cell,
1106 &nf_imt_root_cell,
1107 &self.notes[i],
1108 i,
1109 GOV_NULL_PUBLIC_OFFSETS[i],
1110 )?;
1111 cmx_cells.push(cmx_i);
1112 v_cells.push(v_i);
1113 gov_null_cells.push(gov_null_i);
1114 }
1115
1116 {
1127 let rho_binding_domain = assign_constant(
1130 layouter.namespace(|| "rho binding domain tag"),
1131 config.advices[0],
1132 domain_tags::delegation_rho_binding(),
1133 )?;
1134 let poseidon_message = [
1135 rho_binding_domain,
1136 cmx_cells[0].clone(),
1137 cmx_cells[1].clone(),
1138 cmx_cells[2].clone(),
1139 cmx_cells[3].clone(),
1140 cmx_cells[4].clone(),
1141 van_comm_cell.clone(),
1142 vote_round_id_cell.clone(),
1143 ];
1144 let poseidon_hasher = PoseidonHash::<
1145 pallas::Base,
1146 _,
1147 poseidon::P128Pow5T3,
1148 ConstantLength<8>,
1149 3,
1150 2,
1151 >::init(
1152 config.poseidon_chip(),
1153 layouter.namespace(|| "rho binding Poseidon init"),
1154 )?;
1155 let derived_rho = poseidon_hasher.hash(
1156 layouter.namespace(|| "Poseidon(domain, cmx_1..5, van_comm, vote_round_id)"),
1157 poseidon_message,
1158 )?;
1159
1160 layouter.assign_region(
1163 || "rho binding equality",
1164 |mut region| region.constrain_equal(derived_rho.cell(), rho_signed.cell()),
1165 )?;
1166 }
1167
1168 let (g_d_new_x, pk_d_new_x) = {
1187 let g_d_new = NonIdentityPoint::new(
1189 ecc_chip.clone(),
1190 layouter.namespace(|| "witness g_d_new"),
1191 self.g_d_new.as_ref().map(|gd| gd.to_affine()),
1192 )?;
1193
1194 let pk_d_new = NonIdentityPoint::new(
1196 ecc_chip.clone(),
1197 layouter.namespace(|| "witness pk_d_new"),
1198 self.pk_d_new.map(|pk_d_new| pk_d_new.inner().to_affine()),
1199 )?;
1200
1201 let rho_new = nf_signed.inner().clone();
1205
1206 let psi_new = assign_free_advice(
1208 layouter.namespace(|| "witness psi_new"),
1209 config.advices[0],
1210 self.psi_new,
1211 )?;
1212
1213 let rcm_new = ScalarFixed::new(
1214 ecc_chip.clone(),
1215 layouter.namespace(|| "rcm_new"),
1216 self.rcm_new.as_ref().map(|rcm_new| rcm_new.inner()),
1217 )?;
1218
1219 let v_new = assign_free_advice(
1224 layouter.namespace(|| "v_new = 0"),
1225 config.advices[0],
1226 Value::known(NoteValue::ZERO),
1227 )?;
1228
1229 let cm_new = note_commit(
1231 layouter.namespace(|| "NoteCommit_rcm_new(g_d_new, pk_d_new, 0, rho_new, psi_new)"),
1232 config.sinsemilla_chip_2(),
1233 config.ecc_chip(),
1234 config.note_commit_chip_new(),
1235 g_d_new.inner(),
1236 pk_d_new.inner(),
1237 v_new,
1238 rho_new,
1239 psi_new,
1240 rcm_new,
1241 )?;
1242
1243 let cmx = cm_new.extract_p();
1245
1246 layouter.constrain_instance(
1248 cmx.inner().cell(),
1249 config.primary,
1250 CMX_NEW_PUBLIC_OFFSET,
1251 )?;
1252
1253 (
1255 g_d_new.extract_p().inner().clone(),
1256 pk_d_new.extract_p().inner().clone(),
1257 )
1258 };
1259
1260 let add_chip = config.add_chip();
1266 let sum_12 = add_chip.add(layouter.namespace(|| "v_1 + v_2"), &v_cells[0], &v_cells[1])?;
1267 let sum_123 = add_chip.add(
1268 layouter.namespace(|| "(v_1 + v_2) + v_3"),
1269 &sum_12,
1270 &v_cells[2],
1271 )?;
1272 let sum_1234 = add_chip.add(
1273 layouter.namespace(|| "(v_1 + v_2 + v_3) + v_4"),
1274 &sum_123,
1275 &v_cells[3],
1276 )?;
1277 let v_total = add_chip.add(
1278 layouter.namespace(|| "(v_1 + v_2 + v_3 + v_4) + v_5"),
1279 &sum_1234,
1280 &v_cells[4],
1281 )?;
1282
1283 let num_ballots = {
1312 let num_ballots = assign_free_advice(
1314 layouter.namespace(|| "witness num_ballots"),
1315 config.advices[0],
1316 self.num_ballots,
1317 )?;
1318
1319 let remainder = assign_free_advice(
1320 layouter.namespace(|| "witness remainder"),
1321 config.advices[0],
1322 self.remainder,
1323 )?;
1324
1325 let ballot_divisor = assign_constant(
1327 layouter.namespace(|| "BALLOT_DIVISOR constant"),
1328 config.advices[0],
1329 pallas::Base::from(BALLOT_DIVISOR),
1330 )?;
1331
1332 let product = config.mul_chip().mul(
1334 layouter.namespace(|| "num_ballots * BALLOT_DIVISOR"),
1335 &num_ballots,
1336 &ballot_divisor,
1337 )?;
1338
1339 let reconstructed = config.add_chip().add(
1341 layouter.namespace(|| "product + remainder"),
1342 &product,
1343 &remainder,
1344 )?;
1345
1346 layouter.assign_region(
1348 || "num_ballots * BALLOT_DIVISOR + remainder == v_total",
1349 |mut region| region.constrain_equal(reconstructed.cell(), v_total.cell()),
1350 )?;
1351
1352 let shift_6 = assign_constant(
1357 layouter.namespace(|| "2^6 shift constant"),
1358 config.advices[0],
1359 pallas::Base::from(1u64 << 6),
1360 )?;
1361 let remainder_shifted = config.mul_chip().mul(
1362 layouter.namespace(|| "remainder * 2^6"),
1363 &remainder,
1364 &shift_6,
1365 )?;
1366 config.range_check_config().copy_check(
1367 layouter.namespace(|| "remainder * 2^6 < 2^30 (i.e. remainder < 2^24)"),
1368 remainder_shifted,
1369 3, true, )?;
1372
1373 let one = assign_constant(
1381 layouter.namespace(|| "one constant"),
1382 config.advices[0],
1383 pallas::Base::one(),
1384 )?;
1385
1386 let nb_minus_one = num_ballots.value().map(|v| *v - pallas::Base::one());
1387 let nb_minus_one = assign_free_advice(
1388 layouter.namespace(|| "witness nb_minus_one"),
1389 config.advices[0],
1390 nb_minus_one,
1391 )?;
1392
1393 let nb_recomputed = config.add_chip().add(
1394 layouter.namespace(|| "nb_minus_one + 1"),
1395 &nb_minus_one,
1396 &one,
1397 )?;
1398 layouter.assign_region(
1399 || "nb_minus_one + 1 == num_ballots",
1400 |mut region| region.constrain_equal(nb_recomputed.cell(), num_ballots.cell()),
1401 )?;
1402
1403 config.range_check_config().copy_check(
1404 layouter.namespace(|| "nb_minus_one < 2^30"),
1405 nb_minus_one,
1406 3, true, )?;
1409
1410 num_ballots
1411 };
1412
1413 {
1434 let van_comm_rand = assign_free_advice(
1435 layouter.namespace(|| "witness van_comm_rand"),
1436 config.advices[0],
1437 self.van_comm_rand,
1438 )?;
1439
1440 let domain_van = assign_constant(
1443 layouter.namespace(|| "DOMAIN_VAN constant"),
1444 config.advices[0],
1445 pallas::Base::from(van_integrity::DOMAIN_VAN),
1446 )?;
1447
1448 let max_proposal_authority = assign_constant(
1451 layouter.namespace(|| "MAX_PROPOSAL_AUTHORITY constant"),
1452 config.advices[0],
1453 pallas::Base::from(MAX_PROPOSAL_AUTHORITY),
1454 )?;
1455
1456 let derived_van_comm = van_integrity::van_integrity_poseidon(
1459 &config.poseidon_config,
1460 &mut layouter,
1461 "Gov commitment",
1462 domain_van,
1463 g_d_new_x,
1464 pk_d_new_x,
1465 num_ballots,
1466 vote_round_id_cell,
1467 max_proposal_authority,
1468 van_comm_rand,
1469 )?;
1470
1471 layouter.assign_region(
1473 || "van_comm integrity",
1474 |mut region| region.constrain_equal(derived_van_comm.cell(), van_comm_cell.cell()),
1475 )?;
1476 }
1477 Ok(())
1478 }
1479}
1480
1481#[allow(clippy::too_many_arguments, non_snake_case)]
1491fn synthesize_note_slot(
1492 config: &Config,
1493 layouter: &mut impl Layouter<pallas::Base>,
1494 ecc_chip: EccChip<OrchardFixedBases>,
1495 ivk_cell: &AssignedCell<pallas::Base, pallas::Base>,
1496 ivk_internal_cell: &AssignedCell<pallas::Base, pallas::Base>,
1497 nk_cell: &AssignedCell<pallas::Base, pallas::Base>,
1498 dom_cell: &AssignedCell<pallas::Base, pallas::Base>,
1499 nc_root_cell: &AssignedCell<pallas::Base, pallas::Base>,
1500 nf_imt_root_cell: &AssignedCell<pallas::Base, pallas::Base>,
1501 note: &NoteSlotWitness,
1502 slot: usize,
1503 gov_null_offset: usize,
1504) -> Result<
1505 (
1506 AssignedCell<pallas::Base, pallas::Base>,
1507 AssignedCell<pallas::Base, pallas::Base>,
1508 AssignedCell<pallas::Base, pallas::Base>,
1509 ),
1510 plonk::Error,
1511> {
1512 let s = slot; let g_d = NonIdentityPoint::new(
1524 ecc_chip.clone(),
1525 layouter.namespace(|| format!("note {s} witness g_d")),
1526 note.g_d.as_ref().map(|gd| gd.to_affine()),
1527 )?;
1528
1529 let pk_d = NonIdentityPoint::new(
1530 ecc_chip.clone(),
1531 layouter.namespace(|| format!("note {s} witness pk_d")),
1532 note.pk_d.as_ref().map(|pk| pk.to_affine()),
1533 )?;
1534
1535 let v = assign_free_advice(
1537 layouter.namespace(|| format!("note {s} witness v")),
1538 config.advices[0],
1539 note.v,
1540 )?;
1541
1542 let rho = assign_free_advice(
1543 layouter.namespace(|| format!("note {s} witness rho")),
1544 config.advices[0],
1545 note.rho,
1546 )?;
1547
1548 let psi = assign_free_advice(
1549 layouter.namespace(|| format!("note {s} witness psi")),
1550 config.advices[0],
1551 note.psi,
1552 )?;
1553
1554 let rcm = ScalarFixed::new(
1556 ecc_chip.clone(),
1557 layouter.namespace(|| format!("note {s} rcm")),
1558 note.rcm.as_ref().map(|rcm| rcm.inner()),
1559 )?;
1560
1561 let cm = Point::new(
1563 ecc_chip.clone(),
1564 layouter.namespace(|| format!("note {s} witness cm")),
1565 note.cm.as_ref().map(|cm| cm.to_affine()),
1566 )?;
1567
1568 let derived_cm = note_commit(
1573 layouter.namespace(|| format!("note {s} NoteCommit")),
1574 config.sinsemilla_chip_1(),
1575 config.ecc_chip(),
1576 config.note_commit_chip_signed(),
1577 g_d.inner(),
1578 pk_d.inner(),
1579 v.clone(),
1580 rho.clone(),
1581 psi.clone(),
1582 rcm,
1583 )?;
1584
1585 derived_cm.constrain_equal(layouter.namespace(|| format!("note {s} cm integrity")), &cm)?;
1586
1587 let cmx_cell = cm.extract_p().inner().clone();
1589
1590 let v_base = assign_free_advice(
1593 layouter.namespace(|| format!("note {s} witness v_base")),
1594 config.advices[0],
1595 note.v.map(|val| pallas::Base::from(val.inner())),
1596 )?;
1597 layouter.assign_region(
1598 || format!("note {s} v = v_base"),
1599 |mut region| region.constrain_equal(v.cell(), v_base.cell()),
1600 )?;
1601
1602 let is_internal = assign_free_advice(
1616 layouter.namespace(|| format!("note {s} witness is_internal")),
1617 config.advices[0],
1618 note.is_internal.map(|b| pallas::Base::from(b as u64)),
1619 )?;
1620
1621 let selected_ivk = layouter.assign_region(
1623 || format!("note {s} scope ivk select"),
1624 |mut region| {
1625 config.q_scope_select.enable(&mut region, 0)?;
1626
1627 is_internal.copy_advice(|| "is_internal", &mut region, config.advices[0], 0)?;
1628 ivk_cell.copy_advice(|| "ivk", &mut region, config.advices[1], 0)?;
1629 ivk_internal_cell.copy_advice(|| "ivk_internal", &mut region, config.advices[2], 0)?;
1630
1631 let selected = ivk_cell
1633 .value()
1634 .zip(ivk_internal_cell.value())
1635 .zip(is_internal.value())
1636 .map(|((ivk, ivk_int), flag)| {
1637 if *flag == pallas::Base::one() {
1638 *ivk_int
1639 } else {
1640 *ivk
1641 }
1642 });
1643 region.assign_advice(|| "selected_ivk", config.advices[3], 0, || selected)
1644 },
1645 )?;
1646
1647 let ivk_scalar = ScalarVar::from_base(
1649 ecc_chip.clone(),
1650 layouter.namespace(|| format!("note {s} selected_ivk to scalar")),
1651 &selected_ivk,
1652 )?;
1653
1654 let (derived_pk_d, _ivk) = g_d.mul(
1656 layouter.namespace(|| format!("note {s} [selected_ivk] g_d")),
1657 ivk_scalar,
1658 )?;
1659
1660 derived_pk_d.constrain_equal(
1662 layouter.namespace(|| format!("note {s} pk_d equality")),
1663 &pk_d,
1664 )?;
1665
1666 let real_nf = derive_nullifier(
1677 layouter.namespace(|| format!("note {s} real_nf = DeriveNullifier")),
1678 config.poseidon_chip(),
1679 config.add_chip(),
1680 ecc_chip.clone(),
1681 rho.clone(),
1682 &psi,
1683 &cm,
1684 nk_cell.clone(),
1685 )?;
1686
1687 let gov_null_tag_cell = assign_constant(
1705 layouter.namespace(|| format!("note {s} governance nullifier domain tag")),
1706 config.advices[0],
1707 gov_null_domain_tag(),
1708 )?;
1709
1710 let gov_null = poseidon_hash_in_circuit(
1712 config.poseidon_chip(),
1713 layouter.namespace(|| format!("note {s} gov_null")),
1714 "Poseidon(gov_null_domain, nk, dom, real_nf)",
1715 [
1716 gov_null_tag_cell,
1717 nk_cell.clone(),
1718 dom_cell.clone(),
1719 real_nf.inner().clone(),
1720 ],
1721 )?;
1722
1723 let gov_null_cell = gov_null.clone();
1725 layouter.constrain_instance(gov_null.cell(), config.primary, gov_null_offset)?;
1726
1727 let root = {
1737 let path = note
1739 .path
1740 .map(|typed_path| typed_path.map(|node| node.inner()));
1741 let merkle_inputs = GadgetMerklePath::construct(
1742 [config.merkle_chip_1(), config.merkle_chip_2()],
1743 OrchardHashDomains::MerkleCrh,
1744 note.pos,
1745 path,
1746 );
1747 let leaf = cm.extract_p().inner().clone();
1749 merkle_inputs
1750 .calculate_root(layouter.namespace(|| format!("note {s} Merkle path")), leaf)?
1751 };
1752
1753 let imt_root = synthesize_imt_non_membership(
1758 &config.imt_config,
1759 &config.poseidon_config,
1760 &config.ecc_config,
1761 layouter,
1762 note.imt_nf_bounds,
1763 note.imt_leaf_pos,
1764 note.imt_path,
1765 real_nf.inner(),
1766 s,
1767 )?;
1768
1769 layouter.assign_region(
1782 || format!("note {s} per-note checks"),
1783 |mut region| {
1784 config.q_per_note.enable(&mut region, 0)?;
1785
1786 v.copy_advice(|| "v", &mut region, config.advices[0], 0)?;
1787 root.copy_advice(|| "calculated root", &mut region, config.advices[1], 0)?;
1788 nc_root_cell.copy_advice(|| "nc_root (anchor)", &mut region, config.advices[2], 0)?;
1789 imt_root.copy_advice(|| "imt_root", &mut region, config.advices[3], 0)?;
1790 nf_imt_root_cell.copy_advice(|| "nf_imt_root", &mut region, config.advices[4], 0)?;
1791
1792 Ok(())
1793 },
1794 )?;
1795
1796 Ok((cmx_cell, v_base, gov_null_cell))
1801}
1802
1803#[derive(Clone, Debug)]
1820pub struct Instance {
1821 pub nf_signed: Nullifier,
1823 pub rk_x: pallas::Base,
1825 pub rk_y: pallas::Base,
1827 pub cmx_new: pallas::Base,
1829 pub van_comm: pallas::Base,
1831 pub vote_round_id: pallas::Base,
1833 pub nc_root: pallas::Base,
1837 pub nf_imt_root: pallas::Base,
1841 pub gov_null: [pallas::Base; 5],
1843 pub dom: pallas::Base,
1845}
1846
1847#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1849pub enum InstanceError {
1850 RkIsIdentity,
1852}
1853
1854impl std::fmt::Display for InstanceError {
1855 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1856 match self {
1857 InstanceError::RkIsIdentity => write!(f, "rk must not be the identity point"),
1858 }
1859 }
1860}
1861
1862impl std::error::Error for InstanceError {}
1863
1864impl Instance {
1865 pub const NUM_PUBLIC_INPUTS: usize = 14;
1867
1868 pub fn from_parts(
1881 nf_signed: Nullifier,
1882 rk: VerificationKey<SpendAuth>,
1883 cmx_new: pallas::Base,
1884 van_comm: pallas::Base,
1885 vote_round_id: pallas::Base,
1886 nc_root: pallas::Base,
1887 nf_imt_root: pallas::Base,
1888 gov_null: [pallas::Base; 5],
1889 dom: pallas::Base,
1890 ) -> Result<Self, InstanceError> {
1891 if rk.is_identity() {
1892 return Err(InstanceError::RkIsIdentity);
1893 }
1894
1895 let rk_bytes: [u8; 32] = (&rk).into();
1896 let rk_point = pallas::Point::from_bytes(&rk_bytes)
1897 .expect("VerificationKey constructor accepted on-curve bytes");
1898 let rk_affine = rk_point.to_affine();
1899 let rk_coords = rk_affine
1900 .coordinates()
1901 .expect("identity rk rejected before coordinate extraction");
1902
1903 Ok(Instance {
1904 nf_signed,
1905 rk_x: *rk_coords.x(),
1906 rk_y: *rk_coords.y(),
1907 cmx_new,
1908 van_comm,
1909 vote_round_id,
1910 nc_root,
1911 nf_imt_root,
1912 gov_null,
1913 dom,
1914 })
1915 }
1916
1917 pub fn to_halo2_instance(&self) -> Vec<vesta::Scalar> {
1924 vec![
1925 self.nf_signed.inner(),
1926 self.rk_x,
1927 self.rk_y,
1928 self.cmx_new,
1929 self.van_comm,
1930 self.vote_round_id,
1931 self.nc_root,
1932 self.nf_imt_root,
1933 self.gov_null[0],
1934 self.gov_null[1],
1935 self.gov_null[2],
1936 self.gov_null[3],
1937 self.gov_null[4],
1938 self.dom,
1939 ]
1940 }
1941}
1942
1943#[cfg(test)]
1948mod tests {
1949 use super::*;
1950 use crate::delegation::imt::{
1951 derive_nullifier_domain, gov_null_hash, ImtProofData, ImtProvider, SpacedLeafImtProvider,
1952 };
1953 use ff::Field;
1954 use halo2_proofs::dev::MockProver;
1955 use incrementalmerkletree::{Hashable, Level};
1956 use orchard::{
1957 keys::{FullViewingKey, Scope, SpendValidatingKey, SpendingKey},
1958 note::{commitment::ExtractedNoteCommitment, Note, Rho},
1959 };
1960 use pasta_curves::{arithmetic::CurveAffine, pallas};
1961 use rand::rngs::OsRng;
1962 use std::string::{String, ToString};
1963
1964 use super::K;
1966
1967 fn make_note_slot(
1969 note: &Note,
1970 auth_path: &[MerkleHashOrchard; MERKLE_DEPTH_ORCHARD],
1971 pos: u32,
1972 imt: &ImtProofData,
1973 is_internal: bool,
1974 ) -> NoteSlotWitness {
1975 let rho = note.rho();
1976 let psi = note.rseed().psi(&rho);
1977 let rcm = note.rseed().rcm(&rho);
1978 let cm = note.commitment();
1979 let recipient = note.recipient();
1980
1981 NoteSlotWitness {
1982 g_d: Value::known(recipient.g_d()),
1983 pk_d: Value::known(recipient.pk_d().inner()),
1984 v: Value::known(note.value()),
1985 rho: Value::known(rho.into_inner()),
1986 psi: Value::known(psi),
1987 rcm: Value::known(rcm),
1988 cm: Value::known(cm.inner()),
1989 path: Value::known(*auth_path),
1990 pos: Value::known(pos),
1991 imt_nf_bounds: Value::known(imt.nf_bounds),
1992 imt_leaf_pos: Value::known(imt.leaf_pos),
1993 imt_path: Value::known(imt.path),
1994 is_internal: Value::known(is_internal),
1995 }
1996 }
1997
1998 struct TestData {
2000 circuit: Circuit,
2001 instance: Instance,
2002 nk: pallas::Base,
2003 note_nullifiers: [pallas::Base; 5],
2004 }
2005
2006 fn make_test_data_with_alpha(alpha_override: Option<pallas::Scalar>) -> TestData {
2008 let mut rng = OsRng;
2009
2010 let sk = SpendingKey::random(&mut rng);
2011 let fvk: FullViewingKey = (&sk).into();
2012 let output_recipient = fvk.address_at(1u32, Scope::External);
2013
2014 let nk_val = fvk.nk().inner();
2016 let ak: SpendValidatingKey = fvk.clone().into();
2017
2018 let vote_round_id = pallas::Base::random(&mut rng);
2019 let dom = derive_nullifier_domain(vote_round_id);
2020 let van_comm_rand = pallas::Base::random(&mut rng);
2021
2022 let imt_provider = SpacedLeafImtProvider::new();
2024 let nf_imt_root = imt_provider.root();
2025
2026 let recipient = fvk.address_at(0u32, Scope::External);
2028 let note_value = NoteValue::from_raw(13_000_000);
2029 let (_, _, dummy_parent) = Note::dummy(&mut rng, None);
2030 let real_note = Note::new(
2031 recipient,
2032 note_value,
2033 Rho::from_nf_old(dummy_parent.nullifier(&fvk)),
2034 &mut rng,
2035 );
2036
2037 let cmx_real_e = ExtractedNoteCommitment::from(real_note.commitment());
2039 let cmx_real = cmx_real_e.inner();
2040 let empty_leaf = MerkleHashOrchard::empty_leaf();
2041 let leaves = [
2042 MerkleHashOrchard::from_cmx(&cmx_real_e),
2043 empty_leaf,
2044 empty_leaf,
2045 empty_leaf,
2046 ];
2047 let l1_0 = MerkleHashOrchard::combine(Level::from(0), &leaves[0], &leaves[1]);
2048 let l1_1 = MerkleHashOrchard::combine(Level::from(0), &leaves[2], &leaves[3]);
2049 let l2_0 = MerkleHashOrchard::combine(Level::from(1), &l1_0, &l1_1);
2050
2051 let mut current = l2_0;
2052 for level in 2..MERKLE_DEPTH_ORCHARD {
2053 let sibling = MerkleHashOrchard::empty_root(Level::from(level as u8));
2054 current = MerkleHashOrchard::combine(Level::from(level as u8), ¤t, &sibling);
2055 }
2056 let nc_root = current.inner();
2057
2058 let mut auth_path_0 = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
2059 auth_path_0[0] = leaves[1];
2060 auth_path_0[1] = l1_1;
2061 for level in 2..MERKLE_DEPTH_ORCHARD {
2062 auth_path_0[level] = MerkleHashOrchard::empty_root(Level::from(level as u8));
2063 }
2064 let real_nf = real_note.nullifier(&fvk);
2066 let imt_0 = imt_provider.non_membership_proof(real_nf.inner()).unwrap();
2067 let gov_null_0 = gov_null_hash(nk_val, dom, real_nf.inner());
2068
2069 let slot_0 = make_note_slot(&real_note, &auth_path_0, 0u32, &imt_0, false);
2070
2071 let mut note_slots = vec![slot_0];
2076 let mut cmx_values = vec![cmx_real];
2077 let mut gov_nulls = vec![gov_null_0];
2078 let mut note_nullifiers = vec![real_nf.inner()];
2079
2080 for i in 1..5u32 {
2081 let padding = crate::delegation::builder::build_padding_slot_for_testing(
2082 i as usize,
2083 (i - 1) as usize,
2084 &fvk,
2085 &ak,
2086 dom,
2087 &imt_provider,
2088 &mut rng,
2089 )
2090 .unwrap();
2091 note_slots.push(padding.witness);
2092 cmx_values.push(padding.cmx);
2093 gov_nulls.push(padding.gov_null);
2094 note_nullifiers.push(padding.real_nf);
2095 }
2096
2097 let notes: [NoteSlotWitness; 5] = note_slots.try_into().unwrap();
2098 let note_nullifiers: [pallas::Base; 5] = note_nullifiers.try_into().unwrap();
2099
2100 let v_total_u64: u64 = 13_000_000;
2103 let num_ballots_u64 = v_total_u64 / BALLOT_DIVISOR;
2104 let remainder_u64 = v_total_u64 % BALLOT_DIVISOR;
2105 let num_ballots_field = pallas::Base::from(num_ballots_u64);
2106
2107 let g_d_new_x = *output_recipient
2109 .g_d()
2110 .to_affine()
2111 .coordinates()
2112 .unwrap()
2113 .x();
2114 let pk_d_new_x = *output_recipient
2115 .pk_d()
2116 .inner()
2117 .to_affine()
2118 .coordinates()
2119 .unwrap()
2120 .x();
2121 let van_comm = van_commitment_hash(
2122 g_d_new_x,
2123 pk_d_new_x,
2124 num_ballots_field,
2125 vote_round_id,
2126 van_comm_rand,
2127 );
2128
2129 let rho = rho_binding_hash(
2131 cmx_values[0],
2132 cmx_values[1],
2133 cmx_values[2],
2134 cmx_values[3],
2135 cmx_values[4],
2136 van_comm,
2137 vote_round_id,
2138 );
2139
2140 let sender_address = fvk.address_at(0u32, Scope::External);
2142 let signed_note = Note::new(
2143 sender_address,
2144 NoteValue::from_raw(1),
2145 Rho::from_nf_old(Nullifier::from_inner(rho)),
2146 &mut rng,
2147 );
2148 let nf_signed = signed_note.nullifier(&fvk);
2149
2150 let output_note = Note::new(
2152 output_recipient,
2153 NoteValue::ZERO,
2154 Rho::from_nf_old(nf_signed),
2155 &mut rng,
2156 );
2157 let cmx_new = ExtractedNoteCommitment::from(output_note.commitment()).inner();
2158
2159 let alpha = alpha_override.unwrap_or_else(|| pallas::Scalar::random(&mut rng));
2160 let rk = ak.randomize(&alpha);
2161
2162 let circuit = Circuit::from_note_unchecked(&fvk, &signed_note, alpha)
2163 .with_output_note(&output_note)
2164 .with_notes(notes)
2165 .with_van_comm_rand(van_comm_rand)
2166 .with_ballot_scaling(
2167 pallas::Base::from(num_ballots_u64),
2168 pallas::Base::from(remainder_u64),
2169 );
2170
2171 let instance = Instance::from_parts(
2172 nf_signed,
2173 rk,
2174 cmx_new,
2175 van_comm,
2176 vote_round_id,
2177 nc_root,
2178 nf_imt_root,
2179 [
2180 gov_nulls[0],
2181 gov_nulls[1],
2182 gov_nulls[2],
2183 gov_nulls[3],
2184 gov_nulls[4],
2185 ],
2186 dom,
2187 )
2188 .expect("test rk must be non-identity");
2189
2190 TestData {
2191 circuit,
2192 instance,
2193 nk: nk_val,
2194 note_nullifiers,
2195 }
2196 }
2197
2198 fn make_test_data() -> TestData {
2199 make_test_data_with_alpha(None)
2200 }
2201
2202 fn assert_rejects(prover: MockProver<pallas::Base>) {
2203 prover.verify().expect_err("malicious bundle must reject");
2204 }
2205
2206 #[test]
2207 fn rho_binding_hash_is_domain_separated_from_legacy_shape() {
2208 let cmx_1 = pallas::Base::from(1u64);
2209 let cmx_2 = pallas::Base::from(2u64);
2210 let cmx_3 = pallas::Base::from(3u64);
2211 let cmx_4 = pallas::Base::from(4u64);
2212 let cmx_5 = pallas::Base::from(5u64);
2213 let van_comm = pallas::Base::from(6u64);
2214 let vote_round_id = pallas::Base::from(7u64);
2215
2216 let tagged = rho_binding_hash(cmx_1, cmx_2, cmx_3, cmx_4, cmx_5, van_comm, vote_round_id);
2217 let legacy_untagged =
2218 poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<7>, 3, 2>::init().hash([
2219 cmx_1,
2220 cmx_2,
2221 cmx_3,
2222 cmx_4,
2223 cmx_5,
2224 van_comm,
2225 vote_round_id,
2226 ]);
2227 let manual_tagged =
2228 poseidon::Hash::<_, poseidon::P128Pow5T3, ConstantLength<8>, 3, 2>::init().hash([
2229 domain_tags::delegation_rho_binding(),
2230 cmx_1,
2231 cmx_2,
2232 cmx_3,
2233 cmx_4,
2234 cmx_5,
2235 van_comm,
2236 vote_round_id,
2237 ]);
2238
2239 assert_ne!(tagged, legacy_untagged);
2240 assert_eq!(tagged, manual_tagged);
2241 }
2242
2243 #[test]
2244 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2245 fn happy_path() {
2246 let t = make_test_data();
2247 let pi = t.instance.to_halo2_instance();
2248
2249 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2250 assert_eq!(prover.verify(), Ok(()));
2251 }
2252
2253 #[test]
2257 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2258 fn spend_authority_alpha_zero_is_accepted_by_relation() {
2259 let t = make_test_data_with_alpha(Some(pallas::Scalar::zero()));
2260 let pi = t.instance.to_halo2_instance();
2261
2262 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2263 assert_eq!(prover.verify(), Ok(()));
2264 }
2265
2266 #[test]
2267 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2268 fn wrong_nf_fails() {
2269 let t = make_test_data();
2270 let mut instance = t.instance.clone();
2271 instance.nf_signed = Nullifier::from_inner(pallas::Base::random(&mut OsRng));
2272
2273 let pi = instance.to_halo2_instance();
2274 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2275 assert!(prover.verify().is_err());
2276 }
2277
2278 #[test]
2279 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2280 fn wrong_rk_fails() {
2281 let mut rng = OsRng;
2282 let t = make_test_data();
2283
2284 let sk2 = SpendingKey::random(&mut rng);
2285 let fvk2: FullViewingKey = (&sk2).into();
2286 let ak2: SpendValidatingKey = fvk2.into();
2287 let wrong_rk = ak2.randomize(&pallas::Scalar::random(&mut rng));
2288
2289 let mut instance = t.instance.clone();
2290 let rk_bytes: [u8; 32] = (&wrong_rk).into();
2291 let wrong_rk_point = pallas::Point::from_bytes(&rk_bytes)
2292 .unwrap()
2293 .to_affine()
2294 .coordinates()
2295 .unwrap();
2296 instance.rk_x = *wrong_rk_point.x();
2297 instance.rk_y = *wrong_rk_point.y();
2298
2299 let pi = instance.to_halo2_instance();
2300 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2301 assert!(prover.verify().is_err());
2302 }
2303
2304 #[test]
2305 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2306 fn wrong_gov_null_fails() {
2307 let t = make_test_data();
2308 let mut instance = t.instance.clone();
2309 instance.gov_null[0] = pallas::Base::random(&mut OsRng);
2310
2311 let pi = instance.to_halo2_instance();
2312 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2313 assert!(prover.verify().is_err());
2314 }
2315
2316 #[test]
2317 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2318 fn wrong_nc_root_fails() {
2319 let t = make_test_data();
2320 let mut instance = t.instance.clone();
2321 instance.nc_root = pallas::Base::random(&mut OsRng);
2322
2323 let pi = instance.to_halo2_instance();
2324 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2325 assert!(prover.verify().is_err());
2326 }
2327
2328 #[test]
2329 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2330 fn wrong_imt_root_fails() {
2331 let t = make_test_data();
2332 let mut instance = t.instance.clone();
2333 instance.nf_imt_root = pallas::Base::random(&mut OsRng);
2334
2335 let pi = instance.to_halo2_instance();
2336 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2337 assert!(prover.verify().is_err());
2338 }
2339
2340 #[test]
2341 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2342 fn wrong_van_comm_fails() {
2343 let t = make_test_data();
2344 let mut instance = t.instance.clone();
2345 instance.van_comm = pallas::Base::random(&mut OsRng);
2346
2347 let pi = instance.to_halo2_instance();
2348 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2349 assert!(prover.verify().is_err());
2350 }
2351
2352 #[test]
2353 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2354 fn wrong_vote_round_id_fails() {
2355 let t = make_test_data();
2356 let mut instance = t.instance.clone();
2357 instance.vote_round_id = pallas::Base::random(&mut OsRng);
2358
2359 let pi = instance.to_halo2_instance();
2360 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2361 assert!(prover.verify().is_err());
2362 }
2363
2364 #[test]
2365 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2366 fn wrong_dom_with_matching_gov_nulls_fails() {
2367 let t = make_test_data();
2368 let mut instance = t.instance.clone();
2369 let mut wrong_dom = pallas::Base::random(&mut OsRng);
2370 while wrong_dom == t.instance.dom {
2371 wrong_dom = pallas::Base::random(&mut OsRng);
2372 }
2373
2374 instance.dom = wrong_dom;
2377 for (slot, real_nf) in t.note_nullifiers.iter().enumerate() {
2378 instance.gov_null[slot] = gov_null_hash(t.nk, instance.dom, *real_nf);
2379 }
2380
2381 let pi = instance.to_halo2_instance();
2382 let prover = MockProver::run(K, &t.circuit, vec![pi]).unwrap();
2383 assert!(prover.verify().is_err());
2384 }
2385
2386 #[test]
2392 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2393 fn malicious_output_recipient_substitution_fails() {
2394 let mut rng = OsRng;
2395 let t = make_test_data();
2396 let mut circuit = t.circuit;
2397 let pi = t.instance.to_halo2_instance();
2398
2399 let attacker_sk = SpendingKey::random(&mut rng);
2400 let attacker_fvk: FullViewingKey = (&attacker_sk).into();
2401 let attacker_recipient = attacker_fvk.address_at(0u32, Scope::External);
2402 let attacker_output = Note::new(
2403 attacker_recipient,
2404 NoteValue::ZERO,
2405 Rho::from_nf_old(t.instance.nf_signed),
2406 &mut rng,
2407 );
2408 circuit = circuit.with_output_note(&attacker_output);
2409
2410 let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
2411 assert_rejects(prover);
2412 }
2413
2414 #[test]
2418 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2419 fn malicious_is_internal_flag_flip_fails() {
2420 let t = make_test_data();
2421 let mut circuit = t.circuit;
2422 let pi = t.instance.to_halo2_instance();
2423
2424 circuit.notes[0].is_internal = Value::known(true);
2425
2426 let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
2427 assert_rejects(prover);
2428 }
2429
2430 #[test]
2435 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2436 fn malicious_note_psi_substitution_fails() {
2437 let t = make_test_data();
2438 let mut circuit = t.circuit;
2439 let pi = t.instance.to_halo2_instance();
2440
2441 circuit.notes[0].psi = Value::known(pallas::Base::random(&mut OsRng));
2442
2443 let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
2444 assert_rejects(prover);
2445 }
2446
2447 #[test]
2452 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2453 fn malicious_note_rcm_substitution_fails() {
2454 let mut rng = OsRng;
2455 let t = make_test_data();
2456 let mut circuit = t.circuit;
2457 let pi = t.instance.to_halo2_instance();
2458
2459 let replacement_sk = SpendingKey::random(&mut rng);
2460 let replacement_fvk = FullViewingKey::from(&replacement_sk);
2461 let (_, _, dummy_parent) = Note::dummy(&mut rng, None);
2462 let rho = Rho::from_nf_old(dummy_parent.nullifier(&replacement_fvk));
2463 let replacement_note = Note::new(
2464 replacement_fvk.address_at(0u32, Scope::External),
2465 NoteValue::ZERO,
2466 rho,
2467 &mut rng,
2468 );
2469 circuit.notes[0].rcm = Value::known(replacement_note.rseed().rcm(&replacement_note.rho()));
2470
2471 let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
2472 assert_rejects(prover);
2473 }
2474
2475 #[test]
2479 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2480 fn malicious_note_value_inflation_fails() {
2481 let t = make_test_data();
2482 let mut circuit = t.circuit;
2483 let pi = t.instance.to_halo2_instance();
2484
2485 circuit.notes[0].v = Value::known(NoteValue::from_raw(26_000_000));
2486 circuit =
2487 circuit.with_ballot_scaling(pallas::Base::from(2u64), pallas::Base::from(1_000_000u64));
2488
2489 let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
2490 assert_rejects(prover);
2491 }
2492
2493 #[test]
2494 fn instance_to_halo2_roundtrip() {
2495 let t = make_test_data();
2496 let pi = t.instance.to_halo2_instance();
2497 assert_eq!(pi.len(), 14, "Expected exactly 14 public inputs");
2498 assert_eq!(pi[NF_SIGNED_PUBLIC_OFFSET], t.instance.nf_signed.inner());
2499 assert_eq!(pi[RK_X_PUBLIC_OFFSET], t.instance.rk_x);
2500 assert_eq!(pi[RK_Y_PUBLIC_OFFSET], t.instance.rk_y);
2501 assert_eq!(pi[CMX_NEW_PUBLIC_OFFSET], t.instance.cmx_new);
2502 assert_eq!(pi[VAN_COMM_PUBLIC_OFFSET], t.instance.van_comm);
2503 assert_eq!(pi[NC_ROOT_PUBLIC_OFFSET], t.instance.nc_root);
2504 assert_eq!(pi[NF_IMT_ROOT_PUBLIC_OFFSET], t.instance.nf_imt_root);
2505 assert_eq!(pi[GOV_NULL_1_PUBLIC_OFFSET], t.instance.gov_null[0]);
2506 assert_eq!(pi[DOM_PUBLIC_OFFSET], t.instance.dom);
2507 }
2508
2509 #[test]
2510 fn instance_from_parts_rejects_identity_rk() {
2511 let t = make_test_data();
2512 let identity_rk = VerificationKey::<SpendAuth>::try_from([0u8; 32])
2513 .expect("RedPallas accepts identity verification keys");
2514
2515 let err = Instance::from_parts(
2516 t.instance.nf_signed,
2517 identity_rk,
2518 t.instance.cmx_new,
2519 t.instance.van_comm,
2520 t.instance.vote_round_id,
2521 t.instance.nc_root,
2522 t.instance.nf_imt_root,
2523 t.instance.gov_null,
2524 t.instance.dom,
2525 )
2526 .unwrap_err();
2527
2528 assert_eq!(err, InstanceError::RkIsIdentity);
2529 }
2530
2531 #[test]
2532 #[ignore = "long-running Halo2 keygen/layout test; run with `cargo test -- --ignored`"]
2533 fn default_circuit_shape() {
2534 let t = make_test_data();
2535 let empty = plonk::Circuit::without_witnesses(&t.circuit);
2536 let params = halo2_proofs::poly::commitment::Params::<vesta::Affine>::new(K);
2537 let vk = halo2_proofs::plonk::keygen_vk(¶ms, &empty);
2538 assert!(
2539 vk.is_ok(),
2540 "keygen_vk must succeed on without_witnesses circuit"
2541 );
2542 }
2543
2544 #[test]
2548 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2549 fn fake_real_note_nonzero_value_fails() {
2550 let mut rng = OsRng;
2551 let t = make_test_data();
2552 let mut circuit = t.circuit;
2553 let pi = t.instance.to_halo2_instance();
2554
2555 let sk2 = SpendingKey::random(&mut rng);
2558 let fvk2: FullViewingKey = (&sk2).into();
2559 let addr2 = fvk2.address_at(0u32, Scope::External);
2560 let (_, _, dummy_parent) = Note::dummy(&mut rng, None);
2561 let fake_note = Note::new(
2562 addr2,
2563 NoteValue::from_raw(100), Rho::from_nf_old(dummy_parent.nullifier(&fvk2)),
2565 &mut rng,
2566 );
2567
2568 let imt_provider = SpacedLeafImtProvider::new();
2569 let fake_nf = fake_note.nullifier(&fvk2);
2570 let fake_imt = imt_provider.non_membership_proof(fake_nf.inner()).unwrap();
2571
2572 let dummy_auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
2574 let fake_slot = make_note_slot(&fake_note, &dummy_auth_path, 0u32, &fake_imt, false);
2576
2577 circuit.notes[1] = fake_slot;
2579
2580 let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
2581 assert!(prover.verify().is_err());
2584 }
2585
2586 #[test]
2591 #[ignore = "long-running Halo2 circuit test; run with `cargo test -- --ignored`"]
2592 fn different_ivk_per_note_fails() {
2593 let mut rng = OsRng;
2594 let t = make_test_data();
2595 let mut circuit = t.circuit;
2596 let pi = t.instance.to_halo2_instance();
2597
2598 let sk2 = SpendingKey::random(&mut rng);
2603 let fvk2: FullViewingKey = (&sk2).into();
2604 let addr2 = fvk2.address_at(100u32, Scope::External);
2605 let (_, _, dummy_parent) = Note::dummy(&mut rng, None);
2606 let foreign_note = Note::new(
2607 addr2,
2608 NoteValue::ZERO,
2609 Rho::from_nf_old(dummy_parent.nullifier(&fvk2)),
2610 &mut rng,
2611 );
2612
2613 let imt_provider = SpacedLeafImtProvider::new();
2614 let foreign_nf = foreign_note.nullifier(&fvk2);
2615 let foreign_imt = imt_provider
2616 .non_membership_proof(foreign_nf.inner())
2617 .unwrap();
2618
2619 let dummy_auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
2620 let foreign_slot =
2623 make_note_slot(&foreign_note, &dummy_auth_path, 0u32, &foreign_imt, false);
2624
2625 circuit.notes[1] = foreign_slot;
2626
2627 let prover = MockProver::run(K, &circuit, vec![pi]).unwrap();
2628 assert!(prover.verify().is_err());
2632 }
2633
2634 use std::collections::BTreeMap;
2639
2640 use halo2_proofs::plonk::{Any, Assigned, Assignment, Column, Error, Fixed, FloorPlanner};
2641
2642 struct RegionInfo {
2643 name: String,
2644 min_row: Option<usize>,
2645 max_row: Option<usize>,
2646 }
2647
2648 impl RegionInfo {
2649 fn track_row(&mut self, row: usize) {
2650 self.min_row = Some(self.min_row.map_or(row, |m| m.min(row)));
2651 self.max_row = Some(self.max_row.map_or(row, |m| m.max(row)));
2652 }
2653
2654 fn row_count(&self) -> usize {
2655 match (self.min_row, self.max_row) {
2656 (Some(lo), Some(hi)) => hi - lo + 1,
2657 _ => 0,
2658 }
2659 }
2660 }
2661
2662 struct RegionTracker {
2663 regions: Vec<RegionInfo>,
2664 current_region: Option<usize>,
2665 total_rows: usize,
2666 namespace_stack: Vec<String>,
2667 }
2668
2669 impl RegionTracker {
2670 fn new() -> Self {
2671 Self {
2672 regions: Vec::new(),
2673 current_region: None,
2674 total_rows: 0,
2675 namespace_stack: Vec::new(),
2676 }
2677 }
2678
2679 fn current_prefix(&self) -> String {
2680 if self.namespace_stack.is_empty() {
2681 String::new()
2682 } else {
2683 format!("{}/", self.namespace_stack.join("/"))
2684 }
2685 }
2686 }
2687
2688 impl Assignment<pallas::Base> for RegionTracker {
2689 fn enter_region<NR, N>(&mut self, name_fn: N)
2690 where
2691 NR: Into<String>,
2692 N: FnOnce() -> NR,
2693 {
2694 let idx = self.regions.len();
2695 let raw_name: String = name_fn().into();
2696 let prefixed = format!("{}{}", self.current_prefix(), raw_name);
2697 self.regions.push(RegionInfo {
2698 name: prefixed,
2699 min_row: None,
2700 max_row: None,
2701 });
2702 self.current_region = Some(idx);
2703 }
2704
2705 fn exit_region(&mut self) {
2706 self.current_region = None;
2707 }
2708
2709 fn enable_selector<A, AR>(
2710 &mut self,
2711 _: A,
2712 _selector: &Selector,
2713 row: usize,
2714 ) -> Result<(), Error>
2715 where
2716 A: FnOnce() -> AR,
2717 AR: Into<String>,
2718 {
2719 if let Some(idx) = self.current_region {
2720 self.regions[idx].track_row(row);
2721 }
2722 if row + 1 > self.total_rows {
2723 self.total_rows = row + 1;
2724 }
2725 Ok(())
2726 }
2727
2728 fn query_instance(
2729 &self,
2730 _column: Column<InstanceColumn>,
2731 _row: usize,
2732 ) -> Result<Value<pallas::Base>, Error> {
2733 Ok(Value::unknown())
2734 }
2735
2736 fn assign_advice<V, VR, A, AR>(
2737 &mut self,
2738 _: A,
2739 _column: Column<Advice>,
2740 row: usize,
2741 _to: V,
2742 ) -> Result<(), Error>
2743 where
2744 V: FnOnce() -> Value<VR>,
2745 VR: Into<Assigned<pallas::Base>>,
2746 A: FnOnce() -> AR,
2747 AR: Into<String>,
2748 {
2749 if let Some(idx) = self.current_region {
2750 self.regions[idx].track_row(row);
2751 }
2752 if row + 1 > self.total_rows {
2753 self.total_rows = row + 1;
2754 }
2755 Ok(())
2756 }
2757
2758 fn assign_fixed<V, VR, A, AR>(
2759 &mut self,
2760 _: A,
2761 _column: Column<Fixed>,
2762 row: usize,
2763 _to: V,
2764 ) -> Result<(), Error>
2765 where
2766 V: FnOnce() -> Value<VR>,
2767 VR: Into<Assigned<pallas::Base>>,
2768 A: FnOnce() -> AR,
2769 AR: Into<String>,
2770 {
2771 if let Some(idx) = self.current_region {
2772 self.regions[idx].track_row(row);
2773 }
2774 if row + 1 > self.total_rows {
2775 self.total_rows = row + 1;
2776 }
2777 Ok(())
2778 }
2779
2780 fn copy(
2781 &mut self,
2782 _left_column: Column<Any>,
2783 _left_row: usize,
2784 _right_column: Column<Any>,
2785 _right_row: usize,
2786 ) -> Result<(), Error> {
2787 Ok(())
2788 }
2789
2790 fn fill_from_row(
2791 &mut self,
2792 _column: Column<Fixed>,
2793 _row: usize,
2794 _to: Value<Assigned<pallas::Base>>,
2795 ) -> Result<(), Error> {
2796 Ok(())
2797 }
2798
2799 fn push_namespace<NR, N>(&mut self, name_fn: N)
2800 where
2801 NR: Into<String>,
2802 N: FnOnce() -> NR,
2803 {
2804 self.namespace_stack.push(name_fn().into());
2805 }
2806
2807 fn pop_namespace(&mut self, _: Option<String>) {
2808 self.namespace_stack.pop();
2809 }
2810 }
2811
2812 #[test]
2813 #[ignore = "long-running row-budget diagnostic; run with `cargo test cost_breakdown -- --ignored --nocapture`"]
2814 fn cost_breakdown() {
2815 let mut cs = plonk::ConstraintSystem::default();
2817 let config = <Circuit as plonk::Circuit<pallas::Base>>::configure(&mut cs);
2818
2819 let constants_col = cs.fixed_column();
2825 let circuit = Circuit::default();
2826 let mut tracker = RegionTracker::new();
2827 floor_planner::V1::synthesize(&mut tracker, &circuit, config, vec![constants_col]).unwrap();
2828
2829 let mut regions: Vec<_> = tracker
2831 .regions
2832 .iter()
2833 .filter(|r| r.row_count() > 0)
2834 .collect();
2835 regions.sort_by(|a, b| b.row_count().cmp(&a.row_count()));
2836
2837 std::println!(
2838 "\n=== Delegation Circuit Cost Breakdown (K={}, {} total rows) ===",
2839 K,
2840 1u64 << K
2841 );
2842 std::println!("Total rows used: {}\n", tracker.total_rows);
2843
2844 std::println!("Per-region (sorted by cost):");
2845 for r in ®ions {
2846 std::println!(
2847 " {:60} {:>6} rows (rows {}-{})",
2848 r.name,
2849 r.row_count(),
2850 r.min_row.unwrap(),
2851 r.max_row.unwrap()
2852 );
2853 }
2854
2855 std::println!("\nAggregated by top-level condition:");
2857 let mut aggregated: BTreeMap<String, (usize, usize)> = BTreeMap::new();
2858 for r in &tracker.regions {
2859 if r.row_count() == 0 {
2860 continue;
2861 }
2862 let key = if r.name.starts_with("note ")
2863 && r.name
2864 .as_bytes()
2865 .get(5)
2866 .map_or(false, |b| b.is_ascii_digit())
2867 {
2868 if let Some(slash) = r.name.find('/') {
2869 let rest = &r.name[slash + 1..];
2870 let top = rest.split('/').next().unwrap_or(rest);
2871 let top = if top.starts_with("MerkleCRH(") {
2872 "Merkle path (Sinsemilla)"
2873 } else if top.starts_with("Poseidon(left, right) level") {
2874 "IMT Poseidon path"
2875 } else if top.starts_with("imt swap level") {
2876 "IMT swap"
2877 } else {
2878 top
2879 };
2880 format!("Per-note: {}", top)
2881 } else {
2882 r.name.clone()
2883 }
2884 } else {
2885 let top = r.name.split('/').next().unwrap_or(&r.name);
2886 top.to_string()
2887 };
2888 let entry = aggregated.entry(key).or_insert((0, 0));
2889 entry.0 += r.row_count();
2890 entry.1 += 1;
2891 }
2892 let mut agg_sorted: Vec<_> = aggregated.into_iter().collect();
2893 agg_sorted.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
2894 for (name, (total, count)) in &agg_sorted {
2895 if *count > 1 {
2896 std::println!(
2897 " {:60} {:>6} rows ({} x{})",
2898 name,
2899 total,
2900 total / count,
2901 count
2902 );
2903 } else {
2904 std::println!(" {:60} {:>6} rows", name, total);
2905 }
2906 }
2907 std::println!();
2908 }
2909
2910 #[test]
2919 #[ignore = "long-running row-budget diagnostic; run with `cargo test row_budget -- --ignored --nocapture`"]
2920 fn row_budget() {
2921 use halo2_proofs::dev::CircuitCost;
2922 use pasta_curves::vesta;
2923 use std::println;
2924
2925 let t = make_test_data();
2926
2927 let cost = CircuitCost::<vesta::Point, _>::measure(K, &t.circuit);
2928 let debug = format!("{cost:?}");
2929
2930 let extract = |field: &str| -> usize {
2931 let prefix = format!("{field}: ");
2932 debug
2933 .split(&prefix)
2934 .nth(1)
2935 .and_then(|s| s.split([',', ' ', '}']).next())
2936 .and_then(|n| n.parse().ok())
2937 .unwrap_or(0)
2938 };
2939
2940 let max_rows = extract("max_rows");
2941 let max_advice_rows = extract("max_advice_rows");
2942 let max_fixed_rows = extract("max_fixed_rows");
2943 let total_available = 1usize << K;
2944
2945 println!("=== delegation circuit row budget (K={K}) ===");
2946 println!(" max_rows (floor-planner high-water mark): {max_rows}");
2947 println!(" max_advice_rows: {max_advice_rows}");
2948 println!(" max_fixed_rows: {max_fixed_rows}");
2949 println!(" 2^K (total available rows): {total_available}");
2950 println!(
2951 " headroom: {}",
2952 total_available.saturating_sub(max_rows)
2953 );
2954 println!(
2955 " utilisation: {:.1}%",
2956 100.0 * max_rows as f64 / total_available as f64
2957 );
2958 println!();
2959 println!(" Full debug: {debug}");
2960
2961 let cost_default = CircuitCost::<vesta::Point, _>::measure(K, &Circuit::default());
2964 let debug_default = format!("{cost_default:?}");
2965 let max_rows_default = debug_default
2966 .split("max_rows: ")
2967 .nth(1)
2968 .and_then(|s| s.split([',', ' ', '}']).next())
2969 .and_then(|n| n.parse::<usize>().ok())
2970 .unwrap_or(0);
2971 if max_rows_default == max_rows {
2972 println!(
2973 " Witness-independence: PASS \
2974 (Circuit::default() max_rows={max_rows_default} == filled max_rows={max_rows})"
2975 );
2976 } else {
2977 println!(
2978 " Witness-independence: FAIL \
2979 (Circuit::default() max_rows={max_rows_default} != filled max_rows={max_rows}) \
2980 — row count depends on witness values!"
2981 );
2982 }
2983
2984 println!(" MERKLE_DEPTH_ORCHARD (circuit constant): {MERKLE_DEPTH_ORCHARD}");
2985 println!(" IMT_DEPTH (circuit constant): {IMT_DEPTH}");
2986
2987 for probe_k in 11u32..=K {
2989 let t = make_test_data();
2990 match MockProver::run(probe_k, &t.circuit, vec![t.instance.to_halo2_instance()]) {
2991 Err(_) => {
2992 println!(" K={probe_k}: not enough rows (synthesizer rejected)");
2993 continue;
2994 }
2995 Ok(p) => match p.verify() {
2996 Ok(()) => {
2997 println!(" Minimum viable K: {probe_k} (2^{probe_k} = {} rows, {:.1}% headroom)",
2998 1usize << probe_k,
2999 100.0 * (1.0 - max_rows as f64 / (1usize << probe_k) as f64));
3000 break;
3001 }
3002 Err(_) => println!(" K={probe_k}: too small"),
3003 },
3004 }
3005 }
3006 }
3007}