1use alloc::vec::Vec;
9use group::Curve;
10use halo2_proofs::circuit::Value;
11use pasta_curves::{arithmetic::CurveAffine, pallas};
12use rand::RngCore;
13
14use orchard::{
15 keys::{FullViewingKey, Scope, SpendValidatingKey},
16 note::{commitment::ExtractedNoteCommitment, nullifier::Nullifier, Note, RandomSeed, Rho},
17 spec::NonIdentityPallasPoint,
18 tree::MerklePath,
19 value::NoteValue,
20};
21
22use super::{
23 circuit::{self, van_commitment_hash, rho_binding_hash, NoteSlotWitness},
24 imt::{derive_nullifier_domain, gov_null_hash, ImtProofData, ImtProvider},
25};
26
27#[derive(Clone, Debug)]
29pub struct PaddedNoteData {
30 pub rho: [u8; 32],
32 pub rseed: [u8; 32],
34}
35
36#[derive(Clone, Debug)]
40pub struct PrecomputedRandomness {
41 pub padded_notes: Vec<PaddedNoteData>,
43 pub rseed_signed: [u8; 32],
45 pub rseed_output: [u8; 32],
47}
48
49#[derive(Debug)]
51pub struct RealNoteInput {
52 pub note: Note,
54 pub fvk: FullViewingKey,
56 pub merkle_path: MerklePath,
58 pub imt_proof: ImtProofData,
60 pub scope: Scope,
62}
63
64#[derive(Debug)]
66pub struct DelegationBundle {
67 pub circuit: circuit::Circuit,
69 pub instance: circuit::Instance,
71}
72
73#[derive(Clone, Debug)]
75pub enum DelegationBuildError {
76 InvalidNoteCount(usize),
78 ImtFetchFailed(super::imt::ImtError),
80}
81
82impl From<super::imt::ImtError> for DelegationBuildError {
83 fn from(e: super::imt::ImtError) -> Self {
84 DelegationBuildError::ImtFetchFailed(e)
85 }
86}
87
88impl std::fmt::Display for DelegationBuildError {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 DelegationBuildError::InvalidNoteCount(n) => {
92 write!(f, "invalid note count: {} (expected 1–5)", n)
93 }
94 DelegationBuildError::ImtFetchFailed(e) => {
95 write!(f, "IMT proof fetch failed: {e}")
96 }
97 }
98 }
99}
100
101#[allow(clippy::too_many_arguments)]
117pub fn build_delegation_bundle(
118 real_notes: Vec<RealNoteInput>,
119 fvk: &FullViewingKey,
120 alpha: pallas::Scalar,
121 output_recipient: orchard::Address,
122 vote_round_id: pallas::Base,
123 nc_root: pallas::Base,
124 van_comm_rand: pallas::Base,
125 imt_provider: &impl ImtProvider,
126 rng: &mut impl RngCore,
127 precomputed: Option<&PrecomputedRandomness>,
128) -> Result<DelegationBundle, DelegationBuildError> {
129 let n_real = real_notes.len();
131 if n_real == 0 || n_real > 5 {
132 return Err(DelegationBuildError::InvalidNoteCount(n_real));
133 }
134
135 let nf_imt_root = imt_provider.root();
137
138 let nk_val = fvk.nk().inner();
140 let ak: SpendValidatingKey = fvk.clone().into();
141
142 let dom = derive_nullifier_domain(vote_round_id);
144
145 let mut note_slots = Vec::with_capacity(5);
147 let mut cmx_values = Vec::with_capacity(5);
148 let mut v_values = Vec::with_capacity(5);
149 let mut gov_nulls = Vec::with_capacity(5);
150
151 for input in &real_notes {
154 let note = &input.note;
155 let rho = note.rho();
156 let psi = note.rseed().psi(&rho);
157 let rcm = note.rseed().rcm(&rho);
158 let cm = note.commitment();
159 let cmx = ExtractedNoteCommitment::from(cm.clone()).inner();
160 let v_raw = note.value().inner();
161 let recipient = note.recipient();
162
163 let real_nf = note.nullifier(fvk);
165 let gov_null = gov_null_hash(nk_val, dom, real_nf.0);
167
168 let slot = NoteSlotWitness {
169 g_d: Value::known(recipient.g_d()),
170 pk_d: Value::known(
171 NonIdentityPallasPoint::from_bytes(&recipient.pk_d().to_bytes()).unwrap(),
172 ),
173 v: Value::known(note.value()),
174 rho: Value::known(rho.into_inner()),
175 psi: Value::known(psi),
176 rcm: Value::known(rcm),
177 cm: Value::known(cm),
178 path: Value::known(input.merkle_path.auth_path()),
179 pos: Value::known(input.merkle_path.position()),
180 imt_nf_bounds: Value::known(input.imt_proof.nf_bounds),
181 imt_leaf_pos: Value::known(input.imt_proof.leaf_pos),
182 imt_path: Value::known(input.imt_proof.path),
183 is_internal: Value::known(matches!(input.scope, Scope::Internal)),
184 };
185
186 note_slots.push(slot);
187 cmx_values.push(cmx);
188 v_values.push(v_raw);
189 gov_nulls.push(gov_null);
190 }
191
192 for i in n_real..5 {
196 let pad_addr = fvk.address_at((1000 + i) as u32, Scope::External);
198 let pad_idx = i - n_real; let pad_note = if let Some(pre) = precomputed {
201 assert!(pad_idx < pre.padded_notes.len(),
203 "precomputed.padded_notes has {} entries but need index {}",
204 pre.padded_notes.len(), pad_idx);
205 let pd = &pre.padded_notes[pad_idx];
206 let rho = Rho::from_bytes(&pd.rho).expect("precomputed rho must be valid");
207 let rseed = RandomSeed::from_bytes(pd.rseed, &rho).expect("precomputed rseed must be valid");
208 Note::from_parts(pad_addr, NoteValue::zero(), rho, rseed).expect("precomputed note must be valid")
209 } else {
210 let (_, _, dummy) = Note::dummy(&mut *rng, None);
211 Note::new(
212 pad_addr,
213 NoteValue::zero(),
214 Rho::from_nf_old(dummy.nullifier(fvk)),
215 &mut *rng,
216 )
217 };
218
219 let rho = pad_note.rho();
220 let psi = pad_note.rseed().psi(&rho);
221 let rcm = pad_note.rseed().rcm(&rho);
222 let cm = pad_note.commitment();
223 let cmx = ExtractedNoteCommitment::from(cm.clone()).inner();
224
225 let real_nf = pad_note.nullifier(fvk);
226 let gov_null = gov_null_hash(nk_val, dom, real_nf.0);
227
228 let imt_proof = imt_provider.non_membership_proof(real_nf.0)?;
230
231 let merkle_path = MerklePath::dummy(&mut *rng);
233
234 let slot = NoteSlotWitness {
235 g_d: Value::known(pad_addr.g_d()),
236 pk_d: Value::known(
237 NonIdentityPallasPoint::from_bytes(&pad_addr.pk_d().to_bytes()).unwrap(),
238 ),
239 v: Value::known(NoteValue::zero()),
240 rho: Value::known(rho.into_inner()),
241 psi: Value::known(psi),
242 rcm: Value::known(rcm),
243 cm: Value::known(cm),
244 path: Value::known(merkle_path.auth_path()),
245 pos: Value::known(merkle_path.position()),
246 imt_nf_bounds: Value::known(imt_proof.nf_bounds),
247 imt_leaf_pos: Value::known(imt_proof.leaf_pos),
248 imt_path: Value::known(imt_proof.path),
249 is_internal: Value::known(false),
250 };
251
252 note_slots.push(slot);
253 cmx_values.push(cmx);
254 v_values.push(0);
255 gov_nulls.push(gov_null);
256 }
257
258 let notes: [NoteSlotWitness; 5] = note_slots.try_into().unwrap_or_else(|_| unreachable!());
259
260 let v_total_u64: u64 = v_values.iter().sum();
263 let num_ballots_u64 = v_total_u64 / circuit::BALLOT_DIVISOR;
264 let remainder_u64 = v_total_u64 % circuit::BALLOT_DIVISOR;
265 let num_ballots_field = pallas::Base::from(num_ballots_u64);
266
267 let g_d_new_x = *output_recipient
273 .g_d()
274 .to_affine()
275 .coordinates()
276 .unwrap()
277 .x();
278 let pk_d_new_x = *output_recipient
279 .pk_d()
280 .inner()
281 .to_affine()
282 .coordinates()
283 .unwrap()
284 .x();
285
286 let van_comm = van_commitment_hash(g_d_new_x, pk_d_new_x, num_ballots_field, vote_round_id, van_comm_rand);
287
288 let rho = rho_binding_hash(
292 cmx_values[0],
293 cmx_values[1],
294 cmx_values[2],
295 cmx_values[3],
296 cmx_values[4],
297 van_comm,
298 vote_round_id,
299 );
300
301 let sender_address = fvk.address_at(0u32, Scope::External);
305 let signed_rho = Rho::from_nf_old(Nullifier(rho));
306 let signed_note = if let Some(pre) = precomputed {
307 let rseed = RandomSeed::from_bytes(pre.rseed_signed, &signed_rho)
308 .expect("precomputed rseed_signed must be valid");
309 Note::from_parts(sender_address, NoteValue::from_raw(1), signed_rho, rseed)
310 .expect("precomputed signed note must be valid")
311 } else {
312 Note::new(
313 sender_address,
314 NoteValue::from_raw(1),
315 signed_rho,
316 &mut *rng,
317 )
318 };
319
320 let nf_signed = signed_note.nullifier(fvk);
322
323 let output_rho = Rho::from_nf_old(nf_signed);
326 let output_note = if let Some(pre) = precomputed {
327 let rseed = RandomSeed::from_bytes(pre.rseed_output, &output_rho)
328 .expect("precomputed rseed_output must be valid");
329 Note::from_parts(output_recipient, NoteValue::zero(), output_rho, rseed)
330 .expect("precomputed output note must be valid")
331 } else {
332 Note::new(
333 output_recipient,
334 NoteValue::zero(),
335 output_rho,
336 &mut *rng,
337 )
338 };
339 let cmx_new = ExtractedNoteCommitment::from(output_note.commitment()).inner();
340
341 let rk = ak.randomize(&alpha);
343
344 let circuit = circuit::Circuit::from_note_unchecked(fvk, &signed_note, alpha)
349 .with_output_note(&output_note)
350 .with_notes(notes)
351 .with_van_comm_rand(van_comm_rand)
352 .with_ballot_scaling(
353 pallas::Base::from(num_ballots_u64),
354 pallas::Base::from(remainder_u64),
355 );
356
357 let instance = circuit::Instance::from_parts(
358 nf_signed,
359 rk,
360 cmx_new,
361 van_comm,
362 vote_round_id,
363 nc_root,
364 nf_imt_root,
365 [gov_nulls[0], gov_nulls[1], gov_nulls[2], gov_nulls[3], gov_nulls[4]],
366 dom,
367 );
368
369 Ok(DelegationBundle { circuit, instance })
370}
371
372#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::delegation::imt::SpacedLeafImtProvider;
380 use orchard::{
381 constants::MERKLE_DEPTH_ORCHARD,
382 keys::{FullViewingKey, Scope, SpendingKey},
383 note::{commitment::ExtractedNoteCommitment, Note, Rho},
384 tree::{MerkleHashOrchard, MerklePath},
385 value::NoteValue,
386 };
387 use ff::Field;
388 use halo2_proofs::dev::MockProver;
389 use incrementalmerkletree::{Hashable, Level};
390 use pasta_curves::pallas;
391 use rand::rngs::OsRng;
392
393 const K: u32 = 14;
395
396 fn make_real_note_inputs(
402 fvk: &FullViewingKey,
403 values: &[u64],
404 scopes: &[Scope],
405 imt_provider: &impl ImtProvider,
406 rng: &mut impl RngCore,
407 ) -> (Vec<RealNoteInput>, pallas::Base) {
408 let n = values.len();
409 assert!(n >= 1 && n <= 5);
410 assert_eq!(n, scopes.len());
411
412 let mut notes = Vec::with_capacity(n);
414 for (idx, &v) in values.iter().enumerate() {
415 let recipient = fvk.address_at(0u32, scopes[idx]);
416 let note_value = NoteValue::from_raw(v);
417 let (_, _, dummy_parent) = Note::dummy(&mut *rng, None);
418 let note = Note::new(
419 recipient,
420 note_value,
421 Rho::from_nf_old(dummy_parent.nullifier(fvk)),
422 &mut *rng,
423 );
424 notes.push(note);
425 }
426
427 let empty_leaf = MerkleHashOrchard::empty_leaf();
429 let mut leaves = [empty_leaf; 8];
430 for (i, note) in notes.iter().enumerate() {
431 let cmx = ExtractedNoteCommitment::from(note.commitment());
432 leaves[i] = MerkleHashOrchard::from_cmx(&cmx);
433 }
434
435 let l1_0 = MerkleHashOrchard::combine(Level::from(0), &leaves[0], &leaves[1]);
437 let l1_1 = MerkleHashOrchard::combine(Level::from(0), &leaves[2], &leaves[3]);
438 let l1_2 = MerkleHashOrchard::combine(Level::from(0), &leaves[4], &leaves[5]);
439 let l1_3 = MerkleHashOrchard::combine(Level::from(0), &leaves[6], &leaves[7]);
440 let l2_0 = MerkleHashOrchard::combine(Level::from(1), &l1_0, &l1_1);
441 let l2_1 = MerkleHashOrchard::combine(Level::from(1), &l1_2, &l1_3);
442 let l3_0 = MerkleHashOrchard::combine(Level::from(2), &l2_0, &l2_1);
443
444 let mut current = l3_0;
446 for level in 3..MERKLE_DEPTH_ORCHARD {
447 let sibling = MerkleHashOrchard::empty_root(Level::from(level as u8));
448 current = MerkleHashOrchard::combine(Level::from(level as u8), ¤t, &sibling);
449 }
450 let nc_root = current.inner();
451
452 let l1 = [l1_0, l1_1, l1_2, l1_3];
454 let l2 = [l2_0, l2_1];
455 let mut inputs = Vec::with_capacity(n);
456 for (i, note) in notes.into_iter().enumerate() {
457 let mut auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
458 auth_path[0] = leaves[i ^ 1];
459 auth_path[1] = l1[(i >> 1) ^ 1];
460 auth_path[2] = l2[1 - (i >> 2)];
461 for level in 3..MERKLE_DEPTH_ORCHARD {
462 auth_path[level] = MerkleHashOrchard::empty_root(Level::from(level as u8));
463 }
464 let merkle_path = MerklePath::from_parts(i as u32, auth_path);
465
466 let real_nf = note.nullifier(fvk);
467 let imt_proof = imt_provider.non_membership_proof(real_nf.0).unwrap();
468
469 inputs.push(RealNoteInput {
470 note,
471 fvk: fvk.clone(),
472 merkle_path,
473 imt_proof,
474 scope: scopes[i],
475 });
476 }
477
478 (inputs, nc_root)
479 }
480
481 fn build_and_verify(values: &[u64], scopes: &[Scope]) -> DelegationBundle {
483 assert_eq!(values.len(), scopes.len());
484 let mut rng = OsRng;
485 let sk = SpendingKey::random(&mut rng);
486 let fvk: FullViewingKey = (&sk).into();
487 let output_recipient = fvk.address_at(1u32, Scope::External);
488 let vote_round_id = pallas::Base::random(&mut rng);
489 let van_comm_rand = pallas::Base::random(&mut rng);
490 let alpha = pallas::Scalar::random(&mut rng);
491
492 let imt = SpacedLeafImtProvider::new();
493 let (inputs, nc_root) =
494 make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
495
496 let bundle = build_delegation_bundle(
497 inputs,
498 &fvk,
499 alpha,
500 output_recipient,
501 vote_round_id,
502 nc_root,
503 van_comm_rand,
504 &imt,
505 &mut rng,
506 None,
507 )
508 .unwrap();
509
510 let pi = bundle.instance.to_halo2_instance();
512 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
513 assert_eq!(prover.verify(), Ok(()), "merged circuit failed");
514
515 bundle
516 }
517
518 #[test]
519 fn test_single_real_note() {
520 build_and_verify(&[13_000_000], &[Scope::External]);
521 }
522
523 #[test]
524 fn test_four_real_notes() {
525 build_and_verify(
527 &[3_200_000, 3_200_000, 3_200_000, 3_200_000],
528 &[Scope::External, Scope::External, Scope::External, Scope::External],
529 );
530 }
531
532 #[test]
533 fn test_two_real_notes() {
534 build_and_verify(&[7_000_000, 7_000_000], &[Scope::External, Scope::External]);
535 }
536
537 #[test]
538 fn test_min_weight_boundary() {
539 build_and_verify(&[12_500_000], &[Scope::External]);
541 }
542
543 #[test]
544 fn test_below_one_ballot() {
545 let mut rng = OsRng;
548 let sk = SpendingKey::random(&mut rng);
549 let fvk: FullViewingKey = (&sk).into();
550 let output_recipient = fvk.address_at(1u32, Scope::External);
551 let vote_round_id = pallas::Base::random(&mut rng);
552 let van_comm_rand = pallas::Base::random(&mut rng);
553 let alpha = pallas::Scalar::random(&mut rng);
554
555 let imt = SpacedLeafImtProvider::new();
556 let (inputs, nc_root) = make_real_note_inputs(&fvk, &[12_499_999], &[Scope::External], &imt, &mut rng);
557
558 let bundle = build_delegation_bundle(
559 inputs,
560 &fvk,
561 alpha,
562 output_recipient,
563 vote_round_id,
564 nc_root,
565 van_comm_rand,
566 &imt,
567 &mut rng,
568 None,
569 )
570 .unwrap();
571
572 let pi = bundle.instance.to_halo2_instance();
573 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
574 assert!(prover.verify().is_err(), "below one ballot should fail");
575 }
576
577 #[test]
578 fn test_three_ballots() {
579 build_and_verify(
581 &[12_500_000, 12_500_000, 12_500_000],
582 &[Scope::External, Scope::External, Scope::External],
583 );
584 }
585
586 #[test]
587 fn test_zero_notes_error() {
588 let mut rng = OsRng;
589 let sk = SpendingKey::random(&mut rng);
590 let fvk: FullViewingKey = (&sk).into();
591 let output_recipient = fvk.address_at(1u32, Scope::External);
592 let imt = SpacedLeafImtProvider::new();
593
594 let result = build_delegation_bundle(
595 vec![],
596 &fvk,
597 pallas::Scalar::random(&mut rng),
598 output_recipient,
599 pallas::Base::random(&mut rng),
600 pallas::Base::random(&mut rng),
601 pallas::Base::random(&mut rng),
602 &imt,
603 &mut rng,
604 None,
605 );
606
607 assert!(matches!(
608 result,
609 Err(DelegationBuildError::InvalidNoteCount(0))
610 ));
611 }
612
613 #[test]
614 fn test_five_real_notes() {
615 build_and_verify(
617 &[2_500_000, 2_500_000, 2_500_000, 2_500_000, 2_500_000],
618 &[Scope::External, Scope::External, Scope::External, Scope::External, Scope::External],
619 );
620 }
621
622 #[test]
623 fn test_six_notes_error() {
624 let mut rng = OsRng;
625 let sk = SpendingKey::random(&mut rng);
626 let fvk: FullViewingKey = (&sk).into();
627 let output_recipient = fvk.address_at(1u32, Scope::External);
628 let imt = SpacedLeafImtProvider::new();
629
630 let (inputs, _) = make_real_note_inputs(
631 &fvk,
632 &[3_000_000, 3_000_000, 3_000_000, 3_000_000, 3_000_000],
633 &[Scope::External, Scope::External, Scope::External, Scope::External, Scope::External],
634 &imt,
635 &mut rng,
636 );
637 let mut inputs = inputs;
639 let (extra, _) = make_real_note_inputs(&fvk, &[3_000_000], &[Scope::External], &imt, &mut rng);
640 inputs.extend(extra);
641
642 let result = build_delegation_bundle(
643 inputs,
644 &fvk,
645 pallas::Scalar::random(&mut rng),
646 output_recipient,
647 pallas::Base::random(&mut rng),
648 pallas::Base::random(&mut rng),
649 pallas::Base::random(&mut rng),
650 &imt,
651 &mut rng,
652 None,
653 );
654
655 assert!(matches!(
656 result,
657 Err(DelegationBuildError::InvalidNoteCount(6))
658 ));
659 }
660
661 #[test]
662 fn test_single_internal_note() {
663 build_and_verify(&[13_000_000], &[Scope::Internal]);
664 }
665
666 #[test]
667 fn test_mixed_scope_notes() {
668 build_and_verify(
669 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
670 &[Scope::External, Scope::Internal, Scope::External, Scope::Internal],
671 );
672 }
673
674 #[test]
675 fn test_all_internal_notes() {
676 build_and_verify(
677 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
678 &[Scope::Internal, Scope::Internal, Scope::Internal, Scope::Internal],
679 );
680 }
681}