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/// - `sender_ovk` - The sender's outgoing viewing key (External scope). With `Some`, the
31///   recipient output's `out_ciphertext` is encrypted under it so the sender can later
32///   recover the sent note (recipient, value, memo) from chain data via OVK recovery —
33///   the Zcash outgoing-transaction-history convention. With `None`, a random outgoing
34///   cipher key is used and the sent note is unrecoverable by anyone.
35/// - `platform_version` - Protocol version
36#[allow(clippy::too_many_arguments)]
37pub async fn build_shield_transition<S: Signer<PlatformAddress>, P: OrchardProver>(
38    recipient: &OrchardAddress,
39    shield_amount: u64,
40    inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
41    fee_strategy: AddressFundsFeeStrategy,
42    signer: &S,
43    user_fee_increase: UserFeeIncrease,
44    prover: &P,
45    memo: [u8; 36],
46    sender_ovk: Option<grovedb_commitment_tree::OutgoingViewingKey>,
47    platform_version: &PlatformVersion,
48) -> Result<StateTransition, ProtocolError> {
49    if fee_strategy.is_empty() {
50        return Err(ProtocolError::ShieldedBuildError(
51            "fee_strategy must have at least one step".to_string(),
52        ));
53    }
54
55    // Shield (Type 15) never pads with anonymity-set fillers — only the
56    // Type 18 ShieldFromAssetLock pool-seeding path does (`dummy_outputs`).
57    let bundle = build_output_only_bundle(recipient, shield_amount, memo, sender_ovk, 0, prover)?;
58    let sb = serialize_authorized_bundle(&bundle);
59
60    ShieldTransition::try_from_bundle_with_signer(
61        inputs,
62        sb.actions,
63        sb.value_balance.unsigned_abs(),
64        sb.anchor,
65        sb.proof,
66        sb.binding_signature,
67        fee_strategy,
68        signer,
69        user_fee_increase,
70        platform_version,
71    )
72    .await
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::address_funds::AddressFundsFeeStrategyStep;
79    use crate::address_funds::AddressWitness;
80    use crate::shielded::builder::test_helpers::{test_orchard_address, TestProver};
81    use platform_value::BinaryData;
82
83    /// A dummy signer that produces a fake 65-byte signature.
84    /// Only used to test the builder pipeline — the signature is not validated here.
85    #[derive(Debug)]
86    struct DummySigner;
87
88    #[async_trait::async_trait]
89    impl Signer<PlatformAddress> for DummySigner {
90        async fn sign(
91            &self,
92            _key: &PlatformAddress,
93            _data: &[u8],
94        ) -> Result<BinaryData, ProtocolError> {
95            Ok(BinaryData::new(vec![0u8; 65]))
96        }
97
98        async fn sign_create_witness(
99            &self,
100            _key: &PlatformAddress,
101            _data: &[u8],
102        ) -> Result<AddressWitness, ProtocolError> {
103            Ok(AddressWitness::P2pkh {
104                signature: BinaryData::new(vec![0u8; 65]),
105            })
106        }
107
108        fn can_sign_with(&self, _key: &PlatformAddress) -> bool {
109            true
110        }
111    }
112
113    #[tokio::test]
114    async fn test_build_shield_empty_fee_strategy() {
115        let recipient = test_orchard_address();
116        let platform_version = PlatformVersion::latest();
117        let result = build_shield_transition(
118            &recipient,
119            1000,
120            BTreeMap::new(),
121            vec![], // empty fee strategy
122            &DummySigner,
123            0,
124            &TestProver,
125            [0u8; 36],
126            None,
127            platform_version,
128        )
129        .await;
130
131        assert!(result.is_err());
132        let err = result.unwrap_err().to_string();
133        assert!(
134            err.contains("fee_strategy must have at least one step"),
135            "unexpected error: {}",
136            err
137        );
138    }
139
140    #[tokio::test]
141    async fn test_build_shield_transition_valid() {
142        let recipient = test_orchard_address();
143        let platform_version = PlatformVersion::latest();
144        // Create a P2PKH address as input
145        let input_address = PlatformAddress::P2pkh([1u8; 20]);
146        let mut inputs = BTreeMap::new();
147        inputs.insert(input_address, (0u32, 100_000u64));
148
149        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
150
151        let result = build_shield_transition(
152            &recipient,
153            50_000,
154            inputs,
155            fee_strategy,
156            &DummySigner,
157            0,
158            &TestProver,
159            [0u8; 36],
160            None,
161            platform_version,
162        )
163        .await;
164
165        assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
166        match result.unwrap() {
167            StateTransition::Shield(_) => {} // correct variant
168            other => panic!("expected Shield variant, got {:?}", other),
169        }
170    }
171
172    // ------------------------------------------------------------
173    // Extra coverage: error/edge paths not exercised above.
174    // ------------------------------------------------------------
175
176    #[tokio::test]
177    async fn test_build_shield_multiple_inputs_all_plumbed() {
178        // Multiple input addresses should each produce their own witness
179        // signature and flow through the downstream Shield transition.
180        let recipient = test_orchard_address();
181        let platform_version = PlatformVersion::latest();
182
183        let mut inputs = BTreeMap::new();
184        inputs.insert(PlatformAddress::P2pkh([1u8; 20]), (0u32, 100_000u64));
185        inputs.insert(PlatformAddress::P2pkh([2u8; 20]), (0u32, 200_000u64));
186        inputs.insert(PlatformAddress::P2pkh([3u8; 20]), (0u32, 300_000u64));
187
188        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
189
190        let result = build_shield_transition(
191            &recipient,
192            50_000,
193            inputs,
194            fee_strategy,
195            &DummySigner,
196            0,
197            &TestProver,
198            [0u8; 36],
199            None,
200            platform_version,
201        )
202        .await;
203        assert!(
204            result.is_ok(),
205            "multi-input shield should succeed: {:?}",
206            result.err()
207        );
208    }
209
210    #[tokio::test]
211    async fn test_build_shield_user_fee_increase_non_zero_succeeds() {
212        // The user_fee_increase param just flows through as metadata.
213        // A non-zero value should not fail the bundle build.
214        let recipient = test_orchard_address();
215        let platform_version = PlatformVersion::latest();
216        let input_address = PlatformAddress::P2pkh([5u8; 20]);
217        let mut inputs = BTreeMap::new();
218        inputs.insert(input_address, (0u32, 500_000u64));
219
220        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
221
222        let result = build_shield_transition(
223            &recipient,
224            100_000,
225            inputs,
226            fee_strategy,
227            &DummySigner,
228            42, // non-zero fee increase
229            &TestProver,
230            [9u8; 36],
231            None,
232            platform_version,
233        )
234        .await;
235        assert!(
236            result.is_ok(),
237            "non-zero user_fee_increase should succeed: {:?}",
238            result.err()
239        );
240    }
241
242    #[tokio::test]
243    async fn test_build_shield_memo_is_fully_plumbed() {
244        // Any 36-byte memo should be accepted — this test is a guard
245        // against accidental panics/regressions in memo handling.
246        let recipient = test_orchard_address();
247        let platform_version = PlatformVersion::latest();
248        let input_address = PlatformAddress::P2pkh([9u8; 20]);
249        let mut inputs = BTreeMap::new();
250        inputs.insert(input_address, (5u32, 200_000u64));
251
252        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
253        let mut memo = [0u8; 36];
254        for (i, b) in memo.iter_mut().enumerate() {
255            *b = i as u8;
256        }
257
258        let result = build_shield_transition(
259            &recipient,
260            80_000,
261            inputs,
262            fee_strategy,
263            &DummySigner,
264            0,
265            &TestProver,
266            memo,
267            None,
268            platform_version,
269        )
270        .await;
271        assert!(
272            result.is_ok(),
273            "varied memo should succeed: {:?}",
274            result.err()
275        );
276    }
277}