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}