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