1use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey};
2
3use crate::address_funds::{OrchardAddress, PlatformAddress};
4use crate::fee::Credits;
5use crate::shielded::compute_shielded_unshield_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)]
40pub fn build_unshield_transition<P: OrchardProver>(
41 spends: Vec<SpendableNote>,
42 output_address: PlatformAddress,
43 unshield_amount: u64,
44 change_address: &OrchardAddress,
45 fvk: &FullViewingKey,
46 ask: &SpendAuthorizingKey,
47 anchor: Anchor,
48 prover: &P,
49 memo: [u8; 36],
50 platform_version: &PlatformVersion,
51) -> Result<(StateTransition, Credits), ProtocolError> {
52 if unshield_amount > i64::MAX as u64 {
53 return Err(ProtocolError::ShieldedBuildError(format!(
54 "unshield amount {} exceeds maximum allowed value {}",
55 unshield_amount,
56 i64::MAX as u64
57 )));
58 }
59
60 let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum();
61
62 let num_actions = spends.len().max(2);
68 let fee = compute_shielded_unshield_fee(num_actions, platform_version)?;
73
74 let required = unshield_amount.checked_add(fee).ok_or_else(|| {
75 ProtocolError::ShieldedBuildError("fee + unshield_amount overflows u64".to_string())
76 })?;
77 if required > total_spent {
78 return Err(ProtocolError::ShieldedBuildError(format!(
79 "unshield amount {} + fee {} = {} exceeds total spendable value {}",
80 unshield_amount, fee, required, total_spent
81 )));
82 }
83
84 let change_amount = total_spent - required;
85
86 let extra_sighash_data = crate::shielded::unshield_extra_sighash_data(
90 &output_address.to_bytes(),
91 required,
92 platform_version,
93 )?;
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 let state_transition = 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 Ok((state_transition, fee))
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::shielded::builder::test_helpers::{
125 test_orchard_address, test_spendable_note, TestProver,
126 };
127
128 #[test]
129 fn test_unshield_insufficient_funds() {
130 let platform_version = PlatformVersion::latest();
131 let change_address = test_orchard_address();
132 let output_address = PlatformAddress::P2pkh([1u8; 20]);
133
134 let note = test_spendable_note(100);
135 let spends = vec![note];
136
137 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
138 .expect("valid spending key bytes");
139 let fvk = FullViewingKey::from(&sk);
140 let ask = SpendAuthorizingKey::from(&sk);
141
142 let result = build_unshield_transition(
143 spends,
144 output_address,
145 1_000_000,
146 &change_address,
147 &fvk,
148 &ask,
149 Anchor::empty_tree(),
150 &TestProver,
151 [0u8; 36],
152 platform_version,
153 );
154
155 assert!(result.is_err());
156 let err = result.unwrap_err().to_string();
157 assert!(
158 err.contains("exceeds total spendable value"),
159 "unexpected error: {}",
160 err
161 );
162 }
163
164 #[test]
169 fn test_unshield_amount_exceeds_i64_max_errors() {
170 let platform_version = PlatformVersion::latest();
171 let change_address = test_orchard_address();
172 let output_address = PlatformAddress::P2pkh([1u8; 20]);
173
174 let note = test_spendable_note(u64::MAX);
175 let spends = vec![note];
176
177 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
178 let fvk = FullViewingKey::from(&sk);
179 let ask = SpendAuthorizingKey::from(&sk);
180
181 let result = build_unshield_transition(
182 spends,
183 output_address,
184 (i64::MAX as u64) + 1, &change_address,
186 &fvk,
187 &ask,
188 Anchor::empty_tree(),
189 &TestProver,
190 [0u8; 36],
191 platform_version,
192 );
193 assert!(result.is_err());
194 let err = result.unwrap_err().to_string();
195 assert!(
196 err.contains("exceeds maximum allowed value"),
197 "unexpected error: {}",
198 err
199 );
200 }
201
202 #[test]
203 fn test_unshield_amount_exceeds_spendable_with_default_fee() {
204 let platform_version = PlatformVersion::latest();
207 let change_address = test_orchard_address();
208 let output_address = PlatformAddress::P2pkh([1u8; 20]);
209
210 let note = test_spendable_note(5_000);
211 let spends = vec![note];
212
213 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
214 let fvk = FullViewingKey::from(&sk);
215 let ask = SpendAuthorizingKey::from(&sk);
216
217 let result = build_unshield_transition(
218 spends,
219 output_address,
220 6_000, &change_address,
222 &fvk,
223 &ask,
224 Anchor::empty_tree(),
225 &TestProver,
226 [0u8; 36],
227 platform_version,
228 );
229 let err = result.unwrap_err().to_string();
230 assert!(
231 err.contains("exceeds total spendable value"),
232 "unexpected error: {}",
233 err
234 );
235 }
236
237 #[test]
238 fn test_unshield_zero_spends_errors() {
239 let platform_version = PlatformVersion::latest();
240 let change_address = test_orchard_address();
241 let output_address = PlatformAddress::P2pkh([1u8; 20]);
242
243 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
244 let fvk = FullViewingKey::from(&sk);
245 let ask = SpendAuthorizingKey::from(&sk);
246
247 let result = build_unshield_transition(
248 vec![],
249 output_address,
250 1,
251 &change_address,
252 &fvk,
253 &ask,
254 Anchor::empty_tree(),
255 &TestProver,
256 [0u8; 36],
257 platform_version,
258 );
259 assert!(result.is_err());
260 let err = result.unwrap_err().to_string();
261 assert!(
262 err.contains("exceeds total spendable value"),
263 "unexpected error: {}",
264 err
265 );
266 }
267
268 #[test]
269 fn test_unshield_fee_default_sufficient_value_reaches_add_spend() {
270 let platform_version = PlatformVersion::latest();
274 let change_address = test_orchard_address();
275 let output_address = PlatformAddress::P2pkh([1u8; 20]);
276
277 let min_fee = crate::shielded::compute_shielded_unshield_fee(2, platform_version)
278 .expect("fee computation should not overflow");
279 let unshield_amount = 42u64;
280 let note = test_spendable_note(unshield_amount + min_fee);
281 let spends = vec![note];
282
283 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
284 let fvk = FullViewingKey::from(&sk);
285 let ask = SpendAuthorizingKey::from(&sk);
286
287 let result = build_unshield_transition(
288 spends,
289 output_address,
290 unshield_amount,
291 &change_address,
292 &fvk,
293 &ask,
294 Anchor::empty_tree(),
295 &TestProver,
296 [0u8; 36],
297 platform_version,
298 );
299 let err_msg = result.unwrap_err().to_string();
300 assert!(
301 err_msg.contains("failed to add spend")
302 || err_msg.contains("anchor")
303 || err_msg.contains("AnchorMismatch"),
304 "expected downstream add_spend error, got: {}",
305 err_msg
306 );
307 }
308}