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}