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