1mod document_token_payment;
33mod identity_create_from_shielded_pool;
34mod identity_top_up_from_shielded_pool;
35mod shield;
36mod shield_from_asset_lock;
37mod shield_from_identity;
38mod shielded_transfer;
39mod shielded_withdrawal;
40mod token_burn_from_pool;
41mod token_claim_to_pool;
42mod token_direct_purchase_to_pool;
43mod token_mint_to_pool;
44mod token_pool_paid;
45mod token_shield;
46mod token_shielded_transfer;
47mod token_unshield;
48mod unshield;
49
50pub use self::shield::build_shield_transition;
51pub use document_token_payment::build_document_shielded_token_payment;
52pub use identity_create_from_shielded_pool::{
53 build_identity_create_from_shielded_pool_transition, IdentityCreateFromShieldedPoolBuildResult,
54};
55pub use identity_top_up_from_shielded_pool::build_identity_top_up_from_shielded_pool_transition;
56pub use shield_from_asset_lock::build_shield_from_asset_lock_transition;
57#[cfg(feature = "core_key_wallet")]
58pub use shield_from_asset_lock::build_shield_from_asset_lock_transition_with_signer;
59pub use shield_from_identity::build_shield_from_identity_transition;
60pub use shielded_transfer::build_shielded_transfer_transition;
61pub use shielded_withdrawal::build_shielded_withdrawal_transition;
62pub use token_burn_from_pool::build_token_burn_from_pool_transition;
63pub use token_claim_to_pool::build_token_claim_to_pool_transition;
64pub use token_direct_purchase_to_pool::build_token_direct_purchase_to_pool_transition;
65pub use token_mint_to_pool::build_token_mint_to_pool_transition;
66pub use token_pool_paid::{
67 build_token_purchase_from_shielded_pool_transition,
68 build_token_shielded_transfer_with_shielded_fee_transition,
69 build_token_unshield_with_shielded_fee_transition, ShieldedFeePayer, TokenPoolSpender,
70};
71pub use token_shield::build_token_shield_transition;
72pub use token_shielded_transfer::build_token_shielded_transfer_transition;
73pub use token_unshield::build_token_unshield_transition;
74pub use unshield::build_unshield_transition;
75
76use grovedb_commitment_tree::{
77 Anchor, Authorized, Builder, Bundle, BundleType, DashMemo, Flags as OrchardFlags,
78 FullViewingKey, MerklePath, Note, NoteValue, OutgoingViewingKey, PaymentAddress, ProvingKey,
79 Scope, SpendAuthorizingKey, SpendingKey,
80};
81use rand::rngs::OsRng;
82use rand::RngCore;
83
84use crate::address_funds::OrchardAddress;
85use crate::shielded::{compute_platform_sighash, SerializedAction};
86use crate::ProtocolError;
87
88pub trait OrchardProver {
94 fn proving_key(&self) -> &ProvingKey;
96}
97
98pub struct SpendableNote {
101 pub note: Note,
103 pub merkle_path: MerklePath,
105}
106
107pub struct SerializedBundle {
110 pub actions: Vec<SerializedAction>,
112 pub flags: u8,
114 pub value_balance: i64,
116 pub anchor: [u8; 32],
120 pub proof: Vec<u8>,
122 pub binding_signature: [u8; 64],
124}
125
126impl From<&OrchardAddress> for PaymentAddress {
127 fn from(address: &OrchardAddress) -> Self {
128 *address.inner()
129 }
130}
131
132pub fn serialize_authorized_bundle(bundle: &Bundle<Authorized, i64, DashMemo>) -> SerializedBundle {
135 let actions: Vec<SerializedAction> = bundle
136 .actions()
137 .iter()
138 .map(|action| {
139 let enc = action.encrypted_note();
140 let mut encrypted_note = Vec::with_capacity(216);
141 encrypted_note.extend_from_slice(&enc.epk_bytes);
142 encrypted_note.extend_from_slice(enc.enc_ciphertext.as_ref());
143 encrypted_note.extend_from_slice(&enc.out_ciphertext);
144 SerializedAction {
145 nullifier: action.nullifier().to_bytes(),
146 rk: <[u8; 32]>::from(action.rk()),
147 cmx: action.cmx().to_bytes(),
148 encrypted_note,
149 cv_net: action.cv_net().to_bytes(),
150 spend_auth_sig: <[u8; 64]>::from(action.authorization()),
151 }
152 })
153 .collect();
154 let flags = bundle.flags().to_byte();
155 let value_balance = *bundle.value_balance();
156 let anchor = bundle.anchor().to_bytes();
157 let proof = bundle.authorization().proof().as_ref().to_vec();
158 let binding_signature = <[u8; 64]>::from(bundle.authorization().binding_signature());
159 SerializedBundle {
160 actions,
161 flags,
162 value_balance,
163 anchor,
164 proof,
165 binding_signature,
166 }
167}
168
169fn random_orchard_payment_address() -> PaymentAddress {
183 let mut rng = OsRng;
184 loop {
185 let mut bytes = [0u8; 32];
186 rng.fill_bytes(&mut bytes);
187 if let Some(sk) = Option::<SpendingKey>::from(SpendingKey::from_bytes(bytes)) {
188 let fvk = FullViewingKey::from(&sk);
189 return fvk.address_at(0u32, Scope::External);
190 }
191 }
192}
193
194pub(crate) fn build_output_only_bundle<P: OrchardProver>(
217 recipient: &OrchardAddress,
218 amount: u64,
219 memo: [u8; 36],
220 sender_ovk: Option<OutgoingViewingKey>,
221 dummy_outputs: usize,
222 extra_sighash_data: &[u8],
223 prover: &P,
224) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
225 let payment_address = PaymentAddress::from(recipient);
226 let anchor = Anchor::empty_tree();
227 let mut builder = Builder::<DashMemo>::new(
228 BundleType::Transactional {
229 flags: OrchardFlags::SPENDS_DISABLED,
230 bundle_required: false,
231 },
232 anchor,
233 );
234
235 builder
236 .add_output(
237 sender_ovk,
238 payment_address,
239 NoteValue::from_raw(amount),
240 memo,
241 )
242 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
243
244 for _ in 0..dummy_outputs {
247 let filler_address = random_orchard_payment_address();
248 builder
249 .add_output(None, filler_address, NoteValue::from_raw(0), [0u8; 36])
250 .map_err(|e| {
251 ProtocolError::ShieldedBuildError(format!("failed to add dummy output: {:?}", e))
252 })?;
253 }
254
255 prove_and_sign_bundle(builder, prover, &[], extra_sighash_data)
256}
257
258#[allow(clippy::too_many_arguments)]
267pub(crate) fn build_spend_bundle<P: OrchardProver>(
268 spends: Vec<SpendableNote>,
269 recipient: &OrchardAddress,
270 output_amount: u64,
271 memo: [u8; 36],
272 fvk: &FullViewingKey,
273 ask: &SpendAuthorizingKey,
274 anchor: Anchor,
275 prover: &P,
276 extra_sighash_data: &[u8],
277) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
278 let data = extra_sighash_data.to_vec();
279 build_spend_bundle_with(
280 spends,
281 recipient,
282 output_amount,
283 memo,
284 fvk,
285 ask,
286 anchor,
287 prover,
288 move |_| Ok(data),
289 )
290}
291
292#[allow(clippy::too_many_arguments)]
302pub(crate) fn build_spend_bundle_with<P: OrchardProver, F>(
303 spends: Vec<SpendableNote>,
304 recipient: &OrchardAddress,
305 output_amount: u64,
306 memo: [u8; 36],
307 fvk: &FullViewingKey,
308 ask: &SpendAuthorizingKey,
309 anchor: Anchor,
310 prover: &P,
311 extra_sighash_data: F,
312) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError>
313where
314 F: FnOnce(&[[u8; 32]]) -> Result<Vec<u8>, ProtocolError>,
315{
316 let payment_address = PaymentAddress::from(recipient);
317
318 let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, anchor);
319
320 for spend in spends {
321 builder
322 .add_spend(fvk.clone(), spend.note, spend.merkle_path)
323 .map_err(|e| {
324 ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
325 })?;
326 }
327
328 builder
329 .add_output(
330 Some(fvk.to_ovk(Scope::External)),
331 payment_address,
332 NoteValue::from_raw(output_amount),
333 memo,
334 )
335 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
336
337 prove_and_sign_bundle_with(
338 builder,
339 prover,
340 std::slice::from_ref(ask),
341 extra_sighash_data,
342 )
343}
344
345pub(crate) fn prove_and_sign_bundle<P: OrchardProver>(
348 builder: Builder<DashMemo>,
349 prover: &P,
350 signing_keys: &[SpendAuthorizingKey],
351 extra_sighash_data: &[u8],
352) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
353 let data = extra_sighash_data.to_vec();
354 prove_and_sign_bundle_with(builder, prover, signing_keys, move |_| Ok(data))
355}
356
357pub(crate) fn prove_and_sign_bundle_with<P: OrchardProver, F>(
362 builder: Builder<DashMemo>,
363 prover: &P,
364 signing_keys: &[SpendAuthorizingKey],
365 extra_sighash_data: F,
366) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError>
367where
368 F: FnOnce(&[[u8; 32]]) -> Result<Vec<u8>, ProtocolError>,
369{
370 let mut rng = OsRng;
371
372 let (unauthorized, _) = builder
373 .build::<i64>(&mut rng)
374 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to build bundle: {:?}", e)))?
375 .ok_or_else(|| {
376 ProtocolError::ShieldedBuildError("bundle was empty after build".to_string())
377 })?;
378
379 let nullifiers: Vec<[u8; 32]> = unauthorized
380 .actions()
381 .iter()
382 .map(|action| action.nullifier().to_bytes())
383 .collect();
384 let extra_sighash_data = extra_sighash_data(&nullifiers)?;
385
386 let bundle_commitment: [u8; 32] = unauthorized.commitment().into();
387 let sighash = compute_platform_sighash(&bundle_commitment, &extra_sighash_data);
388
389 let proven = unauthorized
390 .create_proof(prover.proving_key(), &mut rng)
391 .map_err(|e| {
392 ProtocolError::ShieldedBuildError(format!("failed to create proof: {:?}", e))
393 })?;
394
395 proven
396 .apply_signatures(rng, sighash, signing_keys)
397 .map_err(|e| {
398 ProtocolError::ShieldedBuildError(format!("failed to apply signatures: {:?}", e))
399 })
400}
401
402#[cfg(test)]
404pub(crate) mod test_helpers {
405 use super::*;
406 use crate::address_funds::AddressWitness;
407 use crate::identity::signer::Signer;
408 use crate::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel};
409 use grovedb_commitment_tree::{
410 FullViewingKey, Hashable, MerkleHashOrchard, Note, NoteValue, ProvingKey, RandomSeed, Rho,
411 Scope, SpendingKey, NOTE_COMMITMENT_TREE_DEPTH,
412 };
413 use platform_value::BinaryData;
414 use platform_version::version::PlatformVersion;
415 use rand::rngs::StdRng;
416 use rand::SeedableRng;
417 use std::sync::OnceLock;
418
419 static PROVING_KEY: OnceLock<ProvingKey> = OnceLock::new();
420
421 pub fn proving_key() -> &'static ProvingKey {
423 PROVING_KEY.get_or_init(ProvingKey::build)
424 }
425
426 pub struct TestProver;
428
429 impl super::OrchardProver for TestProver {
430 fn proving_key(&self) -> &ProvingKey {
431 proving_key()
432 }
433 }
434
435 pub fn test_orchard_address() -> OrchardAddress {
437 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
438 let fvk = FullViewingKey::from(&sk);
439 let payment_address = fvk.address_at(0u32, Scope::External);
440 OrchardAddress::from_raw_bytes(&payment_address.to_raw_address_bytes())
441 .expect("valid orchard address bytes")
442 }
443
444 #[derive(Debug)]
447 pub struct DummyIdentitySigner;
448
449 #[async_trait::async_trait]
450 impl Signer<IdentityPublicKey> for DummyIdentitySigner {
451 async fn sign(
452 &self,
453 _key: &IdentityPublicKey,
454 _data: &[u8],
455 ) -> Result<BinaryData, ProtocolError> {
456 Ok(BinaryData::new(vec![0u8; 65]))
457 }
458
459 async fn sign_create_witness(
460 &self,
461 _key: &IdentityPublicKey,
462 _data: &[u8],
463 ) -> Result<AddressWitness, ProtocolError> {
464 Err(ProtocolError::ShieldedBuildError(
465 "identity signer never creates address witnesses".to_string(),
466 ))
467 }
468
469 fn can_sign_with(&self, _key: &IdentityPublicKey) -> bool {
470 true
471 }
472 }
473
474 pub fn test_identity_key() -> IdentityPublicKey {
476 let mut rng = StdRng::seed_from_u64(42);
477 let (key, _) = IdentityPublicKey::random_key_with_known_attributes(
478 0,
479 &mut rng,
480 Purpose::AUTHENTICATION,
481 SecurityLevel::CRITICAL,
482 KeyType::ECDSA_SECP256K1,
483 None,
484 PlatformVersion::latest(),
485 )
486 .expect("authentication key");
487 key
488 }
489
490 pub fn test_spendable_note(value: u64) -> SpendableNote {
497 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
498 let fvk = FullViewingKey::from(&sk);
499 let payment_address = fvk.address_at(0u32, Scope::External);
500
501 let rho: Rho =
503 Option::from(Rho::from_bytes(&[0u8; 32])).expect("zero is valid pallas::Base");
504 let rseed: RandomSeed =
505 Option::from(RandomSeed::from_bytes([1u8; 32], &rho)).expect("valid random seed");
506 let note: Note = Option::from(Note::from_parts(
507 payment_address,
508 NoteValue::from_raw(value),
509 rho,
510 rseed,
511 ))
512 .expect("note commitment should be valid");
513
514 let auth_path = [MerkleHashOrchard::empty_leaf(); NOTE_COMMITMENT_TREE_DEPTH];
516 let merkle_path = MerklePath::from_parts(0, auth_path);
517
518 SpendableNote { note, merkle_path }
519 }
520}
521
522#[cfg(test)]
523mod mod_tests {
524 use super::test_helpers::{test_orchard_address, test_spendable_note, TestProver};
525 use super::*;
526 use grovedb_commitment_tree::{FullViewingKey, SpendAuthorizingKey, SpendingKey};
527
528 #[test]
535 fn output_only_bundle_flags_and_value_balance() {
536 let recipient = test_orchard_address();
537 let bundle =
538 build_output_only_bundle(&recipient, 10_000, [0u8; 36], None, 0, &[], &TestProver)
539 .expect("bundle should build");
540
541 assert!(!bundle.flags().spends_enabled());
543 assert!(bundle.flags().outputs_enabled());
544 assert_eq!(*bundle.value_balance(), -10_000i64);
546 assert!(
547 !bundle.actions().is_empty(),
548 "at least one padding action expected"
549 );
550 }
551
552 #[test]
566 fn dummy_output_padding_action_count_and_value_balance() {
567 let recipient = test_orchard_address();
568 let amount = 10_000u64;
569
570 for (dummies, expected_actions) in [(0usize, 2usize), (1, 2), (5, 6)] {
572 let bundle = build_output_only_bundle(
573 &recipient,
574 amount,
575 [0u8; 36],
576 None,
577 dummies,
578 &[],
579 &TestProver,
580 )
581 .expect("bundle should build");
582 assert_eq!(
583 bundle.actions().len(),
584 expected_actions,
585 "dummy_outputs={dummies} should serialize to {expected_actions} actions"
586 );
587 assert_eq!(
589 *bundle.value_balance(),
590 -(amount as i64),
591 "value_balance must equal the real amount regardless of dummy_outputs ({dummies})"
592 );
593 }
594 }
595
596 #[test]
602 fn serialize_authorized_bundle_preserves_fields() {
603 let recipient = test_orchard_address();
604 let bundle =
605 build_output_only_bundle(&recipient, 7_777, [3u8; 36], None, 0, &[], &TestProver)
606 .expect("bundle should build");
607 let sb = serialize_authorized_bundle(&bundle);
608
609 assert_eq!(sb.value_balance, *bundle.value_balance());
610 assert_eq!(sb.flags, bundle.flags().to_byte());
611 assert_eq!(sb.anchor, bundle.anchor().to_bytes());
612 assert!(!sb.proof.is_empty(), "Halo 2 proof must not be empty");
613 assert_eq!(sb.binding_signature.len(), 64);
614 assert_eq!(sb.actions.len(), bundle.actions().len());
615 for action in &sb.actions {
616 assert!(!action.encrypted_note.is_empty());
621 assert_eq!(action.nullifier.len(), 32);
622 assert_eq!(action.cmx.len(), 32);
623 assert_eq!(action.cv_net.len(), 32);
624 assert_eq!(action.rk.len(), 32);
625 assert_eq!(action.spend_auth_sig.len(), 64);
626 }
627 }
628
629 #[test]
637 fn output_built_with_sender_ovk_recovers_under_that_ovk_only() {
638 use grovedb_commitment_tree::{try_output_recovery_with_ovk, OrchardDomain, Scope};
639
640 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
641 let sender_ovk = FullViewingKey::from(&sk).to_ovk(Scope::External);
642
643 let recipient = test_orchard_address();
644 let amount = 31_337u64;
645 let mut memo = [0u8; 36];
646 memo[..9].copy_from_slice(b"ovk-round");
647
648 let bundle = build_output_only_bundle(
649 &recipient,
650 amount,
651 memo,
652 Some(sender_ovk.clone()),
653 0,
654 &[],
655 &TestProver,
656 )
657 .expect("bundle should build");
658
659 let recover_all = |ovk: &grovedb_commitment_tree::OutgoingViewingKey| {
660 bundle
661 .actions()
662 .iter()
663 .filter_map(|action| {
664 let domain = OrchardDomain::<DashMemo>::for_action(action);
665 try_output_recovery_with_ovk(
666 &domain,
667 ovk,
668 action,
669 action.cv_net(),
670 &action.encrypted_note().out_ciphertext,
671 )
672 })
673 .collect::<Vec<_>>()
674 };
675
676 let recovered = recover_all(&sender_ovk);
677 assert_eq!(
678 recovered.len(),
679 1,
680 "exactly the real recipient output must recover; padding stays opaque"
681 );
682 let (note, recovered_addr, recovered_memo) = &recovered[0];
683 assert_eq!(note.value().inner(), amount, "recovered value mismatch");
684 assert_eq!(
685 recovered_addr.to_raw_address_bytes(),
686 recipient.inner().to_raw_address_bytes(),
687 "recovered recipient mismatch"
688 );
689 assert_eq!(*recovered_memo, memo, "recovered memo mismatch");
690
691 let other_sk = SpendingKey::from_bytes([7u8; 32]).expect("valid spending key bytes");
694 let other_ovk = FullViewingKey::from(&other_sk).to_ovk(Scope::External);
695 assert!(
696 recover_all(&other_ovk).is_empty(),
697 "a foreign OVK must not recover the output"
698 );
699 }
700
701 #[test]
706 fn from_orchard_address_to_payment_address_preserves_bytes() {
707 let addr = test_orchard_address();
708 let pa: PaymentAddress = (&addr).into();
709 assert_eq!(
710 pa.to_raw_address_bytes(),
711 addr.inner().to_raw_address_bytes()
712 );
713 }
714
715 #[test]
724 fn build_spend_bundle_add_spend_anchor_mismatch_surfaces_error() {
725 let recipient = test_orchard_address();
726 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key");
727 let fvk = FullViewingKey::from(&sk);
728 let ask = SpendAuthorizingKey::from(&sk);
729
730 let spends = vec![test_spendable_note(50_000)];
731
732 let result = build_spend_bundle(
733 spends,
734 &recipient,
735 40_000,
736 [1u8; 36],
737 &fvk,
738 &ask,
739 Anchor::empty_tree(),
740 &TestProver,
741 &[],
742 );
743 let err = result.expect_err("anchor mismatch should bubble up");
744 match err {
745 ProtocolError::ShieldedBuildError(msg) => {
746 assert!(
747 msg.contains("failed to add spend")
748 || msg.contains("AnchorMismatch")
749 || msg.contains("anchor"),
750 "unexpected error message: {}",
751 msg
752 );
753 }
754 other => panic!("expected ShieldedBuildError, got {:?}", other),
755 }
756 }
757
758 #[test]
759 fn build_spend_bundle_empty_spends_still_returns_some_output_bundle_or_error() {
760 let recipient = test_orchard_address();
765 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
766 let fvk = FullViewingKey::from(&sk);
767 let ask = SpendAuthorizingKey::from(&sk);
768
769 let result = build_spend_bundle(
770 vec![],
771 &recipient,
772 0,
773 [0u8; 36],
774 &fvk,
775 &ask,
776 Anchor::empty_tree(),
777 &TestProver,
778 &[],
779 );
780 match result {
783 Ok(_) => {}
784 Err(ProtocolError::ShieldedBuildError(_)) => {}
785 Err(e) => panic!("unexpected error kind: {:?}", e),
786 }
787 }
788
789 fn output_only_builder(amount: u64) -> Builder<DashMemo> {
792 let recipient = test_orchard_address();
793 let payment_address = PaymentAddress::from(&recipient);
794 let mut builder = Builder::<DashMemo>::new(
795 BundleType::Transactional {
796 flags: OrchardFlags::SPENDS_DISABLED,
797 bundle_required: false,
798 },
799 Anchor::empty_tree(),
800 );
801 builder
802 .add_output(
803 None,
804 payment_address,
805 NoteValue::from_raw(amount),
806 [0u8; 36],
807 )
808 .expect("add output");
809 builder
810 }
811
812 #[test]
822 fn prove_and_sign_bundle_with_closure_receives_published_nullifiers() {
823 let builder = output_only_builder(10_000);
824
825 let mut recorded: Option<Vec<[u8; 32]>> = None;
826 let bundle = prove_and_sign_bundle_with(builder, &TestProver, &[], |nullifiers| {
827 recorded = Some(nullifiers.to_vec());
828 Ok(vec![])
829 })
830 .expect("output-only bundle should prove");
831
832 let recorded = recorded.expect("the extra-sighash closure must run");
833 assert_eq!(
836 recorded.len(),
837 2,
838 "closure must see one nullifier per PUBLISHED action (incl. padding)"
839 );
840 assert_ne!(
841 recorded[0], recorded[1],
842 "padding dummy nullifiers are randomized per action"
843 );
844 let published: Vec<[u8; 32]> = bundle
847 .actions()
848 .iter()
849 .map(|action| action.nullifier().to_bytes())
850 .collect();
851 assert_eq!(
852 recorded, published,
853 "closure must receive the bundle's published nullifiers in on-wire order"
854 );
855 }
856
857 #[test]
858 fn prove_and_sign_bundle_with_closure_error_short_circuits_before_proving() {
859 let builder = output_only_builder(10_000);
860
861 let result = prove_and_sign_bundle_with(builder, &TestProver, &[], |_| {
862 Err(ProtocolError::ShieldedBuildError(
863 "closure rejected".to_string(),
864 ))
865 });
866
867 match result {
868 Err(ProtocolError::ShieldedBuildError(msg)) => {
869 assert_eq!(msg, "closure rejected", "closure error must pass through");
870 }
871 other => panic!("expected the closure's error to propagate, got {:?}", other),
872 }
873 }
874}