Skip to main content

dpp/shielded/builder/
shield_from_asset_lock.rs

1use crate::address_funds::OrchardAddress;
2use crate::prelude::AssetLockProof;
3use crate::state_transition::shield_from_asset_lock_transition::methods::ShieldFromAssetLockTransitionMethodsV0;
4use crate::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition;
5use crate::state_transition::StateTransition;
6use crate::ProtocolError;
7use platform_version::version::PlatformVersion;
8
9use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver};
10
11/// Builds a ShieldFromAssetLock state transition (core asset lock -> shielded pool).
12///
13/// Like Shield, constructs an output-only Orchard bundle. The funds come from
14/// a core asset lock proof rather than platform address inputs.
15///
16/// # Parameters
17/// - `recipient` - Orchard address to receive the shielded note
18/// - `shield_amount` - Amount of credits to shield (from the asset lock)
19/// - `asset_lock_proof` - Proof that funds are locked on core chain
20/// - `asset_lock_private_key` - Private key for the asset lock (signs the transition)
21/// - `prover` - Orchard prover (holds the Halo 2 proving key)
22/// - `memo` - 36-byte structured memo for the recipient (4-byte type tag + 32-byte payload)
23/// - `platform_version` - Protocol version
24#[allow(clippy::too_many_arguments)]
25pub fn build_shield_from_asset_lock_transition<P: OrchardProver>(
26    recipient: &OrchardAddress,
27    shield_amount: u64,
28    asset_lock_proof: AssetLockProof,
29    asset_lock_private_key: &[u8],
30    prover: &P,
31    memo: [u8; 36],
32    platform_version: &PlatformVersion,
33) -> Result<StateTransition, ProtocolError> {
34    let bundle = build_output_only_bundle(recipient, shield_amount, memo, prover)?;
35    let sb = serialize_authorized_bundle(&bundle);
36
37    // For output-only bundles, Orchard value_balance is negative (value flowing in).
38    // Convert to u64 (absolute amount entering the pool).
39    let value_balance = sb
40        .value_balance
41        .checked_neg()
42        .and_then(|v| u64::try_from(v).ok())
43        .ok_or_else(|| {
44            ProtocolError::ShieldedBuildError(
45                "shield_from_asset_lock: bundle value_balance is not negative".to_string(),
46            )
47        })?;
48
49    ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle(
50        asset_lock_proof,
51        asset_lock_private_key,
52        sb.actions,
53        value_balance,
54        sb.anchor,
55        sb.proof,
56        sb.binding_signature,
57        platform_version,
58    )
59}
60
61#[cfg(test)]
62mod tests {
63    use super::super::{build_output_only_bundle, serialize_authorized_bundle};
64    use crate::shielded::builder::test_helpers::{test_orchard_address, TestProver};
65
66    /// Verifies that an output-only bundle produces a negative value_balance
67    /// (value flowing into the pool), which is the precondition for
68    /// shield_from_asset_lock's value_balance conversion.
69    #[test]
70    fn test_output_only_bundle_value_balance_is_negative() {
71        let recipient = test_orchard_address();
72        let amount = 50_000u64;
73
74        let bundle = build_output_only_bundle(&recipient, amount, [0u8; 36], &TestProver)
75            .expect("bundle should build successfully");
76        let sb = serialize_authorized_bundle(&bundle);
77
78        // Output-only bundles have negative value_balance (value entering the pool)
79        assert!(
80            sb.value_balance < 0,
81            "expected negative value_balance, got {}",
82            sb.value_balance
83        );
84
85        // The absolute value should match the shield amount
86        let abs_balance = sb
87            .value_balance
88            .checked_neg()
89            .and_then(|v| u64::try_from(v).ok())
90            .expect("value_balance should be safely negatable");
91        assert_eq!(abs_balance, amount);
92    }
93
94    // -------------------------------------------------------------
95    // Arithmetic edge cases on the value_balance conversion branch
96    // (the `checked_neg().and_then(u64::try_from)` chain).
97    // -------------------------------------------------------------
98
99    #[test]
100    fn test_value_balance_positive_would_fail_conversion() {
101        // This is a regression-guard: if a *positive* value_balance ever
102        // reached the conversion path, `checked_neg` on i64::MIN would
103        // overflow and the `.try_from::<u64>` on a negative value would
104        // fail. We simulate by constructing a hypothetical value_balance
105        // scenario rather than calling the high-level builder (which
106        // requires a real AssetLockProof).
107        let positive: i64 = 123;
108        let converted = positive.checked_neg().and_then(|v| u64::try_from(v).ok());
109        assert!(converted.is_none(), "negative result cannot be u64");
110
111        let zero: i64 = 0;
112        let converted_zero = zero.checked_neg().and_then(|v| u64::try_from(v).ok());
113        assert_eq!(converted_zero, Some(0));
114
115        let negative: i64 = -42;
116        let converted_neg = negative.checked_neg().and_then(|v| u64::try_from(v).ok());
117        assert_eq!(converted_neg, Some(42));
118    }
119
120    #[test]
121    fn test_output_only_various_amounts_negative_balance() {
122        // Try several amounts to ensure the helper consistently produces a
123        // negative value_balance equal in magnitude to the requested amount.
124        for amount in [1u64, 100, 1_000_000, u32::MAX as u64] {
125            let recipient = test_orchard_address();
126            let bundle = build_output_only_bundle(&recipient, amount, [0u8; 36], &TestProver)
127                .expect("bundle should build");
128            let sb = serialize_authorized_bundle(&bundle);
129            assert_eq!(
130                sb.value_balance,
131                -(amount as i64),
132                "value_balance mismatch for amount {}",
133                amount
134            );
135        }
136    }
137}