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