dpp/shielded/builder/
shielded_transfer.rs

1use grovedb_commitment_tree::{
2    Anchor, Builder, BundleType, DashMemo, FullViewingKey, NoteValue, PaymentAddress,
3    SpendAuthorizingKey,
4};
5
6use crate::address_funds::OrchardAddress;
7use crate::fee::Credits;
8use crate::shielded::compute_minimum_shielded_fee;
9use crate::state_transition::shielded_transfer_transition::methods::ShieldedTransferTransitionMethodsV0;
10use crate::state_transition::shielded_transfer_transition::ShieldedTransferTransition;
11use crate::state_transition::StateTransition;
12use crate::ProtocolError;
13use platform_version::version::PlatformVersion;
14
15use super::{prove_and_sign_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
16
17/// Builds a ShieldedTransfer state transition (shielded pool -> shielded pool).
18///
19/// Spends existing notes and creates a new note for the recipient. The shielded
20/// fee is deducted from the spent notes. Any remaining change is returned to
21/// the `change_address`.
22///
23/// # Parameters
24/// - `spends` - Notes to spend with their Merkle paths
25/// - `recipient` - Orchard address to receive the transferred note
26/// - `transfer_amount` - Amount to transfer to the recipient
27/// - `change_address` - Orchard address for change output (if any)
28/// - `fvk` - Full viewing key for spend authorization
29/// - `ask` - Spend authorizing key for RedPallas signatures
30/// - `anchor` - Sinsemilla root of the note commitment tree (Orchard Anchor)
31/// - `prover` - Orchard prover (holds the Halo 2 proving key)
32/// - `memo` - 36-byte structured memo for the recipient (4-byte type tag + 32-byte payload)
33/// - `fee` - Optional fee override; if `None`, the minimum fee is computed automatically.
34///   If `Some`, must be >= the minimum fee.
35/// - `platform_version` - Protocol version
36#[allow(clippy::too_many_arguments)]
37pub fn build_shielded_transfer_transition<P: OrchardProver>(
38    spends: Vec<SpendableNote>,
39    recipient: &OrchardAddress,
40    transfer_amount: u64,
41    change_address: &OrchardAddress,
42    fvk: &FullViewingKey,
43    ask: &SpendAuthorizingKey,
44    anchor: Anchor,
45    prover: &P,
46    memo: [u8; 36],
47    fee: Option<Credits>,
48    platform_version: &PlatformVersion,
49) -> Result<StateTransition, ProtocolError> {
50    let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum();
51
52    // Conservative action count: at least (spends, 2) since we always have
53    // a recipient output and likely a change output.
54    let num_actions = spends.len().max(2);
55    let min_fee = compute_minimum_shielded_fee(num_actions, platform_version);
56    let effective_fee = match fee {
57        Some(f) if f < min_fee => {
58            return Err(ProtocolError::ShieldedBuildError(format!(
59                "fee {} is below minimum required fee {}",
60                f, min_fee
61            )));
62        }
63        Some(f) if f > min_fee.saturating_mul(1000) => {
64            return Err(ProtocolError::ShieldedBuildError(format!(
65                "fee {} exceeds 1000x the minimum fee {}",
66                f, min_fee
67            )));
68        }
69        Some(f) => f,
70        None => min_fee,
71    };
72
73    let required = transfer_amount.checked_add(effective_fee).ok_or_else(|| {
74        ProtocolError::ShieldedBuildError("fee + transfer_amount overflows u64".to_string())
75    })?;
76    if required > total_spent {
77        return Err(ProtocolError::ShieldedBuildError(format!(
78            "transfer amount {} + fee {} = {} exceeds total spendable value {}",
79            transfer_amount, effective_fee, required, total_spent
80        )));
81    }
82
83    let change_amount = total_spent - required;
84
85    let recipient_payment = PaymentAddress::from(recipient);
86
87    let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, anchor);
88
89    for spend in spends {
90        builder
91            .add_spend(fvk.clone(), spend.note, spend.merkle_path)
92            .map_err(|e| {
93                ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
94            })?;
95    }
96
97    // Primary output to recipient
98    builder
99        .add_output(
100            None,
101            recipient_payment,
102            NoteValue::from_raw(transfer_amount),
103            memo,
104        )
105        .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
106
107    // Change output (if any)
108    if change_amount > 0 {
109        let change_payment = PaymentAddress::from(change_address);
110        builder
111            .add_output(
112                None,
113                change_payment,
114                NoteValue::from_raw(change_amount),
115                [0u8; 36],
116            )
117            .map_err(|e| {
118                ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e))
119            })?;
120    }
121
122    // ShieldedTransfer has no extra_data in sighash
123    let bundle = prove_and_sign_bundle(builder, prover, std::slice::from_ref(ask), &[])?;
124    let sb = serialize_authorized_bundle(&bundle);
125
126    // value_balance = effective_fee (the amount leaving the shielded pool as fee)
127    ShieldedTransferTransition::try_from_bundle(
128        sb.actions,
129        sb.value_balance as u64,
130        sb.anchor,
131        sb.proof,
132        sb.binding_signature,
133        platform_version,
134    )
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::shielded::builder::test_helpers::{
141        test_orchard_address, test_spendable_note, TestProver,
142    };
143
144    #[test]
145    fn test_shielded_transfer_fee_below_minimum() {
146        let platform_version = PlatformVersion::latest();
147        let recipient = test_orchard_address();
148        let change_address = test_orchard_address();
149
150        let note = test_spendable_note(1_000_000);
151        let spends = vec![note];
152
153        let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
154            .expect("valid spending key bytes");
155        let fvk = FullViewingKey::from(&sk);
156        let ask = SpendAuthorizingKey::from(&sk);
157
158        let result = build_shielded_transfer_transition(
159            spends,
160            &recipient,
161            100,
162            &change_address,
163            &fvk,
164            &ask,
165            Anchor::empty_tree(),
166            &TestProver,
167            [0u8; 36],
168            Some(1), // fee = 1, should be below minimum
169            platform_version,
170        );
171
172        assert!(result.is_err());
173        let err = result.unwrap_err().to_string();
174        assert!(
175            err.contains("below minimum required fee"),
176            "unexpected error: {}",
177            err
178        );
179    }
180
181    #[test]
182    fn test_shielded_transfer_insufficient_funds() {
183        let platform_version = PlatformVersion::latest();
184        let recipient = test_orchard_address();
185        let change_address = test_orchard_address();
186
187        // Note with only 100 credits
188        let note = test_spendable_note(100);
189        let spends = vec![note];
190
191        let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
192            .expect("valid spending key bytes");
193        let fvk = FullViewingKey::from(&sk);
194        let ask = SpendAuthorizingKey::from(&sk);
195
196        let result = build_shielded_transfer_transition(
197            spends,
198            &recipient,
199            1_000_000,
200            &change_address,
201            &fvk,
202            &ask,
203            Anchor::empty_tree(),
204            &TestProver,
205            [0u8; 36],
206            None,
207            platform_version,
208        );
209
210        assert!(result.is_err());
211        let err = result.unwrap_err().to_string();
212        assert!(
213            err.contains("exceeds total spendable value"),
214            "unexpected error: {}",
215            err
216        );
217    }
218}