1use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey};
2
3use crate::address_funds::OrchardAddress;
4use crate::fee::Credits;
5use crate::identity::core_script::CoreScript;
6use crate::shielded::compute_minimum_shielded_fee;
7use crate::state_transition::shielded_withdrawal_transition::methods::ShieldedWithdrawalTransitionMethodsV0;
8use crate::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition;
9use crate::state_transition::StateTransition;
10use crate::withdrawal::Pooling;
11use crate::ProtocolError;
12use platform_version::version::PlatformVersion;
13
14use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
15
16#[allow(clippy::too_many_arguments)]
38pub fn build_shielded_withdrawal_transition<P: OrchardProver>(
39 spends: Vec<SpendableNote>,
40 withdrawal_amount: u64,
41 output_script: CoreScript,
42 core_fee_per_byte: u32,
43 pooling: Pooling,
44 change_address: &OrchardAddress,
45 fvk: &FullViewingKey,
46 ask: &SpendAuthorizingKey,
47 anchor: Anchor,
48 prover: &P,
49 memo: [u8; 36],
50 fee: Option<Credits>,
51 platform_version: &PlatformVersion,
52) -> Result<StateTransition, ProtocolError> {
53 if withdrawal_amount > i64::MAX as u64 {
54 return Err(ProtocolError::ShieldedBuildError(format!(
55 "withdrawal amount {} exceeds maximum allowed value {}",
56 withdrawal_amount,
57 i64::MAX as u64
58 )));
59 }
60
61 let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum();
62
63 let num_actions = spends.len().max(1);
65 let min_fee = compute_minimum_shielded_fee(num_actions, platform_version);
66 let effective_fee = match fee {
67 Some(f) if f < min_fee => {
68 return Err(ProtocolError::ShieldedBuildError(format!(
69 "fee {} is below minimum required fee {}",
70 f, min_fee
71 )));
72 }
73 Some(f) if f > min_fee.saturating_mul(1000) => {
74 return Err(ProtocolError::ShieldedBuildError(format!(
75 "fee {} exceeds 1000x the minimum fee {}",
76 f, min_fee
77 )));
78 }
79 Some(f) => f,
80 None => min_fee,
81 };
82
83 let required = withdrawal_amount
84 .checked_add(effective_fee)
85 .ok_or_else(|| {
86 ProtocolError::ShieldedBuildError("fee + withdrawal_amount overflows u64".to_string())
87 })?;
88 if required > total_spent {
89 return Err(ProtocolError::ShieldedBuildError(format!(
90 "withdrawal amount {} + fee {} = {} exceeds total spendable value {}",
91 withdrawal_amount, effective_fee, required, total_spent
92 )));
93 }
94
95 let change_amount = total_spent - required;
96
97 let mut extra_sighash_data = output_script.as_bytes().to_vec();
101 extra_sighash_data.extend_from_slice(&required.to_le_bytes());
102
103 let bundle = build_spend_bundle(
104 spends,
105 change_address,
106 change_amount,
107 memo,
108 fvk,
109 ask,
110 anchor,
111 prover,
112 &extra_sighash_data,
113 )?;
114
115 let sb = serialize_authorized_bundle(&bundle);
116
117 ShieldedWithdrawalTransition::try_from_bundle(
118 sb.actions,
119 sb.value_balance as u64,
120 sb.anchor,
121 sb.proof,
122 sb.binding_signature,
123 core_fee_per_byte,
124 pooling,
125 output_script,
126 platform_version,
127 )
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::shielded::builder::test_helpers::{
134 test_orchard_address, test_spendable_note, TestProver,
135 };
136
137 #[test]
138 fn test_shielded_withdrawal_fee_below_minimum() {
139 let platform_version = PlatformVersion::latest();
140 let change_address = test_orchard_address();
141
142 let note = test_spendable_note(1_000_000);
143 let spends = vec![note];
144
145 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
146 .expect("valid spending key bytes");
147 let fvk = FullViewingKey::from(&sk);
148 let ask = SpendAuthorizingKey::from(&sk);
149
150 let result = build_shielded_withdrawal_transition(
151 spends,
152 100,
153 CoreScript::new_p2pkh([1u8; 20]), 1,
155 Pooling::Never,
156 &change_address,
157 &fvk,
158 &ask,
159 Anchor::empty_tree(),
160 &TestProver,
161 [0u8; 36],
162 Some(1), platform_version,
164 );
165
166 assert!(result.is_err());
167 let err = result.unwrap_err().to_string();
168 assert!(
169 err.contains("below minimum required fee"),
170 "unexpected error: {}",
171 err
172 );
173 }
174
175 #[test]
176 fn test_shielded_withdrawal_fee_above_upper_bound() {
177 let platform_version = PlatformVersion::latest();
178 let change_address = test_orchard_address();
179
180 let note = test_spendable_note(u64::MAX);
181 let spends = vec![note];
182
183 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
184 .expect("valid spending key bytes");
185 let fvk = FullViewingKey::from(&sk);
186 let ask = SpendAuthorizingKey::from(&sk);
187
188 let num_actions = 1usize;
190 let min_fee = crate::shielded::compute_minimum_shielded_fee(num_actions, platform_version);
191 let excessive_fee = min_fee.saturating_mul(1000) + 1;
192
193 let result = build_shielded_withdrawal_transition(
194 spends,
195 100,
196 CoreScript::new_p2pkh([1u8; 20]),
197 1,
198 Pooling::Never,
199 &change_address,
200 &fvk,
201 &ask,
202 Anchor::empty_tree(),
203 &TestProver,
204 [0u8; 36],
205 Some(excessive_fee),
206 platform_version,
207 );
208
209 assert!(result.is_err());
210 let err = result.unwrap_err().to_string();
211 assert!(
212 err.contains("exceeds 1000x the minimum fee"),
213 "unexpected error: {}",
214 err
215 );
216 }
217
218 #[test]
219 fn test_shielded_withdrawal_insufficient_funds() {
220 let platform_version = PlatformVersion::latest();
221 let change_address = test_orchard_address();
222
223 let note = test_spendable_note(100);
224 let spends = vec![note];
225
226 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
227 .expect("valid spending key bytes");
228 let fvk = FullViewingKey::from(&sk);
229 let ask = SpendAuthorizingKey::from(&sk);
230
231 let result = build_shielded_withdrawal_transition(
232 spends,
233 1_000_000,
234 CoreScript::new_p2pkh([1u8; 20]),
235 1,
236 Pooling::Never,
237 &change_address,
238 &fvk,
239 &ask,
240 Anchor::empty_tree(),
241 &TestProver,
242 [0u8; 36],
243 None,
244 platform_version,
245 );
246
247 assert!(result.is_err());
248 let err = result.unwrap_err().to_string();
249 assert!(
250 err.contains("exceeds total spendable value"),
251 "unexpected error: {}",
252 err
253 );
254 }
255
256 #[test]
261 fn test_shielded_withdrawal_amount_exceeds_i64_max_errors() {
262 let platform_version = PlatformVersion::latest();
265 let change_address = test_orchard_address();
266
267 let note = test_spendable_note(u64::MAX);
268 let spends = vec![note];
269
270 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
271 let fvk = FullViewingKey::from(&sk);
272 let ask = SpendAuthorizingKey::from(&sk);
273
274 let result = build_shielded_withdrawal_transition(
275 spends,
276 (i64::MAX as u64) + 1, CoreScript::new_p2pkh([1u8; 20]),
278 1,
279 Pooling::Never,
280 &change_address,
281 &fvk,
282 &ask,
283 Anchor::empty_tree(),
284 &TestProver,
285 [0u8; 36],
286 None,
287 platform_version,
288 );
289 assert!(result.is_err());
290 let err = result.unwrap_err().to_string();
291 assert!(
292 err.contains("exceeds maximum allowed value"),
293 "unexpected error: {}",
294 err
295 );
296 }
297
298 #[test]
299 fn test_shielded_withdrawal_fee_at_exact_upper_bound_accepted() {
300 let platform_version = PlatformVersion::latest();
304 let change_address = test_orchard_address();
305
306 let note = test_spendable_note(u64::MAX);
307 let spends = vec![note];
308
309 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
310 let fvk = FullViewingKey::from(&sk);
311 let ask = SpendAuthorizingKey::from(&sk);
312
313 let min_fee = crate::shielded::compute_minimum_shielded_fee(1, platform_version);
314 let fee_at_boundary = min_fee.saturating_mul(1000);
315
316 let result = build_shielded_withdrawal_transition(
317 spends,
318 100,
319 CoreScript::new_p2pkh([1u8; 20]),
320 1,
321 Pooling::Never,
322 &change_address,
323 &fvk,
324 &ask,
325 Anchor::empty_tree(),
326 &TestProver,
327 [0u8; 36],
328 Some(fee_at_boundary),
329 platform_version,
330 );
331 if let Err(err) = result {
336 let err = err.to_string();
337 assert!(
338 !err.contains("exceeds 1000x"),
339 "boundary value should not trigger upper-bound error: {}",
340 err
341 );
342 }
343 }
344
345 #[test]
346 fn test_shielded_withdrawal_fee_at_exact_min_accepted() {
347 let platform_version = PlatformVersion::latest();
350 let change_address = test_orchard_address();
351
352 let note = test_spendable_note(1_000_000);
353 let spends = vec![note];
354
355 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
356 let fvk = FullViewingKey::from(&sk);
357 let ask = SpendAuthorizingKey::from(&sk);
358
359 let min_fee = crate::shielded::compute_minimum_shielded_fee(1, platform_version);
360
361 let result = build_shielded_withdrawal_transition(
362 spends,
363 100,
364 CoreScript::new_p2pkh([1u8; 20]),
365 1,
366 Pooling::Never,
367 &change_address,
368 &fvk,
369 &ask,
370 Anchor::empty_tree(),
371 &TestProver,
372 [0u8; 36],
373 Some(min_fee),
374 platform_version,
375 );
376 if let Err(err) = result {
380 let err = err.to_string();
381 assert!(
382 !err.contains("below minimum required fee"),
383 "fee at min must not trip the lower bound: {}",
384 err
385 );
386 }
387 }
388
389 #[test]
390 fn test_shielded_withdrawal_zero_spends_errors() {
391 let platform_version = PlatformVersion::latest();
393 let change_address = test_orchard_address();
394
395 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
396 let fvk = FullViewingKey::from(&sk);
397 let ask = SpendAuthorizingKey::from(&sk);
398
399 let result = build_shielded_withdrawal_transition(
400 vec![],
401 1,
402 CoreScript::new_p2pkh([1u8; 20]),
403 1,
404 Pooling::Never,
405 &change_address,
406 &fvk,
407 &ask,
408 Anchor::empty_tree(),
409 &TestProver,
410 [0u8; 36],
411 None,
412 platform_version,
413 );
414 assert!(result.is_err());
415 let err = result.unwrap_err().to_string();
416 assert!(
417 err.contains("exceeds total spendable value"),
418 "unexpected error: {}",
419 err
420 );
421 }
422}