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_shielded_withdrawal_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)]
43pub fn build_shielded_withdrawal_transition<P: OrchardProver>(
44 spends: Vec<SpendableNote>,
45 withdrawal_amount: u64,
46 output_script: CoreScript,
47 core_fee_per_byte: u32,
48 pooling: Pooling,
49 change_address: &OrchardAddress,
50 fvk: &FullViewingKey,
51 ask: &SpendAuthorizingKey,
52 anchor: Anchor,
53 prover: &P,
54 memo: [u8; 36],
55 platform_version: &PlatformVersion,
56) -> Result<(StateTransition, Credits), ProtocolError> {
57 if withdrawal_amount > i64::MAX as u64 {
58 return Err(ProtocolError::ShieldedBuildError(format!(
59 "withdrawal amount {} exceeds maximum allowed value {}",
60 withdrawal_amount,
61 i64::MAX as u64
62 )));
63 }
64
65 let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum();
66
67 let num_actions = spends.len().max(2);
74 let fee = compute_shielded_withdrawal_fee(num_actions, platform_version)?;
79
80 let required = withdrawal_amount.checked_add(fee).ok_or_else(|| {
81 ProtocolError::ShieldedBuildError("fee + withdrawal_amount overflows u64".to_string())
82 })?;
83 if required > total_spent {
84 return Err(ProtocolError::ShieldedBuildError(format!(
85 "withdrawal amount {} + fee {} = {} exceeds total spendable value {}",
86 withdrawal_amount, fee, required, total_spent
87 )));
88 }
89
90 let change_amount = total_spent - required;
91
92 let extra_sighash_data = crate::shielded::shielded_withdrawal_extra_sighash_data(
96 output_script.as_bytes(),
97 required,
98 core_fee_per_byte,
99 pooling,
100 platform_version,
101 )?;
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 let state_transition = 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 Ok((state_transition, fee))
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::shielded::builder::test_helpers::{
135 test_orchard_address, test_spendable_note, TestProver,
136 };
137
138 #[test]
139 fn test_shielded_withdrawal_insufficient_funds() {
140 let platform_version = PlatformVersion::latest();
141 let change_address = test_orchard_address();
142
143 let note = test_spendable_note(100);
144 let spends = vec![note];
145
146 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32])
147 .expect("valid spending key bytes");
148 let fvk = FullViewingKey::from(&sk);
149 let ask = SpendAuthorizingKey::from(&sk);
150
151 let result = build_shielded_withdrawal_transition(
152 spends,
153 1_000_000,
154 CoreScript::new_p2pkh([1u8; 20]),
155 1,
156 Pooling::Never,
157 &change_address,
158 &fvk,
159 &ask,
160 Anchor::empty_tree(),
161 &TestProver,
162 [0u8; 36],
163 platform_version,
164 );
165
166 assert!(result.is_err());
167 let err = result.unwrap_err().to_string();
168 assert!(
169 err.contains("exceeds total spendable value"),
170 "unexpected error: {}",
171 err
172 );
173 }
174
175 #[test]
180 fn test_shielded_withdrawal_amount_exceeds_i64_max_errors() {
181 let platform_version = PlatformVersion::latest();
184 let change_address = test_orchard_address();
185
186 let note = test_spendable_note(u64::MAX);
187 let spends = vec![note];
188
189 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
190 let fvk = FullViewingKey::from(&sk);
191 let ask = SpendAuthorizingKey::from(&sk);
192
193 let result = build_shielded_withdrawal_transition(
194 spends,
195 (i64::MAX as u64) + 1, 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 platform_version,
206 );
207 assert!(result.is_err());
208 let err = result.unwrap_err().to_string();
209 assert!(
210 err.contains("exceeds maximum allowed value"),
211 "unexpected error: {}",
212 err
213 );
214 }
215
216 #[test]
217 fn test_shielded_withdrawal_zero_spends_errors() {
218 let platform_version = PlatformVersion::latest();
220 let change_address = test_orchard_address();
221
222 let sk = grovedb_commitment_tree::SpendingKey::from_bytes([42u8; 32]).expect("valid sk");
223 let fvk = FullViewingKey::from(&sk);
224 let ask = SpendAuthorizingKey::from(&sk);
225
226 let result = build_shielded_withdrawal_transition(
227 vec![],
228 1,
229 CoreScript::new_p2pkh([1u8; 20]),
230 1,
231 Pooling::Never,
232 &change_address,
233 &fvk,
234 &ask,
235 Anchor::empty_tree(),
236 &TestProver,
237 [0u8; 36],
238 platform_version,
239 );
240 assert!(result.is_err());
241 let err = result.unwrap_err().to_string();
242 assert!(
243 err.contains("exceeds total spendable value"),
244 "unexpected error: {}",
245 err
246 );
247 }
248}