1use group::Curve;
9use halo2_proofs::circuit::Value;
10use pasta_curves::{arithmetic::CurveAffine, pallas};
11use rand::RngCore;
12use std::vec::Vec;
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, rho_binding_hash, van_commitment_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.inner());
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!(
203 pad_idx < pre.padded_notes.len(),
204 "precomputed.padded_notes has {} entries but need index {}",
205 pre.padded_notes.len(),
206 pad_idx
207 );
208 let pd = &pre.padded_notes[pad_idx];
209 let rho = Rho::from_bytes(&pd.rho).expect("precomputed rho must be valid");
210 let rseed =
211 RandomSeed::from_bytes(pd.rseed, &rho).expect("precomputed rseed must be valid");
212 Note::from_parts(pad_addr, NoteValue::ZERO, rho, rseed)
213 .expect("precomputed note must be valid")
214 } else {
215 let (_, _, dummy) = Note::dummy(&mut *rng, None);
216 Note::new(
217 pad_addr,
218 NoteValue::ZERO,
219 Rho::from_nf_old(dummy.nullifier(fvk)),
220 &mut *rng,
221 )
222 };
223
224 let rho = pad_note.rho();
225 let psi = pad_note.rseed().psi(&rho);
226 let rcm = pad_note.rseed().rcm(&rho);
227 let cm = pad_note.commitment();
228 let cmx = ExtractedNoteCommitment::from(cm.clone()).inner();
229
230 let real_nf = pad_note.nullifier(fvk);
231 let gov_null = gov_null_hash(nk_val, dom, real_nf.inner());
232
233 let imt_proof = imt_provider.non_membership_proof(real_nf.inner())?;
235
236 let merkle_path = MerklePath::dummy(&mut *rng);
238
239 let slot = NoteSlotWitness {
240 g_d: Value::known(pad_addr.g_d()),
241 pk_d: Value::known(
242 NonIdentityPallasPoint::from_bytes(&pad_addr.pk_d().to_bytes()).unwrap(),
243 ),
244 v: Value::known(NoteValue::ZERO),
245 rho: Value::known(rho.into_inner()),
246 psi: Value::known(psi),
247 rcm: Value::known(rcm),
248 cm: Value::known(cm),
249 path: Value::known(merkle_path.auth_path()),
250 pos: Value::known(merkle_path.position()),
251 imt_nf_bounds: Value::known(imt_proof.nf_bounds),
252 imt_leaf_pos: Value::known(imt_proof.leaf_pos),
253 imt_path: Value::known(imt_proof.path),
254 is_internal: Value::known(false),
255 };
256
257 note_slots.push(slot);
258 cmx_values.push(cmx);
259 v_values.push(0);
260 gov_nulls.push(gov_null);
261 }
262
263 let notes: [NoteSlotWitness; 5] = note_slots.try_into().unwrap_or_else(|_| unreachable!());
264
265 let v_total_u64: u64 = v_values.iter().sum();
268 let num_ballots_u64 = v_total_u64 / circuit::BALLOT_DIVISOR;
269 let remainder_u64 = v_total_u64 % circuit::BALLOT_DIVISOR;
270 let num_ballots_field = pallas::Base::from(num_ballots_u64);
271
272 let g_d_new_x = *output_recipient
278 .g_d()
279 .to_affine()
280 .coordinates()
281 .unwrap()
282 .x();
283 let pk_d_new_x = *output_recipient
284 .pk_d()
285 .inner()
286 .to_affine()
287 .coordinates()
288 .unwrap()
289 .x();
290
291 let van_comm = van_commitment_hash(
292 g_d_new_x,
293 pk_d_new_x,
294 num_ballots_field,
295 vote_round_id,
296 van_comm_rand,
297 );
298
299 let rho = rho_binding_hash(
303 cmx_values[0],
304 cmx_values[1],
305 cmx_values[2],
306 cmx_values[3],
307 cmx_values[4],
308 van_comm,
309 vote_round_id,
310 );
311
312 let sender_address = fvk.address_at(0u32, Scope::External);
316 let signed_rho = Rho::from_nf_old(Nullifier::from_inner(rho));
317 let signed_note = if let Some(pre) = precomputed {
318 let rseed = RandomSeed::from_bytes(pre.rseed_signed, &signed_rho)
319 .expect("precomputed rseed_signed must be valid");
320 Note::from_parts(sender_address, NoteValue::from_raw(1), signed_rho, rseed)
321 .expect("precomputed signed note must be valid")
322 } else {
323 Note::new(
324 sender_address,
325 NoteValue::from_raw(1),
326 signed_rho,
327 &mut *rng,
328 )
329 };
330
331 let nf_signed = signed_note.nullifier(fvk);
333
334 let output_rho = Rho::from_nf_old(nf_signed);
337 let output_note = if let Some(pre) = precomputed {
338 let rseed = RandomSeed::from_bytes(pre.rseed_output, &output_rho)
339 .expect("precomputed rseed_output must be valid");
340 Note::from_parts(output_recipient, NoteValue::ZERO, output_rho, rseed)
341 .expect("precomputed output note must be valid")
342 } else {
343 Note::new(output_recipient, NoteValue::ZERO, output_rho, &mut *rng)
344 };
345 let cmx_new = ExtractedNoteCommitment::from(output_note.commitment()).inner();
346
347 let rk = ak.randomize(&alpha);
349
350 let circuit = circuit::Circuit::from_note_unchecked(fvk, &signed_note, alpha)
355 .with_output_note(&output_note)
356 .with_notes(notes)
357 .with_van_comm_rand(van_comm_rand)
358 .with_ballot_scaling(
359 pallas::Base::from(num_ballots_u64),
360 pallas::Base::from(remainder_u64),
361 );
362
363 let instance = circuit::Instance::from_parts(
364 nf_signed,
365 rk,
366 cmx_new,
367 van_comm,
368 vote_round_id,
369 nc_root,
370 nf_imt_root,
371 [
372 gov_nulls[0],
373 gov_nulls[1],
374 gov_nulls[2],
375 gov_nulls[3],
376 gov_nulls[4],
377 ],
378 dom,
379 );
380
381 Ok(DelegationBundle { circuit, instance })
382}
383
384#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::delegation::imt::SpacedLeafImtProvider;
392 use ff::Field;
393 use halo2_proofs::dev::MockProver;
394 use incrementalmerkletree::{Hashable, Level};
395 use orchard::{
396 constants::MERKLE_DEPTH_ORCHARD,
397 keys::{FullViewingKey, Scope, SpendingKey},
398 note::{commitment::ExtractedNoteCommitment, Note, Rho},
399 tree::{MerkleHashOrchard, MerklePath},
400 value::NoteValue,
401 };
402 use pasta_curves::pallas;
403 use rand::rngs::OsRng;
404
405 const K: u32 = 14;
407
408 fn make_real_note_inputs(
414 fvk: &FullViewingKey,
415 values: &[u64],
416 scopes: &[Scope],
417 imt_provider: &impl ImtProvider,
418 rng: &mut impl RngCore,
419 ) -> (Vec<RealNoteInput>, pallas::Base) {
420 let n = values.len();
421 assert!(n >= 1 && n <= 5);
422 assert_eq!(n, scopes.len());
423
424 let mut notes = Vec::with_capacity(n);
426 for (idx, &v) in values.iter().enumerate() {
427 let recipient = fvk.address_at(0u32, scopes[idx]);
428 let note_value = NoteValue::from_raw(v);
429 let (_, _, dummy_parent) = Note::dummy(&mut *rng, None);
430 let note = Note::new(
431 recipient,
432 note_value,
433 Rho::from_nf_old(dummy_parent.nullifier(fvk)),
434 &mut *rng,
435 );
436 notes.push(note);
437 }
438
439 let empty_leaf = MerkleHashOrchard::empty_leaf();
441 let mut leaves = [empty_leaf; 8];
442 for (i, note) in notes.iter().enumerate() {
443 let cmx = ExtractedNoteCommitment::from(note.commitment());
444 leaves[i] = MerkleHashOrchard::from_cmx(&cmx);
445 }
446
447 let l1_0 = MerkleHashOrchard::combine(Level::from(0), &leaves[0], &leaves[1]);
449 let l1_1 = MerkleHashOrchard::combine(Level::from(0), &leaves[2], &leaves[3]);
450 let l1_2 = MerkleHashOrchard::combine(Level::from(0), &leaves[4], &leaves[5]);
451 let l1_3 = MerkleHashOrchard::combine(Level::from(0), &leaves[6], &leaves[7]);
452 let l2_0 = MerkleHashOrchard::combine(Level::from(1), &l1_0, &l1_1);
453 let l2_1 = MerkleHashOrchard::combine(Level::from(1), &l1_2, &l1_3);
454 let l3_0 = MerkleHashOrchard::combine(Level::from(2), &l2_0, &l2_1);
455
456 let mut current = l3_0;
458 for level in 3..MERKLE_DEPTH_ORCHARD {
459 let sibling = MerkleHashOrchard::empty_root(Level::from(level as u8));
460 current = MerkleHashOrchard::combine(Level::from(level as u8), ¤t, &sibling);
461 }
462 let nc_root = current.inner();
463
464 let l1 = [l1_0, l1_1, l1_2, l1_3];
466 let l2 = [l2_0, l2_1];
467 let mut inputs = Vec::with_capacity(n);
468 for (i, note) in notes.into_iter().enumerate() {
469 let mut auth_path = [MerkleHashOrchard::empty_leaf(); MERKLE_DEPTH_ORCHARD];
470 auth_path[0] = leaves[i ^ 1];
471 auth_path[1] = l1[(i >> 1) ^ 1];
472 auth_path[2] = l2[1 - (i >> 2)];
473 for level in 3..MERKLE_DEPTH_ORCHARD {
474 auth_path[level] = MerkleHashOrchard::empty_root(Level::from(level as u8));
475 }
476 let merkle_path = MerklePath::from_parts(i as u32, auth_path);
477
478 let real_nf = note.nullifier(fvk);
479 let imt_proof = imt_provider.non_membership_proof(real_nf.inner()).unwrap();
480
481 inputs.push(RealNoteInput {
482 note,
483 fvk: fvk.clone(),
484 merkle_path,
485 imt_proof,
486 scope: scopes[i],
487 });
488 }
489
490 (inputs, nc_root)
491 }
492
493 fn build_and_verify(values: &[u64], scopes: &[Scope]) -> DelegationBundle {
495 assert_eq!(values.len(), scopes.len());
496 let mut rng = OsRng;
497 let sk = SpendingKey::random(&mut rng);
498 let fvk: FullViewingKey = (&sk).into();
499 let output_recipient = fvk.address_at(1u32, Scope::External);
500 let vote_round_id = pallas::Base::random(&mut rng);
501 let van_comm_rand = pallas::Base::random(&mut rng);
502 let alpha = pallas::Scalar::random(&mut rng);
503
504 let imt = SpacedLeafImtProvider::new();
505 let (inputs, nc_root) = make_real_note_inputs(&fvk, values, scopes, &imt, &mut rng);
506
507 let bundle = build_delegation_bundle(
508 inputs,
509 &fvk,
510 alpha,
511 output_recipient,
512 vote_round_id,
513 nc_root,
514 van_comm_rand,
515 &imt,
516 &mut rng,
517 None,
518 )
519 .unwrap();
520
521 let pi = bundle.instance.to_halo2_instance();
523 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
524 assert_eq!(prover.verify(), Ok(()), "merged circuit failed");
525
526 bundle
527 }
528
529 #[test]
530 fn test_single_real_note() {
531 build_and_verify(&[13_000_000], &[Scope::External]);
532 }
533
534 #[test]
535 fn test_four_real_notes() {
536 build_and_verify(
538 &[3_200_000, 3_200_000, 3_200_000, 3_200_000],
539 &[
540 Scope::External,
541 Scope::External,
542 Scope::External,
543 Scope::External,
544 ],
545 );
546 }
547
548 #[test]
549 fn test_two_real_notes() {
550 build_and_verify(&[7_000_000, 7_000_000], &[Scope::External, Scope::External]);
551 }
552
553 #[test]
554 fn test_min_weight_boundary() {
555 build_and_verify(&[12_500_000], &[Scope::External]);
557 }
558
559 #[test]
560 fn test_below_one_ballot() {
561 let mut rng = OsRng;
564 let sk = SpendingKey::random(&mut rng);
565 let fvk: FullViewingKey = (&sk).into();
566 let output_recipient = fvk.address_at(1u32, Scope::External);
567 let vote_round_id = pallas::Base::random(&mut rng);
568 let van_comm_rand = pallas::Base::random(&mut rng);
569 let alpha = pallas::Scalar::random(&mut rng);
570
571 let imt = SpacedLeafImtProvider::new();
572 let (inputs, nc_root) =
573 make_real_note_inputs(&fvk, &[12_499_999], &[Scope::External], &imt, &mut rng);
574
575 let bundle = build_delegation_bundle(
576 inputs,
577 &fvk,
578 alpha,
579 output_recipient,
580 vote_round_id,
581 nc_root,
582 van_comm_rand,
583 &imt,
584 &mut rng,
585 None,
586 )
587 .unwrap();
588
589 let pi = bundle.instance.to_halo2_instance();
590 let prover = MockProver::run(K, &bundle.circuit, vec![pi]).unwrap();
591 assert!(prover.verify().is_err(), "below one ballot should fail");
592 }
593
594 #[test]
595 fn test_three_ballots() {
596 build_and_verify(
598 &[12_500_000, 12_500_000, 12_500_000],
599 &[Scope::External, Scope::External, Scope::External],
600 );
601 }
602
603 #[test]
604 fn test_zero_notes_error() {
605 let mut rng = OsRng;
606 let sk = SpendingKey::random(&mut rng);
607 let fvk: FullViewingKey = (&sk).into();
608 let output_recipient = fvk.address_at(1u32, Scope::External);
609 let imt = SpacedLeafImtProvider::new();
610
611 let result = build_delegation_bundle(
612 vec![],
613 &fvk,
614 pallas::Scalar::random(&mut rng),
615 output_recipient,
616 pallas::Base::random(&mut rng),
617 pallas::Base::random(&mut rng),
618 pallas::Base::random(&mut rng),
619 &imt,
620 &mut rng,
621 None,
622 );
623
624 assert!(matches!(
625 result,
626 Err(DelegationBuildError::InvalidNoteCount(0))
627 ));
628 }
629
630 #[test]
631 fn test_five_real_notes() {
632 build_and_verify(
634 &[2_500_000, 2_500_000, 2_500_000, 2_500_000, 2_500_000],
635 &[
636 Scope::External,
637 Scope::External,
638 Scope::External,
639 Scope::External,
640 Scope::External,
641 ],
642 );
643 }
644
645 #[test]
646 fn test_six_notes_error() {
647 let mut rng = OsRng;
648 let sk = SpendingKey::random(&mut rng);
649 let fvk: FullViewingKey = (&sk).into();
650 let output_recipient = fvk.address_at(1u32, Scope::External);
651 let imt = SpacedLeafImtProvider::new();
652
653 let (inputs, _) = make_real_note_inputs(
654 &fvk,
655 &[3_000_000, 3_000_000, 3_000_000, 3_000_000, 3_000_000],
656 &[
657 Scope::External,
658 Scope::External,
659 Scope::External,
660 Scope::External,
661 Scope::External,
662 ],
663 &imt,
664 &mut rng,
665 );
666 let mut inputs = inputs;
668 let (extra, _) =
669 make_real_note_inputs(&fvk, &[3_000_000], &[Scope::External], &imt, &mut rng);
670 inputs.extend(extra);
671
672 let result = build_delegation_bundle(
673 inputs,
674 &fvk,
675 pallas::Scalar::random(&mut rng),
676 output_recipient,
677 pallas::Base::random(&mut rng),
678 pallas::Base::random(&mut rng),
679 pallas::Base::random(&mut rng),
680 &imt,
681 &mut rng,
682 None,
683 );
684
685 assert!(matches!(
686 result,
687 Err(DelegationBuildError::InvalidNoteCount(6))
688 ));
689 }
690
691 #[test]
692 fn test_single_internal_note() {
693 build_and_verify(&[13_000_000], &[Scope::Internal]);
694 }
695
696 #[test]
697 fn test_mixed_scope_notes() {
698 build_and_verify(
699 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
700 &[
701 Scope::External,
702 Scope::Internal,
703 Scope::External,
704 Scope::Internal,
705 ],
706 );
707 }
708
709 #[test]
710 fn test_all_internal_notes() {
711 build_and_verify(
712 &[4_000_000, 4_000_000, 3_000_000, 2_000_000],
713 &[
714 Scope::Internal,
715 Scope::Internal,
716 Scope::Internal,
717 Scope::Internal,
718 ],
719 );
720 }
721}