Skip to main content

dpp/shielded/builder/
document_token_payment.rs

1use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey};
2
3use crate::address_funds::OrchardAddress;
4use crate::balances::credits::TokenAmount;
5use crate::prelude::Identifier;
6use crate::shielded::document_token_payment_extra_sighash_data;
7use crate::tokens::token_payment_info::v1::TokenShieldedPayment;
8use crate::ProtocolError;
9use platform_version::version::PlatformVersion;
10
11use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
12
13/// Builds the shielded payment of a document action whose token cost is `amount`: spends
14/// `spends` from the token's shielded pool, returns the remainder to `change_address` as a
15/// new note and binds the token id, the batch owner, the document's contract and id and the
16/// amount into the Orchard sighash. Put the result into a `TokenPaymentInfo::V1` on the
17/// document transition's base; the identity signing the batch still pays the credit fee.
18///
19/// For document creation, `document_id` must be the final ID derived from the creation
20/// nonce. Call `Document::set_id_for_creation` before building this payment; a document
21/// factory's placeholder ID is replaced when the create transition is built.
22///
23/// `value_balance == amount` exactly; the pool pays the cost, nothing is carved for fees.
24#[allow(clippy::too_many_arguments)]
25pub fn build_document_shielded_token_payment<P: OrchardProver>(
26    token_id: Identifier,
27    owner_id: Identifier,
28    data_contract_id: Identifier,
29    document_id: Identifier,
30    spends: Vec<SpendableNote>,
31    amount: TokenAmount,
32    change_address: &OrchardAddress,
33    fvk: &FullViewingKey,
34    ask: &SpendAuthorizingKey,
35    anchor: Anchor,
36    memo: [u8; 36],
37    prover: &P,
38    platform_version: &PlatformVersion,
39) -> Result<TokenShieldedPayment, ProtocolError> {
40    if amount == 0 {
41        return Err(ProtocolError::ShieldedBuildError(
42            "document token payment amount must be greater than zero".to_string(),
43        ));
44    }
45    if amount > i64::MAX as u64 {
46        return Err(ProtocolError::ShieldedBuildError(format!(
47            "document token payment amount {} exceeds maximum allowed value {}",
48            amount,
49            i64::MAX as u64
50        )));
51    }
52
53    let total_spent: u64 = spends
54        .iter()
55        .try_fold(0u64, |total, spend| {
56            total.checked_add(spend.note.value().inner())
57        })
58        .ok_or_else(|| {
59            ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string())
60        })?;
61    if amount > total_spent {
62        return Err(ProtocolError::ShieldedBuildError(format!(
63            "document token payment amount {} exceeds total spendable value {}",
64            amount, total_spent
65        )));
66    }
67    let change_amount = total_spent - amount;
68
69    let extra_sighash_data = document_token_payment_extra_sighash_data(
70        &token_id.to_buffer(),
71        &owner_id.to_buffer(),
72        &data_contract_id.to_buffer(),
73        &document_id.to_buffer(),
74        amount,
75        platform_version,
76    )?;
77
78    let bundle = build_spend_bundle(
79        spends,
80        change_address,
81        change_amount,
82        memo,
83        fvk,
84        ask,
85        anchor,
86        prover,
87        &extra_sighash_data,
88    )?;
89    let sb = serialize_authorized_bundle(&bundle);
90
91    if sb.value_balance != amount as i64 {
92        return Err(ProtocolError::ShieldedBuildError(format!(
93            "document token payment bundle value balance {} does not equal the amount {}",
94            sb.value_balance, amount
95        )));
96    }
97
98    Ok(TokenShieldedPayment {
99        amount,
100        actions: sb.actions,
101        anchor: sb.anchor,
102        proof: sb.proof,
103        binding_signature: sb.binding_signature,
104    })
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::shielded::builder::test_helpers::{
111        test_orchard_address, test_spendable_note, TestProver,
112    };
113    use grovedb_commitment_tree::SpendingKey;
114
115    fn keys() -> (FullViewingKey, SpendAuthorizingKey) {
116        let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
117        (FullViewingKey::from(&sk), SpendAuthorizingKey::from(&sk))
118    }
119
120    #[test]
121    fn rejects_amount_above_spendable_value() {
122        let (fvk, ask) = keys();
123        let err = build_document_shielded_token_payment(
124            Identifier::from([1u8; 32]),
125            Identifier::from([2u8; 32]),
126            Identifier::from([3u8; 32]),
127            Identifier::from([4u8; 32]),
128            vec![test_spendable_note(100)],
129            1_000,
130            &test_orchard_address(),
131            &fvk,
132            &ask,
133            Anchor::empty_tree(),
134            [0u8; 36],
135            &TestProver,
136            PlatformVersion::latest(),
137        )
138        .expect_err("overspend must be rejected")
139        .to_string();
140        assert!(
141            err.contains("exceeds total spendable value"),
142            "unexpected error: {err}"
143        );
144    }
145
146    #[test]
147    fn rejects_zero_amount() {
148        let (fvk, ask) = keys();
149        let err = build_document_shielded_token_payment(
150            Identifier::from([1u8; 32]),
151            Identifier::from([2u8; 32]),
152            Identifier::from([3u8; 32]),
153            Identifier::from([4u8; 32]),
154            vec![test_spendable_note(100)],
155            0,
156            &test_orchard_address(),
157            &fvk,
158            &ask,
159            Anchor::empty_tree(),
160            [0u8; 36],
161            &TestProver,
162            PlatformVersion::latest(),
163        )
164        .expect_err("zero amount must be rejected")
165        .to_string();
166        assert!(err.contains("greater than zero"), "unexpected error: {err}");
167    }
168}