dpp/shielded/builder/
mod.rs

1//! Convenience builders for constructing shielded state transitions.
2//!
3//! These functions encapsulate the full Orchard bundle construction pipeline:
4//! builder configuration, proof generation, signature application,
5//! and serialization into platform state transitions.
6//!
7//! Requires the `shielded-client` feature, which pulls in
8//! `grovedb-commitment-tree` (and transitively the `orchard` crate).
9//!
10//! # Example
11//!
12//! ```ignore
13//! use dpp::shielded::builder::*;
14//! use grovedb_commitment_tree::{SpendingKey, FullViewingKey, Scope, ProvingKey};
15//!
16//! // Derive recipient address
17//! let sk = SpendingKey::from_bytes(seed)?;
18//! let fvk = FullViewingKey::from(&sk);
19//! let recipient = OrchardAddress::from_raw_bytes(
20//!     &fvk.address_at(0, Scope::External).to_raw_address_bytes(),
21//! );
22//!
23//! // Build a shield transition
24//! let pk = ProvingKey::build();
25//! let st = build_shield_transition(
26//!     &recipient, shield_amount, inputs, fee_strategy,
27//!     &signer, 0, &pk, [0u8; 36], platform_version,
28//! )?;
29//! ```
30
31mod shield;
32mod shield_from_asset_lock;
33mod shielded_transfer;
34mod shielded_withdrawal;
35mod unshield;
36
37pub use self::shield::build_shield_transition;
38pub use shield_from_asset_lock::build_shield_from_asset_lock_transition;
39pub use shielded_transfer::build_shielded_transfer_transition;
40pub use shielded_withdrawal::build_shielded_withdrawal_transition;
41pub use unshield::build_unshield_transition;
42
43use grovedb_commitment_tree::{
44    Anchor, Authorized, Builder, Bundle, BundleType, DashMemo, Flags as OrchardFlags,
45    FullViewingKey, MerklePath, Note, NoteValue, PaymentAddress, ProvingKey, SpendAuthorizingKey,
46};
47use rand::rngs::OsRng;
48
49use crate::address_funds::OrchardAddress;
50use crate::shielded::{compute_platform_sighash, SerializedAction};
51use crate::ProtocolError;
52
53/// Trait abstracting over Orchard proof generation.
54///
55/// This follows the same pattern as `Signer` — callers provide an implementation
56/// that holds (and potentially caches) the expensive `ProvingKey`, and the builder
57/// functions use it via this trait.
58pub trait OrchardProver {
59    /// Returns a reference to the Halo 2 proving key for the Orchard circuit.
60    fn proving_key(&self) -> &ProvingKey;
61}
62
63/// A note that can be spent in a shielded transaction, paired with its
64/// Merkle inclusion path in the commitment tree.
65pub struct SpendableNote {
66    /// The Orchard note to spend.
67    pub note: Note,
68    /// Merkle path proving the note's commitment exists in the tree.
69    pub merkle_path: MerklePath,
70}
71
72/// The serialized fields extracted from an authorized Orchard bundle,
73/// ready for use by state transition constructors.
74pub struct SerializedBundle {
75    /// Serialized Orchard actions (spends + outputs).
76    pub actions: Vec<SerializedAction>,
77    /// Bundle flags byte.
78    pub flags: u8,
79    /// Net value balance (positive = value leaving the shielded pool).
80    pub value_balance: i64,
81    /// Sinsemilla root of the Orchard note commitment tree (32 bytes).
82    /// This is the Orchard `Anchor` — the root hash of the depth-32 Sinsemilla
83    /// Merkle tree over extracted note commitments (cmx values).
84    pub anchor: [u8; 32],
85    /// Halo 2 proof bytes.
86    pub proof: Vec<u8>,
87    /// Binding signature (64 bytes).
88    pub binding_signature: [u8; 64],
89}
90
91impl From<&OrchardAddress> for PaymentAddress {
92    fn from(address: &OrchardAddress) -> Self {
93        *address.inner()
94    }
95}
96
97/// Serializes an authorized Orchard bundle into the raw fields used by
98/// state transition constructors.
99pub fn serialize_authorized_bundle(bundle: &Bundle<Authorized, i64, DashMemo>) -> SerializedBundle {
100    let actions: Vec<SerializedAction> = bundle
101        .actions()
102        .iter()
103        .map(|action| {
104            let enc = action.encrypted_note();
105            let mut encrypted_note = Vec::with_capacity(216);
106            encrypted_note.extend_from_slice(&enc.epk_bytes);
107            encrypted_note.extend_from_slice(enc.enc_ciphertext.as_ref());
108            encrypted_note.extend_from_slice(&enc.out_ciphertext);
109            SerializedAction {
110                nullifier: action.nullifier().to_bytes(),
111                rk: <[u8; 32]>::from(action.rk()),
112                cmx: action.cmx().to_bytes(),
113                encrypted_note,
114                cv_net: action.cv_net().to_bytes(),
115                spend_auth_sig: <[u8; 64]>::from(action.authorization()),
116            }
117        })
118        .collect();
119    let flags = bundle.flags().to_byte();
120    let value_balance = *bundle.value_balance();
121    let anchor = bundle.anchor().to_bytes();
122    let proof = bundle.authorization().proof().as_ref().to_vec();
123    let binding_signature = <[u8; 64]>::from(bundle.authorization().binding_signature());
124    SerializedBundle {
125        actions,
126        flags,
127        value_balance,
128        anchor,
129        proof,
130        binding_signature,
131    }
132}
133
134// ---------------------------------------------------------------------------
135// Internal helpers
136// ---------------------------------------------------------------------------
137
138/// Builds an output-only Orchard bundle (no spends).
139///
140/// Used by Shield and ShieldFromAssetLock transitions where funds enter
141/// the shielded pool from transparent sources.
142pub(crate) fn build_output_only_bundle<P: OrchardProver>(
143    recipient: &OrchardAddress,
144    amount: u64,
145    memo: [u8; 36],
146    prover: &P,
147) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
148    let payment_address = PaymentAddress::from(recipient);
149    let anchor = Anchor::empty_tree();
150    let mut builder = Builder::<DashMemo>::new(
151        BundleType::Transactional {
152            flags: OrchardFlags::SPENDS_DISABLED,
153            bundle_required: false,
154        },
155        anchor,
156    );
157
158    builder
159        .add_output(None, payment_address, NoteValue::from_raw(amount), memo)
160        .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
161
162    prove_and_sign_bundle(builder, prover, &[], &[])
163}
164
165/// Builds a spend+output Orchard bundle.
166///
167/// Used by ShieldedTransfer, Unshield, and ShieldedWithdrawal where funds
168/// are spent from existing notes.
169#[allow(clippy::too_many_arguments)]
170pub(crate) fn build_spend_bundle<P: OrchardProver>(
171    spends: Vec<SpendableNote>,
172    recipient: &OrchardAddress,
173    output_amount: u64,
174    memo: [u8; 36],
175    fvk: &FullViewingKey,
176    ask: &SpendAuthorizingKey,
177    anchor: Anchor,
178    prover: &P,
179    extra_sighash_data: &[u8],
180) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
181    let payment_address = PaymentAddress::from(recipient);
182
183    let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, anchor);
184
185    for spend in spends {
186        builder
187            .add_spend(fvk.clone(), spend.note, spend.merkle_path)
188            .map_err(|e| {
189                ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
190            })?;
191    }
192
193    builder
194        .add_output(
195            None,
196            payment_address,
197            NoteValue::from_raw(output_amount),
198            memo,
199        )
200        .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
201
202    prove_and_sign_bundle(
203        builder,
204        prover,
205        std::slice::from_ref(ask),
206        extra_sighash_data,
207    )
208}
209
210/// Takes a configured Builder, generates the proof, computes the platform
211/// sighash, and applies signatures.
212pub(crate) fn prove_and_sign_bundle<P: OrchardProver>(
213    builder: Builder<DashMemo>,
214    prover: &P,
215    signing_keys: &[SpendAuthorizingKey],
216    extra_sighash_data: &[u8],
217) -> Result<Bundle<Authorized, i64, DashMemo>, ProtocolError> {
218    let mut rng = OsRng;
219
220    let (unauthorized, _) = builder
221        .build::<i64>(&mut rng)
222        .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to build bundle: {:?}", e)))?
223        .ok_or_else(|| {
224            ProtocolError::ShieldedBuildError("bundle was empty after build".to_string())
225        })?;
226
227    let bundle_commitment: [u8; 32] = unauthorized.commitment().into();
228    let sighash = compute_platform_sighash(&bundle_commitment, extra_sighash_data);
229
230    let proven = unauthorized
231        .create_proof(prover.proving_key(), &mut rng)
232        .map_err(|e| {
233            ProtocolError::ShieldedBuildError(format!("failed to create proof: {:?}", e))
234        })?;
235
236    proven
237        .apply_signatures(rng, sighash, signing_keys)
238        .map_err(|e| {
239            ProtocolError::ShieldedBuildError(format!("failed to apply signatures: {:?}", e))
240        })
241}
242
243/// Shared test utilities for builder tests.
244#[cfg(test)]
245pub(crate) mod test_helpers {
246    use super::*;
247    use grovedb_commitment_tree::{
248        FullViewingKey, Hashable, MerkleHashOrchard, Note, NoteValue, ProvingKey, RandomSeed, Rho,
249        Scope, SpendingKey, NOTE_COMMITMENT_TREE_DEPTH,
250    };
251    use std::sync::OnceLock;
252
253    static PROVING_KEY: OnceLock<ProvingKey> = OnceLock::new();
254
255    /// Returns a cached ProvingKey (~30s to build on first call).
256    pub fn proving_key() -> &'static ProvingKey {
257        PROVING_KEY.get_or_init(ProvingKey::build)
258    }
259
260    /// Test implementation of `OrchardProver` backed by the cached proving key.
261    pub struct TestProver;
262
263    impl super::OrchardProver for TestProver {
264        fn proving_key(&self) -> &ProvingKey {
265            proving_key()
266        }
267    }
268
269    /// Creates a test OrchardAddress from a deterministic spending key.
270    pub fn test_orchard_address() -> OrchardAddress {
271        let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
272        let fvk = FullViewingKey::from(&sk);
273        let payment_address = fvk.address_at(0u32, Scope::External);
274        OrchardAddress::from_raw_bytes(&payment_address.to_raw_address_bytes())
275            .expect("valid orchard address bytes")
276    }
277
278    /// Creates a SpendableNote with the given value.
279    ///
280    /// The note is cryptographically valid (has a valid commitment) but uses
281    /// an all-zeros Merkle path, so it will only pass the Orchard circuit when
282    /// paired with `Anchor::empty_tree()`. Suitable for both error-path tests
283    /// (where the proving key is never reached) and happy-path tests.
284    pub fn test_spendable_note(value: u64) -> SpendableNote {
285        let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
286        let fvk = FullViewingKey::from(&sk);
287        let payment_address = fvk.address_at(0u32, Scope::External);
288
289        // Construct a valid Rho from the zero element (always valid in pallas)
290        let rho: Rho =
291            Option::from(Rho::from_bytes(&[0u8; 32])).expect("zero is valid pallas::Base");
292        let rseed: RandomSeed =
293            Option::from(RandomSeed::from_bytes([1u8; 32], &rho)).expect("valid random seed");
294        let note: Note = Option::from(Note::from_parts(
295            payment_address,
296            NoteValue::from_raw(value),
297            rho,
298            rseed,
299        ))
300        .expect("note commitment should be valid");
301
302        // All-zeros merkle path at position 0 — consistent with Anchor::empty_tree()
303        let auth_path = [MerkleHashOrchard::empty_leaf(); NOTE_COMMITMENT_TREE_DEPTH];
304        let merkle_path = MerklePath::from_parts(0, auth_path);
305
306        SpendableNote { note, merkle_path }
307    }
308}