Skip to main content

dpp/shielded/builder/
shield.rs

1use std::collections::BTreeMap;
2
3use crate::address_funds::AddressFundsFeeStrategy;
4use crate::address_funds::{OrchardAddress, PlatformAddress};
5use crate::fee::Credits;
6use crate::identity::signer::Signer;
7use crate::prelude::{AddressNonce, UserFeeIncrease};
8use crate::state_transition::shield_transition::methods::ShieldTransitionMethodsV0;
9use crate::state_transition::shield_transition::ShieldTransition;
10use crate::state_transition::StateTransition;
11use crate::ProtocolError;
12use platform_version::version::PlatformVersion;
13
14use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver};
15
16/// Builds a Shield state transition (transparent platform addresses -> shielded pool).
17///
18/// Constructs an output-only Orchard bundle (no spends), proves it, signs the
19/// transparent input witnesses, and returns a ready-to-broadcast `StateTransition`.
20///
21/// # Parameters
22/// - `recipient` - Orchard address to receive the shielded note
23/// - `shield_amount` - Amount of credits to shield
24/// - `inputs` - Platform address inputs with their nonces and balances
25/// - `fee_strategy` - How to deduct fees from the transparent inputs
26/// - `signer` - Signs each input address witness (ECDSA)
27/// - `user_fee_increase` - Fee multiplier (0 = 100% base fee)
28/// - `prover` - Orchard prover (holds the Halo 2 proving key; cache with `OnceLock` — ~30s to build)
29/// - `memo` - 36-byte structured memo for the recipient (4-byte type tag + 32-byte payload)
30/// - `platform_version` - Protocol version
31#[allow(clippy::too_many_arguments)]
32pub async fn build_shield_transition<S: Signer<PlatformAddress>, P: OrchardProver>(
33    recipient: &OrchardAddress,
34    shield_amount: u64,
35    inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
36    fee_strategy: AddressFundsFeeStrategy,
37    signer: &S,
38    user_fee_increase: UserFeeIncrease,
39    prover: &P,
40    memo: [u8; 36],
41    platform_version: &PlatformVersion,
42) -> Result<StateTransition, ProtocolError> {
43    if fee_strategy.is_empty() {
44        return Err(ProtocolError::ShieldedBuildError(
45            "fee_strategy must have at least one step".to_string(),
46        ));
47    }
48
49    let bundle = build_output_only_bundle(recipient, shield_amount, memo, prover)?;
50    let sb = serialize_authorized_bundle(&bundle);
51
52    ShieldTransition::try_from_bundle_with_signer(
53        inputs,
54        sb.actions,
55        sb.value_balance.unsigned_abs(),
56        sb.anchor,
57        sb.proof,
58        sb.binding_signature,
59        fee_strategy,
60        signer,
61        user_fee_increase,
62        platform_version,
63    )
64    .await
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::address_funds::AddressFundsFeeStrategyStep;
71    use crate::address_funds::AddressWitness;
72    use crate::shielded::builder::test_helpers::{test_orchard_address, TestProver};
73    use platform_value::BinaryData;
74
75    /// A dummy signer that produces a fake 65-byte signature.
76    /// Only used to test the builder pipeline — the signature is not validated here.
77    #[derive(Debug)]
78    struct DummySigner;
79
80    #[async_trait::async_trait]
81    impl Signer<PlatformAddress> for DummySigner {
82        async fn sign(
83            &self,
84            _key: &PlatformAddress,
85            _data: &[u8],
86        ) -> Result<BinaryData, ProtocolError> {
87            Ok(BinaryData::new(vec![0u8; 65]))
88        }
89
90        async fn sign_create_witness(
91            &self,
92            _key: &PlatformAddress,
93            _data: &[u8],
94        ) -> Result<AddressWitness, ProtocolError> {
95            Ok(AddressWitness::P2pkh {
96                signature: BinaryData::new(vec![0u8; 65]),
97            })
98        }
99
100        fn can_sign_with(&self, _key: &PlatformAddress) -> bool {
101            true
102        }
103    }
104
105    #[tokio::test]
106    async fn test_build_shield_empty_fee_strategy() {
107        let recipient = test_orchard_address();
108        let platform_version = PlatformVersion::latest();
109        let result = build_shield_transition(
110            &recipient,
111            1000,
112            BTreeMap::new(),
113            vec![], // empty fee strategy
114            &DummySigner,
115            0,
116            &TestProver,
117            [0u8; 36],
118            platform_version,
119        )
120        .await;
121
122        assert!(result.is_err());
123        let err = result.unwrap_err().to_string();
124        assert!(
125            err.contains("fee_strategy must have at least one step"),
126            "unexpected error: {}",
127            err
128        );
129    }
130
131    #[tokio::test]
132    async fn test_build_shield_transition_valid() {
133        let recipient = test_orchard_address();
134        let platform_version = PlatformVersion::latest();
135        // Create a P2PKH address as input
136        let input_address = PlatformAddress::P2pkh([1u8; 20]);
137        let mut inputs = BTreeMap::new();
138        inputs.insert(input_address, (0u32, 100_000u64));
139
140        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
141
142        let result = build_shield_transition(
143            &recipient,
144            50_000,
145            inputs,
146            fee_strategy,
147            &DummySigner,
148            0,
149            &TestProver,
150            [0u8; 36],
151            platform_version,
152        )
153        .await;
154
155        assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
156        match result.unwrap() {
157            StateTransition::Shield(_) => {} // correct variant
158            other => panic!("expected Shield variant, got {:?}", other),
159        }
160    }
161
162    // ------------------------------------------------------------
163    // Extra coverage: error/edge paths not exercised above.
164    // ------------------------------------------------------------
165
166    #[tokio::test]
167    async fn test_build_shield_multiple_inputs_all_plumbed() {
168        // Multiple input addresses should each produce their own witness
169        // signature and flow through the downstream Shield transition.
170        let recipient = test_orchard_address();
171        let platform_version = PlatformVersion::latest();
172
173        let mut inputs = BTreeMap::new();
174        inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (0u32, 100_000u64));
175        inputs.insert(PlatformAddress::P2pkh([2u8; 20]), (0u32, 200_000u64));
176        inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (0u32, 300_000u64));
177
178        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
179
180        let result = build_shield_transition(
181            &recipient,
182            50_000,
183            inputs,
184            fee_strategy,
185            &DummySigner,
186            0,
187            &TestProver,
188            [0u8; 36],
189            platform_version,
190        )
191        .await;
192        assert!(
193            result.is_ok(),
194            "multi-input shield should succeed: {:?}",
195            result.err()
196        );
197    }
198
199    #[tokio::test]
200    async fn test_build_shield_user_fee_increase_non_zero_succeeds() {
201        // The user_fee_increase param just flows through as metadata.
202        // A non-zero value should not fail the bundle build.
203        let recipient = test_orchard_address();
204        let platform_version = PlatformVersion::latest();
205        let input_address = PlatformAddress::P2pkh([5u8; 20]);
206        let mut inputs = BTreeMap::new();
207        inputs.insert(input_address, (0u32, 500_000u64));
208
209        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
210
211        let result = build_shield_transition(
212            &recipient,
213            100_000,
214            inputs,
215            fee_strategy,
216            &DummySigner,
217            42, // non-zero fee increase
218            &TestProver,
219            [9u8; 36],
220            platform_version,
221        )
222        .await;
223        assert!(
224            result.is_ok(),
225            "non-zero user_fee_increase should succeed: {:?}",
226            result.err()
227        );
228    }
229
230    #[tokio::test]
231    async fn test_build_shield_memo_is_fully_plumbed() {
232        // Any 36-byte memo should be accepted — this test is a guard
233        // against accidental panics/regressions in memo handling.
234        let recipient = test_orchard_address();
235        let platform_version = PlatformVersion::latest();
236        let input_address = PlatformAddress::P2pkh([9u8; 20]);
237        let mut inputs = BTreeMap::new();
238        inputs.insert(input_address, (5u32, 200_000u64));
239
240        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
241        let mut memo = [0u8; 36];
242        for (i, b) in memo.iter_mut().enumerate() {
243            *b = i as u8;
244        }
245
246        let result = build_shield_transition(
247            &recipient,
248            80_000,
249            inputs,
250            fee_strategy,
251            &DummySigner,
252            0,
253            &TestProver,
254            memo,
255            platform_version,
256        )
257        .await;
258        assert!(
259            result.is_ok(),
260            "varied memo should succeed: {:?}",
261            result.err()
262        );
263    }
264}