1use grovedb_commitment_tree::{
2 Anchor, Builder, BundleType, DashMemo, Flags, FullViewingKey, NoteValue, PaymentAddress, Scope,
3 SpendAuthorizingKey,
4};
5
6use crate::address_funds::OrchardAddress;
7use crate::balances::credits::TokenAmount;
8use crate::data_contract::TokenContractPosition;
9use crate::fee::Credits;
10use crate::shielded::{
11 compute_token_purchase_from_shielded_pool_fee,
12 compute_token_shielded_transfer_with_shielded_fee_fee,
13 compute_token_unshield_with_shielded_fee_fee, token_pool_fee_bundle_extra_sighash_data,
14 token_purchase_from_shielded_pool_extra_sighash_data,
15 token_shielded_transfer_with_shielded_fee_extra_sighash_data,
16 token_unshield_with_shielded_fee_extra_sighash_data, SerializedAction,
17 TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE,
18 TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE,
19};
20use crate::state_transition::token_purchase_from_shielded_pool_transition::methods::TokenPurchaseFromShieldedPoolTransitionMethodsV0;
21use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition;
22use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::methods::TokenShieldedTransferWithShieldedFeeTransitionMethodsV0;
23use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition;
24use crate::state_transition::token_unshield_with_shielded_fee_transition::methods::TokenUnshieldWithShieldedFeeTransitionMethodsV0;
25use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition;
26use crate::state_transition::StateTransition;
27use crate::ProtocolError;
28use platform_value::Identifier;
29use platform_version::version::PlatformVersion;
30
31use super::{
32 build_spend_bundle, prove_and_sign_bundle, serialize_authorized_bundle, OrchardProver,
33 SerializedBundle, SpendableNote,
34};
35
36pub struct ShieldedFeePayer<'a> {
39 pub spends: Vec<SpendableNote>,
41 pub change_address: &'a OrchardAddress,
43 pub fvk: &'a FullViewingKey,
45 pub ask: &'a SpendAuthorizingKey,
47 pub anchor: Anchor,
49}
50
51pub struct TokenPoolSpender<'a> {
53 pub spends: Vec<SpendableNote>,
55 pub change_address: &'a OrchardAddress,
57 pub fvk: &'a FullViewingKey,
59 pub ask: &'a SpendAuthorizingKey,
61 pub anchor: Anchor,
63}
64
65fn total_value(spends: &[SpendableNote]) -> Result<u64, ProtocolError> {
66 spends
67 .iter()
68 .try_fold(0u64, |total, spend| {
69 total.checked_add(spend.note.value().inner())
70 })
71 .ok_or_else(|| {
72 ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string())
73 })
74}
75
76#[allow(clippy::too_many_arguments)]
80fn build_fee_bundle<P: OrchardProver>(
81 state_transition_type: u8,
82 token_id: &Identifier,
83 token_actions: &[SerializedAction],
84 payer: ShieldedFeePayer<'_>,
85 credits_leaving: Credits,
86 expected_actions: usize,
87 prover: &P,
88 platform_version: &PlatformVersion,
89) -> Result<SerializedBundle, ProtocolError> {
90 let total_spent = total_value(&payer.spends)?;
91 if credits_leaving > total_spent {
92 return Err(ProtocolError::ShieldedBuildError(format!(
93 "credits leaving the pool {} exceed the total spendable value {}",
94 credits_leaving, total_spent
95 )));
96 }
97 let change_amount = total_spent - credits_leaving;
98 let extra_sighash_data = token_pool_fee_bundle_extra_sighash_data(
99 state_transition_type,
100 &token_id.to_buffer(),
101 token_actions,
102 platform_version,
103 )?;
104 let bundle = build_spend_bundle(
105 payer.spends,
106 payer.change_address,
107 change_amount,
108 [0u8; 36],
109 payer.fvk,
110 payer.ask,
111 payer.anchor,
112 prover,
113 &extra_sighash_data,
114 )?;
115 let sb = serialize_authorized_bundle(&bundle);
116 if sb.value_balance != credits_leaving as i64 {
117 return Err(ProtocolError::ShieldedBuildError(format!(
118 "fee bundle value balance {} does not equal the credits leaving the pool {}",
119 sb.value_balance, credits_leaving
120 )));
121 }
122 if sb.actions.len() != expected_actions {
123 return Err(ProtocolError::ShieldedBuildError(format!(
124 "fee bundle has {} actions, the fee was computed for {}",
125 sb.actions.len(),
126 expected_actions
127 )));
128 }
129 Ok(sb)
130}
131
132#[allow(clippy::too_many_arguments)]
136pub fn build_token_shielded_transfer_with_shielded_fee_transition<P: OrchardProver>(
137 token_id: Identifier,
138 data_contract_id: Identifier,
139 token_contract_position: TokenContractPosition,
140 spender: TokenPoolSpender<'_>,
141 recipient: &OrchardAddress,
142 transfer_amount: TokenAmount,
143 memo: [u8; 36],
144 fee_payer: ShieldedFeePayer<'_>,
145 prover: &P,
146 platform_version: &PlatformVersion,
147) -> Result<(StateTransition, Credits), ProtocolError> {
148 if transfer_amount == 0 {
149 return Err(ProtocolError::ShieldedBuildError(
150 "token shielded transfer amount must be greater than zero".to_string(),
151 ));
152 }
153 let total_spent = total_value(&spender.spends)?;
154 if transfer_amount > total_spent {
155 return Err(ProtocolError::ShieldedBuildError(format!(
156 "token shielded transfer amount {} exceeds total spendable value {}",
157 transfer_amount, total_spent
158 )));
159 }
160 let change_amount = total_spent - transfer_amount;
161 let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, spender.anchor);
162 for spend in spender.spends {
163 builder
164 .add_spend(spender.fvk.clone(), spend.note, spend.merkle_path)
165 .map_err(|e| {
166 ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
167 })?;
168 }
169 let sender_ovk = spender.fvk.to_ovk(Scope::External);
170 builder
171 .add_output(
172 Some(sender_ovk.clone()),
173 PaymentAddress::from(recipient),
174 NoteValue::from_raw(transfer_amount),
175 memo,
176 )
177 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
178 if change_amount > 0 {
179 builder
180 .add_output(
181 Some(sender_ovk),
182 PaymentAddress::from(spender.change_address),
183 NoteValue::from_raw(change_amount),
184 [0u8; 36],
185 )
186 .map_err(|e| {
187 ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e))
188 })?;
189 }
190 let token_extra = token_shielded_transfer_with_shielded_fee_extra_sighash_data(
191 &token_id.to_buffer(),
192 platform_version,
193 )?;
194 let token_bundle = prove_and_sign_bundle(
195 builder,
196 prover,
197 std::slice::from_ref(spender.ask),
198 &token_extra,
199 )?;
200 let token_sb = serialize_authorized_bundle(&token_bundle);
201 if token_sb.value_balance != 0 {
202 return Err(ProtocolError::ShieldedBuildError(format!(
203 "token shielded transfer bundle value balance must be zero, got {}",
204 token_sb.value_balance
205 )));
206 }
207 let fee_actions = fee_payer.spends.len().max(2);
208 let fee = compute_token_shielded_transfer_with_shielded_fee_fee(
209 token_sb.actions.len(),
210 fee_actions,
211 platform_version,
212 )?;
213 let fee_sb = build_fee_bundle(
214 TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE,
215 &token_id,
216 &token_sb.actions,
217 fee_payer,
218 fee,
219 fee_actions,
220 prover,
221 platform_version,
222 )?;
223 let transition = TokenShieldedTransferWithShieldedFeeTransition::try_from_bundles(
224 data_contract_id,
225 token_contract_position,
226 token_id,
227 token_sb.actions,
228 token_sb.anchor,
229 token_sb.proof,
230 token_sb.binding_signature,
231 fee_sb.actions,
232 fee_sb.anchor,
233 fee_sb.proof,
234 fee_sb.binding_signature,
235 fee,
236 platform_version,
237 )?;
238 Ok((transition, fee))
239}
240
241#[allow(clippy::too_many_arguments)]
245pub fn build_token_unshield_with_shielded_fee_transition<P: OrchardProver>(
246 token_id: Identifier,
247 data_contract_id: Identifier,
248 token_contract_position: TokenContractPosition,
249 spender: TokenPoolSpender<'_>,
250 recipient_id: Identifier,
251 amount: TokenAmount,
252 memo: [u8; 36],
253 fee_payer: ShieldedFeePayer<'_>,
254 prover: &P,
255 platform_version: &PlatformVersion,
256) -> Result<(StateTransition, Credits), ProtocolError> {
257 if amount == 0 {
258 return Err(ProtocolError::ShieldedBuildError(
259 "token unshield amount must be greater than zero".to_string(),
260 ));
261 }
262 let total_spent = total_value(&spender.spends)?;
263 if amount > total_spent {
264 return Err(ProtocolError::ShieldedBuildError(format!(
265 "token unshield amount {} exceeds total spendable value {}",
266 amount, total_spent
267 )));
268 }
269 let change_amount = total_spent - amount;
270 let token_extra = token_unshield_with_shielded_fee_extra_sighash_data(
271 &token_id.to_buffer(),
272 &recipient_id.to_buffer(),
273 amount,
274 platform_version,
275 )?;
276 let token_bundle = build_spend_bundle(
277 spender.spends,
278 spender.change_address,
279 change_amount,
280 memo,
281 spender.fvk,
282 spender.ask,
283 spender.anchor,
284 prover,
285 &token_extra,
286 )?;
287 let token_sb = serialize_authorized_bundle(&token_bundle);
288 if token_sb.value_balance != amount as i64 {
289 return Err(ProtocolError::ShieldedBuildError(format!(
290 "token unshield bundle value balance {} does not equal the amount {}",
291 token_sb.value_balance, amount
292 )));
293 }
294 let fee_actions = fee_payer.spends.len().max(2);
295 let fee = compute_token_unshield_with_shielded_fee_fee(
296 token_sb.actions.len(),
297 fee_actions,
298 platform_version,
299 )?;
300 let fee_sb = build_fee_bundle(
301 TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE,
302 &token_id,
303 &token_sb.actions,
304 fee_payer,
305 fee,
306 fee_actions,
307 prover,
308 platform_version,
309 )?;
310 let transition = TokenUnshieldWithShieldedFeeTransition::try_from_bundles(
311 data_contract_id,
312 token_contract_position,
313 token_id,
314 recipient_id,
315 amount,
316 token_sb.actions,
317 token_sb.anchor,
318 token_sb.proof,
319 token_sb.binding_signature,
320 fee_sb.actions,
321 fee_sb.anchor,
322 fee_sb.proof,
323 fee_sb.binding_signature,
324 fee,
325 platform_version,
326 )?;
327 Ok((transition, fee))
328}
329
330#[allow(clippy::too_many_arguments)]
335pub fn build_token_purchase_from_shielded_pool_transition<P: OrchardProver>(
336 token_id: Identifier,
337 data_contract_id: Identifier,
338 token_contract_position: TokenContractPosition,
339 recipient: &OrchardAddress,
340 recipient_fvk: &FullViewingKey,
341 token_count: TokenAmount,
342 total_agreed_price: Credits,
343 memo: [u8; 36],
344 fee_payer: ShieldedFeePayer<'_>,
345 prover: &P,
346 platform_version: &PlatformVersion,
347) -> Result<(StateTransition, Credits), ProtocolError> {
348 if token_count == 0 {
349 return Err(ProtocolError::ShieldedBuildError(
350 "token purchase count must be greater than zero".to_string(),
351 ));
352 }
353 if token_count > i64::MAX as u64 {
354 return Err(ProtocolError::ShieldedBuildError(format!(
355 "token purchase count {} exceeds maximum allowed value {}",
356 token_count,
357 i64::MAX as u64
358 )));
359 }
360 let token_extra = token_purchase_from_shielded_pool_extra_sighash_data(
363 &token_id.to_buffer(),
364 token_count,
365 total_agreed_price,
366 platform_version,
367 )?;
368 let mut builder = Builder::<DashMemo>::new(
369 BundleType::Transactional {
370 flags: Flags::SPENDS_DISABLED,
371 bundle_required: false,
372 },
373 Anchor::empty_tree(),
374 );
375 builder
376 .add_output(
377 Some(recipient_fvk.to_ovk(Scope::External)),
378 PaymentAddress::from(recipient),
379 NoteValue::from_raw(token_count),
380 memo,
381 )
382 .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
383 let token_sb =
384 serialize_authorized_bundle(&prove_and_sign_bundle(builder, prover, &[], &token_extra)?);
385 if token_sb.value_balance != -(token_count as i64) {
386 return Err(ProtocolError::ShieldedBuildError(format!(
387 "token purchase bundle value balance {} does not equal minus the token count {}",
388 token_sb.value_balance, token_count
389 )));
390 }
391 let fee_actions = fee_payer.spends.len().max(2);
392 let fee = compute_token_purchase_from_shielded_pool_fee(
393 token_sb.actions.len(),
394 fee_actions,
395 platform_version,
396 )?;
397 let credits_leaving = total_agreed_price.checked_add(fee).ok_or_else(|| {
398 ProtocolError::ShieldedBuildError("price + fee overflows u64".to_string())
399 })?;
400 let fee_sb = build_fee_bundle(
401 TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE,
402 &token_id,
403 &token_sb.actions,
404 fee_payer,
405 credits_leaving,
406 fee_actions,
407 prover,
408 platform_version,
409 )?;
410 let transition = TokenPurchaseFromShieldedPoolTransition::try_from_bundles(
411 data_contract_id,
412 token_contract_position,
413 token_id,
414 token_count,
415 total_agreed_price,
416 token_sb.actions,
417 token_sb.anchor,
418 token_sb.proof,
419 token_sb.binding_signature,
420 fee_sb.actions,
421 fee_sb.anchor,
422 fee_sb.proof,
423 fee_sb.binding_signature,
424 credits_leaving,
425 platform_version,
426 )?;
427 Ok((transition, fee))
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::shielded::builder::test_helpers::{
434 test_orchard_address, test_spendable_note, TestProver,
435 };
436 use grovedb_commitment_tree::SpendingKey;
437
438 fn keys() -> (FullViewingKey, SpendAuthorizingKey) {
439 let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
440 (FullViewingKey::from(&sk), SpendAuthorizingKey::from(&sk))
441 }
442
443 #[test]
444 fn rejects_a_transfer_above_the_spendable_token_value() {
445 let (fvk, ask) = keys();
446 let address = test_orchard_address();
447 let err = build_token_shielded_transfer_with_shielded_fee_transition(
448 Identifier::from([1u8; 32]),
449 Identifier::from([2u8; 32]),
450 0,
451 TokenPoolSpender {
452 spends: vec![test_spendable_note(100)],
453 change_address: &address,
454 fvk: &fvk,
455 ask: &ask,
456 anchor: Anchor::empty_tree(),
457 },
458 &address,
459 1_000,
460 [0u8; 36],
461 ShieldedFeePayer {
462 spends: vec![test_spendable_note(1_000_000_000)],
463 change_address: &address,
464 fvk: &fvk,
465 ask: &ask,
466 anchor: Anchor::empty_tree(),
467 },
468 &TestProver,
469 PlatformVersion::latest(),
470 )
471 .expect_err("overspend must be rejected")
472 .to_string();
473 assert!(
474 err.contains("exceeds total spendable value"),
475 "unexpected error: {err}"
476 );
477 }
478
479 #[test]
480 fn rejects_a_zero_unshield_amount() {
481 let (fvk, ask) = keys();
482 let address = test_orchard_address();
483 let err = build_token_unshield_with_shielded_fee_transition(
484 Identifier::from([1u8; 32]),
485 Identifier::from([2u8; 32]),
486 0,
487 TokenPoolSpender {
488 spends: vec![test_spendable_note(100)],
489 change_address: &address,
490 fvk: &fvk,
491 ask: &ask,
492 anchor: Anchor::empty_tree(),
493 },
494 Identifier::from([3u8; 32]),
495 0,
496 [0u8; 36],
497 ShieldedFeePayer {
498 spends: vec![test_spendable_note(1_000_000_000)],
499 change_address: &address,
500 fvk: &fvk,
501 ask: &ask,
502 anchor: Anchor::empty_tree(),
503 },
504 &TestProver,
505 PlatformVersion::latest(),
506 )
507 .expect_err("zero amount must be rejected")
508 .to_string();
509 assert!(err.contains("greater than zero"), "unexpected error: {err}");
510 }
511
512 #[test]
513 fn rejects_a_purchase_count_outside_the_value_balance_range() {
514 let (fvk, ask) = keys();
515 let address = test_orchard_address();
516 let err = build_token_purchase_from_shielded_pool_transition(
517 Identifier::from([1u8; 32]),
518 Identifier::from([2u8; 32]),
519 0,
520 &address,
521 &fvk,
522 1u64 << 63,
523 1_000,
524 [0u8; 36],
525 ShieldedFeePayer {
526 spends: vec![test_spendable_note(1_000_000_000)],
527 change_address: &address,
528 fvk: &fvk,
529 ask: &ask,
530 anchor: Anchor::empty_tree(),
531 },
532 &TestProver,
533 PlatformVersion::latest(),
534 )
535 .expect_err("a count that cannot be negated as i64 must be rejected before proving")
536 .to_string();
537 assert!(
538 err.contains("exceeds maximum allowed value"),
539 "unexpected error: {err}"
540 );
541 }
542
543 #[test]
544 fn rejects_a_purchase_the_fee_notes_cannot_pay() {
545 let (fvk, ask) = keys();
546 let address = test_orchard_address();
547 let err = build_token_purchase_from_shielded_pool_transition(
548 Identifier::from([1u8; 32]),
549 Identifier::from([2u8; 32]),
550 0,
551 &address,
552 &fvk,
553 5,
554 1_000,
555 [0u8; 36],
556 ShieldedFeePayer {
557 spends: vec![test_spendable_note(10)],
558 change_address: &address,
559 fvk: &fvk,
560 ask: &ask,
561 anchor: Anchor::empty_tree(),
562 },
563 &TestProver,
564 PlatformVersion::latest(),
565 )
566 .expect_err("the fee notes cannot cover price plus fee")
567 .to_string();
568 assert!(
569 err.contains("exceed the total spendable value"),
570 "unexpected error: {err}"
571 );
572 }
573}