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 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}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::address_funds::AddressFundsFeeStrategyStep;
70    use crate::address_funds::AddressWitness;
71    use crate::shielded::builder::test_helpers::{test_orchard_address, TestProver};
72    use platform_value::BinaryData;
73
74    /// A dummy signer that produces a fake 65-byte signature.
75    /// Only used to test the builder pipeline — the signature is not validated here.
76    #[derive(Debug)]
77    struct DummySigner;
78
79    impl Signer<PlatformAddress> for DummySigner {
80        fn sign(&self, _key: &PlatformAddress, _data: &[u8]) -> Result<BinaryData, ProtocolError> {
81            Ok(BinaryData::new(vec![0u8; 65]))
82        }
83
84        fn sign_create_witness(
85            &self,
86            _key: &PlatformAddress,
87            _data: &[u8],
88        ) -> Result<AddressWitness, ProtocolError> {
89            Ok(AddressWitness::P2pkh {
90                signature: BinaryData::new(vec![0u8; 65]),
91            })
92        }
93
94        fn can_sign_with(&self, _key: &PlatformAddress) -> bool {
95            true
96        }
97    }
98
99    #[test]
100    fn test_build_shield_empty_fee_strategy() {
101        let recipient = test_orchard_address();
102        let platform_version = PlatformVersion::latest();
103        let result = build_shield_transition(
104            &recipient,
105            1000,
106            BTreeMap::new(),
107            vec![], // empty fee strategy
108            &DummySigner,
109            0,
110            &TestProver,
111            [0u8; 36],
112            platform_version,
113        );
114
115        assert!(result.is_err());
116        let err = result.unwrap_err().to_string();
117        assert!(
118            err.contains("fee_strategy must have at least one step"),
119            "unexpected error: {}",
120            err
121        );
122    }
123
124    #[test]
125    fn test_build_shield_transition_valid() {
126        let recipient = test_orchard_address();
127        let platform_version = PlatformVersion::latest();
128        // Create a P2PKH address as input
129        let input_address = PlatformAddress::P2pkh([1u8; 20]);
130        let mut inputs = BTreeMap::new();
131        inputs.insert(input_address, (0u32, 100_000u64));
132
133        let fee_strategy = vec![AddressFundsFeeStrategyStep::DeductFromInput(0)];
134
135        let result = build_shield_transition(
136            &recipient,
137            50_000,
138            inputs,
139            fee_strategy,
140            &DummySigner,
141            0,
142            &TestProver,
143            [0u8; 36],
144            platform_version,
145        );
146
147        assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
148        match result.unwrap() {
149            StateTransition::Shield(_) => {} // correct variant
150            other => panic!("expected Shield variant, got {:?}", other),
151        }
152    }
153}