Skip to main content

dpp/shielded/builder/
token_shielded_transfer.rs

1use grovedb_commitment_tree::{
2    Anchor, Builder, BundleType, DashMemo, FullViewingKey, NoteValue, PaymentAddress, Scope,
3    SpendAuthorizingKey,
4};
5
6use crate::address_funds::OrchardAddress;
7use crate::balances::credits::TokenAmount;
8use crate::identity::signer::Signer;
9use crate::identity::IdentityPublicKey;
10use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease};
11use crate::shielded::{token_shielded_transfer_extra_sighash_data, OrchardBundleParams};
12use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1;
13use crate::state_transition::batch_transition::BatchTransition;
14use crate::state_transition::StateTransition;
15use crate::ProtocolError;
16use platform_version::version::PlatformVersion;
17
18use super::{prove_and_sign_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
19
20/// Builds a `TokenShieldedTransfer` batch transition: spends `spends` inside the token's
21/// shielded pool, pays `transfer_amount` to `recipient` and any remainder to `change_address`,
22/// and wraps the bundle in a batch transition signed by `owner_id`, which pays the fee in
23/// credits.
24///
25/// Nothing leaves the pool, so the bundle's value balance is exactly zero; consensus rejects
26/// anything else. Both outputs are encrypted under the sender's external outgoing viewing key so
27/// the sending wallet can recover its own send history. The token id and owner id are bound into
28/// the Orchard sighash, so the bundle is tied to this token and this signing identity.
29#[allow(clippy::too_many_arguments)]
30pub async fn build_token_shielded_transfer_transition<
31    S: Signer<IdentityPublicKey>,
32    P: OrchardProver,
33>(
34    token_id: Identifier,
35    owner_id: Identifier,
36    data_contract_id: Identifier,
37    token_contract_position: u16,
38    spends: Vec<SpendableNote>,
39    recipient: &OrchardAddress,
40    transfer_amount: TokenAmount,
41    change_address: &OrchardAddress,
42    fvk: &FullViewingKey,
43    ask: &SpendAuthorizingKey,
44    anchor: Anchor,
45    memo: [u8; 36],
46    identity_public_key: &IdentityPublicKey,
47    identity_contract_nonce: IdentityNonce,
48    user_fee_increase: UserFeeIncrease,
49    signer: &S,
50    prover: &P,
51    platform_version: &PlatformVersion,
52) -> Result<StateTransition, ProtocolError> {
53    if transfer_amount == 0 {
54        return Err(ProtocolError::ShieldedBuildError(
55            "token shielded transfer amount must be greater than zero".to_string(),
56        ));
57    }
58
59    let total_spent: u64 = spends
60        .iter()
61        .try_fold(0u64, |total, spend| {
62            total.checked_add(spend.note.value().inner())
63        })
64        .ok_or_else(|| {
65            ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string())
66        })?;
67    if transfer_amount > total_spent {
68        return Err(ProtocolError::ShieldedBuildError(format!(
69            "token shielded transfer amount {} exceeds total spendable value {}",
70            transfer_amount, total_spent
71        )));
72    }
73    let change_amount = total_spent - transfer_amount;
74
75    let mut builder = Builder::<DashMemo>::new(BundleType::DEFAULT, anchor);
76
77    for spend in spends {
78        builder
79            .add_spend(fvk.clone(), spend.note, spend.merkle_path)
80            .map_err(|e| {
81                ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e))
82            })?;
83    }
84
85    let sender_ovk = fvk.to_ovk(Scope::External);
86
87    builder
88        .add_output(
89            Some(sender_ovk.clone()),
90            PaymentAddress::from(recipient),
91            NoteValue::from_raw(transfer_amount),
92            memo,
93        )
94        .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?;
95
96    if change_amount > 0 {
97        builder
98            .add_output(
99                Some(sender_ovk),
100                PaymentAddress::from(change_address),
101                NoteValue::from_raw(change_amount),
102                [0u8; 36],
103            )
104            .map_err(|e| {
105                ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e))
106            })?;
107    }
108
109    let extra_sighash_data = token_shielded_transfer_extra_sighash_data(
110        &token_id.to_buffer(),
111        &owner_id.to_buffer(),
112        platform_version,
113    )?;
114
115    let bundle = prove_and_sign_bundle(
116        builder,
117        prover,
118        std::slice::from_ref(ask),
119        &extra_sighash_data,
120    )?;
121    let sb = serialize_authorized_bundle(&bundle);
122
123    if sb.value_balance != 0 {
124        return Err(ProtocolError::ShieldedBuildError(format!(
125            "token shielded transfer bundle value balance must be zero, got {}",
126            sb.value_balance
127        )));
128    }
129
130    BatchTransition::new_token_shielded_transfer_transition(
131        token_id,
132        owner_id,
133        data_contract_id,
134        token_contract_position,
135        OrchardBundleParams {
136            actions: sb.actions,
137            anchor: sb.anchor,
138            proof: sb.proof,
139            binding_signature: sb.binding_signature,
140        },
141        identity_public_key,
142        identity_contract_nonce,
143        user_fee_increase,
144        signer,
145        platform_version,
146        None,
147    )
148    .await
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::shielded::builder::test_helpers::{
155        test_identity_key, test_orchard_address, test_spendable_note, DummyIdentitySigner,
156        TestProver,
157    };
158    use grovedb_commitment_tree::SpendingKey;
159
160    #[tokio::test]
161    async fn rejects_amount_above_spendable_value() {
162        let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
163        let fvk = FullViewingKey::from(&sk);
164        let ask = SpendAuthorizingKey::from(&sk);
165        let key = test_identity_key();
166        let err = build_token_shielded_transfer_transition(
167            Identifier::from([1u8; 32]),
168            Identifier::from([2u8; 32]),
169            Identifier::from([3u8; 32]),
170            0,
171            vec![test_spendable_note(100)],
172            &test_orchard_address(),
173            1_000,
174            &test_orchard_address(),
175            &fvk,
176            &ask,
177            Anchor::empty_tree(),
178            [0u8; 36],
179            &key,
180            1,
181            0,
182            &DummyIdentitySigner,
183            &TestProver,
184            PlatformVersion::latest(),
185        )
186        .await
187        .expect_err("overspend must be rejected")
188        .to_string();
189        assert!(
190            err.contains("exceeds total spendable value"),
191            "unexpected error: {err}"
192        );
193    }
194}