1mod identity_create_from_shielded_pool;
33mod shield;
34mod shield_from_asset_lock;
35mod shielded_transfer;
36mod shielded_withdrawal;
37mod unshield;
38
39pub use self::shield::build_shield_transition;
40pub use identity_create_from_shielded_pool::{
41 build_identity_create_from_shielded_pool_transition, IdentityCreateFromShieldedPoolBuildResult,
42};
43pub use shield_from_asset_lock::build_shield_from_asset_lock_transition;
44#[cfg(feature = "core_key_wallet")]
45pub use shield_from_asset_lock::build_shield_from_asset_lock_transition_with_signer;
46pub use shielded_transfer::build_shielded_transfer_transition;
47pub use shielded_withdrawal::build_shielded_withdrawal_transition;
48pub use unshield::build_unshield_transition;
49
50use grovedb_commitment_tree::{
51 Anchor, Authorized, Builder, Bundle, BundleType, DashMemo, Flags as OrchardFlags,
52 FullViewingKey, MerklePath, Note, NoteValue, OutgoingViewingKey, PaymentAddress, ProvingKey,
53 Scope, SpendAuthorizingKey, SpendingKey,
54};
55use rand::rngs::OsRng;
56use rand::RngCore;
57
58use crate::address_funds::OrchardAddress;
59use crate::shielded::{compute_platform_sighash, SerializedAction};
60use crate::ProtocolError;
61
62pub trait OrchardProver {
68 fn proving_key(&self) -> &ProvingKey;
70}
71
72pub struct SpendableNote {
75 pub note: Note,
77 pub merkle_path: MerklePath,
79}
80
81pub struct SerializedBundle {
84 pub actions: Vec<SerializedAction>,
86 pub flags: u8,
88 pub value_balance: i64,
90 pub anchor: [u8; 32],
94 pub proof: Vec<u8>,
96 pub binding_signature: [u8; 64],
98}
99
100impl From<&OrchardAddress> for PaymentAddress {
101 fn from(address: &OrchardAddress) -> Self {
102 *address.inner()
103 }
104}
105
106pub fn serialize_authorized_bundle(bundle: &Bundle<Authorized, i64, DashMemo>) -> SerializedBundle {
109 let actions: Vec<SerializedAction> = bundle
110 .actions()
111 .iter()
112 .map(|action| {
113 let enc = action.encrypted_note();
114 let mut encrypted_note = Vec::with_capacity(216);
115 encrypted_note.extend_from_slice(&enc.epk_bytes);
116 encrypted_note.extend_from_slice(enc.enc_ciphertext.as_ref());
117 encrypted_note.extend_from_slice(&enc.out_ciphertext);
118 SerializedAction {
119 nullifier: action.nullifier().to_bytes(),
120 rk: <[u8; 32]>::from(action.rk()),
121 cmx: action.cmx().to_bytes(),
122 encrypted_note,
123 cv_net: action.cv_net().to_bytes(),
124 spend_auth_sig: <[u8; 64]>::from(action.authorization()),
125 }
126 })
127 .collect();
128 let flags = bundle.flags().to_byte();
129 let value_balance = *bundle.value_balance();
130 let anchor = bundle.anchor().to_bytes();
131 let proof = bundle.authorization().proof().as_ref().to_vec();
132 let binding_signature = <[u8; 64]>::from(bundle.authorization().binding_signature());
133 SerializedBundle {
134 actions,
135 flags,
136 value_balance,
137 anchor,
138 proof,
139 binding_signature,
140 }
141}
142
143fn random_orchard_payment_address() -> PaymentAddress {
157 let mut rng = OsRng;
158 loop {
159 let mut bytes = [0u8; 32];
160 rng.fill_bytes(&mut bytes);
161 if let Some(sk) = Option::<SpendingKey>::from(SpendingKey::from_bytes(bytes)) {
162 let fvk = FullViewingKey::from(&sk);
163 return fvk.address_at(0u32, Scope::External);
164 }
165 }
166}
167
168pub(crate) fn build_output_only_bundle<P: OrchardProver>(
191 recipient: &OrchardAddress,
192 amount: u64,
193 memo: [u8; 36],
194 sender_ovk: Option<OutgoingViewingKey>,
195 dummy_outputs: usize,
196 prover: &P,
197) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
198 let payment_address = PaymentAddress::from(recipient);
199 let anchor = Anchor::empty_tree();
200 let mut builder = Builder::<DashMemo>::new(
201 BundleType::Transactional {
202 flags: OrchardFlags::SPENDS_DISABLED,
203 bundle_required: false,
204 },
205 anchor,
206 );
207
208 builder
209 .add_output(
210 sender_ovk,
211 payment_address,
212 NoteValue::from_raw(amount),
213 memo,
214 )
215 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
216
217 for _ in 0..dummy_outputs {
220 let filler_address = random_orchard_payment_address();
221 builder
222 .add_output(None, filler_address, NoteValue::from_raw(0), [0u8; 36])
223 .map_err(|e| {
224 ProtocolError::ShieldedBuildError(format!("failed to add dummy output: {:?}", e))
225 })?;
226 }
227
228 prove_and_sign_bundle(builder, prover, &[], &[])
229}
230
231#[allow(clippy::too_many_arguments)]
240pub(crate) fn build_spend_bundle<P: OrchardProver>(
241 spends: Vec<SpendableNote>,
242 recipient: &OrchardAddress,
243 output_amount: u64,
244 memo: [u8; 36],
245 fvk: &FullViewingKey,
246 ask: &SpendAuthorizingKey,
247 anchor: Anchor,
248 prover: &P,
249 extra_sighash_data: &[u8],
250) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
251 let data = extra_sighash_data.to_vec();
252 build_spend_bundle_with(
253 spends,
254 recipient,
255 output_amount,
256 memo,
257 fvk,
258 ask,
259 anchor,
260 prover,
261 move |_| Ok(data),
262 )
263}
264
265#[allow(clippy::too_many_arguments)]
275pub(crate) fn build_spend_bundle_with<P: OrchardProver, F>(
276 spends: Vec<SpendableNote>,
277 recipient: &OrchardAddress,
278 output_amount: u64,
279 memo: [u8; 36],
280 fvk: &FullViewingKey,
281 ask: &SpendAuthorizingKey,
282 anchor: Anchor,
283 prover: &P,
284 extra_sighash_data: F,
285) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError>
286where
287 F: FnOnce(&[[u8; 32]]) -> Result<Vec<u8>, ProtocolError>,
288{
289 let payment_address = PaymentAddress::from(recipient);
290
291 let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, anchor);
292
293 for spend in spends {
294 builder
295 .add_spend(fvk.clone(), spend.note, spend.merkle_path)
296 .map_err(|e| {
297 ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
298 })?;
299 }
300
301 builder
302 .add_output(
303 Some(fvk.to_ovk(Scope::External)),
304 payment_address,
305 NoteValue::from_raw(output_amount),
306 memo,
307 )
308 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
309
310 prove_and_sign_bundle_with(
311 builder,
312 prover,
313 std::slice::from_ref(ask),
314 extra_sighash_data,
315 )
316}
317
318pub(crate) fn prove_and_sign_bundle<P: OrchardProver>(
321 builder: Builder<DashMemo>,
322 prover: &P,
323 signing_keys: &[SpendAuthorizingKey],
324 extra_sighash_data: &[u8],
325) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
326 let data = extra_sighash_data.to_vec();
327 prove_and_sign_bundle_with(builder, prover, signing_keys, move |_| Ok(data))
328}
329
330pub(crate) fn prove_and_sign_bundle_with<P: OrchardProver, F>(
335 builder: Builder<DashMemo>,
336 prover: &P,
337 signing_keys: &[SpendAuthorizingKey],
338 extra_sighash_data: F,
339) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError>
340where
341 F: FnOnce(&[[u8; 32]]) -> Result<Vec<u8>, ProtocolError>,
342{
343 let mut rng = OsRng;
344
345 let (unauthorized, _) = builder
346 .build::<i64>(&mut rng)
347 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to build bundle: {:?}", e)))?
348 .ok_or_else(|| {
349 ProtocolError::ShieldedBuildError("bundle was empty after build".to_string())
350 })?;
351
352 let nullifiers: Vec<[u8; 32]> = unauthorized
353 .actions()
354 .iter()
355 .map(|action| action.nullifier().to_bytes())
356 .collect();
357 let extra_sighash_data = extra_sighash_data(&nullifiers)?;
358
359 let bundle_commitment: [u8; 32] = unauthorized.commitment().into();
360 let sighash = compute_platform_sighash(&bundle_commitment, &extra_sighash_data);
361
362 let proven = unauthorized
363 .create_proof(prover.proving_key(), &mut rng)
364 .map_err(|e| {
365 ProtocolError::ShieldedBuildError(format!("failed to create proof: {:?}", e))
366 })?;
367
368 proven
369 .apply_signatures(rng, sighash, signing_keys)
370 .map_err(|e| {
371 ProtocolError::ShieldedBuildError(format!("failed to apply signatures: {:?}", e))
372 })
373}
374
375#[cfg(test)]
377pub(crate) mod test_helpers {
378 use super::*;
379 use grovedb_commitment_tree::{
380 FullViewingKey, Hashable, MerkleHashOrchard, Note, NoteValue, ProvingKey, RandomSeed, Rho,
381 Scope, SpendingKey, NOTE_COMMITMENT_TREE_DEPTH,
382 };
383 use std::sync::OnceLock;
384
385 static PROVING_KEY: OnceLock<ProvingKey> = OnceLock::new();
386
387 pub fn proving_key() -> &'static ProvingKey {
389 PROVING_KEY.get_or_init(ProvingKey::build)
390 }
391
392 pub struct TestProver;
394
395 impl super::OrchardProver for TestProver {
396 fn proving_key(&self) -> &ProvingKey {
397 proving_key()
398 }
399 }
400
401 pub fn test_orchard_address() -> OrchardAddress {
403 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
404 let fvk = FullViewingKey::from(&sk);
405 let payment_address = fvk.address_at(0u32, Scope::External);
406 OrchardAddress::from_raw_bytes(&payment_address.to_raw_address_bytes())
407 .expect("valid orchard address bytes")
408 }
409
410 pub fn test_spendable_note(value: u64) -> SpendableNote {
417 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
418 let fvk = FullViewingKey::from(&sk);
419 let payment_address = fvk.address_at(0u32, Scope::External);
420
421 let rho: Rho =
423 Option::from(Rho::from_bytes(&[0u8; 32])).expect("zero is valid pallas::Base");
424 let rseed: RandomSeed =
425 Option::from(RandomSeed::from_bytes([1u8; 32], &rho)).expect("valid random seed");
426 let note: Note = Option::from(Note::from_parts(
427 payment_address,
428 NoteValue::from_raw(value),
429 rho,
430 rseed,
431 ))
432 .expect("note commitment should be valid");
433
434 let auth_path = [MerkleHashOrchard::empty_leaf(); NOTE_COMMITMENT_TREE_DEPTH];
436 let merkle_path = MerklePath::from_parts(0, auth_path);
437
438 SpendableNote { note, merkle_path }
439 }
440}
441
442#[cfg(test)]
443mod mod_tests {
444 use super::test_helpers::{test_orchard_address, test_spendable_note, TestProver};
445 use super::*;
446 use grovedb_commitment_tree::{FullViewingKey, SpendAuthorizingKey, SpendingKey};
447
448 #[test]
455 fn output_only_bundle_flags_and_value_balance() {
456 let recipient = test_orchard_address();
457 let bundle = build_output_only_bundle(&recipient, 10_000, [0u8; 36], None, 0, &TestProver)
458 .expect("bundle should build");
459
460 assert!(!bundle.flags().spends_enabled());
462 assert!(bundle.flags().outputs_enabled());
463 assert_eq!(*bundle.value_balance(), -10_000i64);
465 assert!(
466 !bundle.actions().is_empty(),
467 "at least one padding action expected"
468 );
469 }
470
471 #[test]
485 fn dummy_output_padding_action_count_and_value_balance() {
486 let recipient = test_orchard_address();
487 let amount = 10_000u64;
488
489 for (dummies, expected_actions) in [(0usize, 2usize), (1, 2), (5, 6)] {
491 let bundle =
492 build_output_only_bundle(&recipient, amount, [0u8; 36], None, dummies, &TestProver)
493 .expect("bundle should build");
494 assert_eq!(
495 bundle.actions().len(),
496 expected_actions,
497 "dummy_outputs={dummies} should serialize to {expected_actions} actions"
498 );
499 assert_eq!(
501 *bundle.value_balance(),
502 -(amount as i64),
503 "value_balance must equal the real amount regardless of dummy_outputs ({dummies})"
504 );
505 }
506 }
507
508 #[test]
514 fn serialize_authorized_bundle_preserves_fields() {
515 let recipient = test_orchard_address();
516 let bundle = build_output_only_bundle(&recipient, 7_777, [3u8; 36], None, 0, &TestProver)
517 .expect("bundle should build");
518 let sb = serialize_authorized_bundle(&bundle);
519
520 assert_eq!(sb.value_balance, *bundle.value_balance());
521 assert_eq!(sb.flags, bundle.flags().to_byte());
522 assert_eq!(sb.anchor, bundle.anchor().to_bytes());
523 assert!(!sb.proof.is_empty(), "Halo 2 proof must not be empty");
524 assert_eq!(sb.binding_signature.len(), 64);
525 assert_eq!(sb.actions.len(), bundle.actions().len());
526 for action in &sb.actions {
527 assert!(!action.encrypted_note.is_empty());
532 assert_eq!(action.nullifier.len(), 32);
533 assert_eq!(action.cmx.len(), 32);
534 assert_eq!(action.cv_net.len(), 32);
535 assert_eq!(action.rk.len(), 32);
536 assert_eq!(action.spend_auth_sig.len(), 64);
537 }
538 }
539
540 #[test]
548 fn output_built_with_sender_ovk_recovers_under_that_ovk_only() {
549 use grovedb_commitment_tree::{try_output_recovery_with_ovk, OrchardDomain, Scope};
550
551 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
552 let sender_ovk = FullViewingKey::from(&sk).to_ovk(Scope::External);
553
554 let recipient = test_orchard_address();
555 let amount = 31_337u64;
556 let mut memo = [0u8; 36];
557 memo[..9].copy_from_slice(b"ovk-round");
558
559 let bundle = build_output_only_bundle(
560 &recipient,
561 amount,
562 memo,
563 Some(sender_ovk.clone()),
564 0,
565 &TestProver,
566 )
567 .expect("bundle should build");
568
569 let recover_all = |ovk: &grovedb_commitment_tree::OutgoingViewingKey| {
570 bundle
571 .actions()
572 .iter()
573 .filter_map(|action| {
574 let domain = OrchardDomain::<DashMemo>::for_action(action);
575 try_output_recovery_with_ovk(
576 &domain,
577 ovk,
578 action,
579 action.cv_net(),
580 &action.encrypted_note().out_ciphertext,
581 )
582 })
583 .collect::<Vec<_>>()
584 };
585
586 let recovered = recover_all(&sender_ovk);
587 assert_eq!(
588 recovered.len(),
589 1,
590 "exactly the real recipient output must recover; padding stays opaque"
591 );
592 let (note, recovered_addr, recovered_memo) = &recovered[0];
593 assert_eq!(note.value().inner(), amount, "recovered value mismatch");
594 assert_eq!(
595 recovered_addr.to_raw_address_bytes(),
596 recipient.inner().to_raw_address_bytes(),
597 "recovered recipient mismatch"
598 );
599 assert_eq!(*recovered_memo, memo, "recovered memo mismatch");
600
601 let other_sk = SpendingKey::from_bytes([7u8; 32]).expect("valid spending key bytes");
604 let other_ovk = FullViewingKey::from(&other_sk).to_ovk(Scope::External);
605 assert!(
606 recover_all(&other_ovk).is_empty(),
607 "a foreign OVK must not recover the output"
608 );
609 }
610
611 #[test]
616 fn from_orchard_address_to_payment_address_preserves_bytes() {
617 let addr = test_orchard_address();
618 let pa: PaymentAddress = (&addr).into();
619 assert_eq!(
620 pa.to_raw_address_bytes(),
621 addr.inner().to_raw_address_bytes()
622 );
623 }
624
625 #[test]
634 fn build_spend_bundle_add_spend_anchor_mismatch_surfaces_error() {
635 let recipient = test_orchard_address();
636 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key");
637 let fvk = FullViewingKey::from(&sk);
638 let ask = SpendAuthorizingKey::from(&sk);
639
640 let spends = vec![test_spendable_note(50_000)];
641
642 let result = build_spend_bundle(
643 spends,
644 &recipient,
645 40_000,
646 [1u8; 36],
647 &fvk,
648 &ask,
649 Anchor::empty_tree(),
650 &TestProver,
651 &[],
652 );
653 let err = result.expect_err("anchor mismatch should bubble up");
654 match err {
655 ProtocolError::ShieldedBuildError(msg) => {
656 assert!(
657 msg.contains("failed to add spend")
658 || msg.contains("AnchorMismatch")
659 || msg.contains("anchor"),
660 "unexpected error message: {}",
661 msg
662 );
663 }
664 other => panic!("expected ShieldedBuildError, got {:?}", other),
665 }
666 }
667
668 #[test]
669 fn build_spend_bundle_empty_spends_still_returns_some_output_bundle_or_error() {
670 let recipient = test_orchard_address();
675 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
676 let fvk = FullViewingKey::from(&sk);
677 let ask = SpendAuthorizingKey::from(&sk);
678
679 let result = build_spend_bundle(
680 vec![],
681 &recipient,
682 0,
683 [0u8; 36],
684 &fvk,
685 &ask,
686 Anchor::empty_tree(),
687 &TestProver,
688 &[],
689 );
690 match result {
693 Ok(_) => {}
694 Err(ProtocolError::ShieldedBuildError(_)) => {}
695 Err(e) => panic!("unexpected error kind: {:?}", e),
696 }
697 }
698
699 fn output_only_builder(amount: u64) -> Builder<DashMemo> {
702 let recipient = test_orchard_address();
703 let payment_address = PaymentAddress::from(&recipient);
704 let mut builder = Builder::<DashMemo>::new(
705 BundleType::Transactional {
706 flags: OrchardFlags::SPENDS_DISABLED,
707 bundle_required: false,
708 },
709 Anchor::empty_tree(),
710 );
711 builder
712 .add_output(
713 None,
714 payment_address,
715 NoteValue::from_raw(amount),
716 [0u8; 36],
717 )
718 .expect("add output");
719 builder
720 }
721
722 #[test]
732 fn prove_and_sign_bundle_with_closure_receives_published_nullifiers() {
733 let builder = output_only_builder(10_000);
734
735 let mut recorded: Option<Vec<[u8; 32]>> = None;
736 let bundle = prove_and_sign_bundle_with(builder, &TestProver, &[], |nullifiers| {
737 recorded = Some(nullifiers.to_vec());
738 Ok(vec![])
739 })
740 .expect("output-only bundle should prove");
741
742 let recorded = recorded.expect("the extra-sighash closure must run");
743 assert_eq!(
746 recorded.len(),
747 2,
748 "closure must see one nullifier per PUBLISHED action (incl. padding)"
749 );
750 assert_ne!(
751 recorded[0], recorded[1],
752 "padding dummy nullifiers are randomized per action"
753 );
754 let published: Vec<[u8; 32]> = bundle
757 .actions()
758 .iter()
759 .map(|action| action.nullifier().to_bytes())
760 .collect();
761 assert_eq!(
762 recorded, published,
763 "closure must receive the bundle's published nullifiers in on-wire order"
764 );
765 }
766
767 #[test]
768 fn prove_and_sign_bundle_with_closure_error_short_circuits_before_proving() {
769 let builder = output_only_builder(10_000);
770
771 let result = prove_and_sign_bundle_with(builder, &TestProver, &[], |_| {
772 Err(ProtocolError::ShieldedBuildError(
773 "closure rejected".to_string(),
774 ))
775 });
776
777 match result {
778 Err(ProtocolError::ShieldedBuildError(msg)) => {
779 assert_eq!(msg, "closure rejected", "closure error must pass through");
780 }
781 other => panic!("expected the closure's error to propagate, got {:?}", other),
782 }
783 }
784}