Skip to main content

dpp/shielded/builder/
token_burn_from_pool.rs

1use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey};
2
3use crate::address_funds::OrchardAddress;
4use crate::balances::credits::TokenAmount;
5use crate::group::GroupStateTransitionInfoStatus;
6use crate::identity::signer::Signer;
7use crate::identity::IdentityPublicKey;
8use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease};
9use crate::shielded::{token_burn_from_pool_extra_sighash_data, OrchardBundleParams};
10use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1;
11use crate::state_transition::batch_transition::BatchTransition;
12use crate::state_transition::StateTransition;
13use crate::ProtocolError;
14use platform_version::version::PlatformVersion;
15
16use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote};
17
18/// Builds a `TokenBurnFromPool` batch transition: spends `spends` inside the token's shielded
19/// pool, destroys `amount` and returns the remainder to `change_address`, then wraps the bundle
20/// in a batch transition signed by `owner_id`, who must be authorized to burn and pays the fee
21/// in credits. The token id, owner id and amount are bound into the Orchard sighash.
22///
23/// A group action burn is proven once, by the proposer
24/// (`GroupStateTransitionInfoProposer`): the group action pins the digest of the actions and
25/// the sighash binds the proposer, so every other signer submits the proposer's bundle unchanged
26/// through `new_token_burn_from_pool_transition` with `GroupStateTransitionInfoOtherSigner`.
27/// Asking this builder for a fresh bundle on behalf of another signer is refused, since consensus
28/// would reject it as a modification of the group action.
29#[allow(clippy::too_many_arguments)]
30pub async fn build_token_burn_from_pool_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    amount: TokenAmount,
40    change_address: &OrchardAddress,
41    fvk: &FullViewingKey,
42    ask: &SpendAuthorizingKey,
43    anchor: Anchor,
44    memo: [u8; 36],
45    public_note: Option<String>,
46    using_group_info: Option<GroupStateTransitionInfoStatus>,
47    identity_public_key: &IdentityPublicKey,
48    identity_contract_nonce: IdentityNonce,
49    user_fee_increase: UserFeeIncrease,
50    signer: &S,
51    prover: &P,
52    platform_version: &PlatformVersion,
53) -> Result<StateTransition, ProtocolError> {
54    if amount == 0 {
55        return Err(ProtocolError::ShieldedBuildError(
56            "token burn from pool amount must be greater than zero".to_string(),
57        ));
58    }
59    if amount > i64::MAX as u64 {
60        return Err(ProtocolError::ShieldedBuildError(format!(
61            "token burn from pool amount {} exceeds maximum allowed value {}",
62            amount,
63            i64::MAX as u64
64        )));
65    }
66    if matches!(
67        using_group_info,
68        Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(_))
69    ) {
70        return Err(ProtocolError::ShieldedBuildError(
71            "a group action burn from pool is proven once by the proposer; another signer submits the proposer's bundle unchanged instead of building its own".to_string(),
72        ));
73    }
74
75    let total_spent: u64 = spends
76        .iter()
77        .try_fold(0u64, |total, spend| {
78            total.checked_add(spend.note.value().inner())
79        })
80        .ok_or_else(|| {
81            ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string())
82        })?;
83    if amount > total_spent {
84        return Err(ProtocolError::ShieldedBuildError(format!(
85            "token burn from pool amount {} exceeds total spendable value {}",
86            amount, total_spent
87        )));
88    }
89    let change_amount = total_spent - amount;
90
91    let extra_sighash_data = token_burn_from_pool_extra_sighash_data(
92        &token_id.to_buffer(),
93        &owner_id.to_buffer(),
94        amount,
95        platform_version,
96    )?;
97
98    let bundle = build_spend_bundle(
99        spends,
100        change_address,
101        change_amount,
102        memo,
103        fvk,
104        ask,
105        anchor,
106        prover,
107        &extra_sighash_data,
108    )?;
109    let sb = serialize_authorized_bundle(&bundle);
110
111    if sb.value_balance != amount as i64 {
112        return Err(ProtocolError::ShieldedBuildError(format!(
113            "token burn from pool bundle value balance {} does not equal the amount {}",
114            sb.value_balance, amount
115        )));
116    }
117
118    BatchTransition::new_token_burn_from_pool_transition(
119        token_id,
120        owner_id,
121        data_contract_id,
122        token_contract_position,
123        amount,
124        OrchardBundleParams {
125            actions: sb.actions,
126            anchor: sb.anchor,
127            proof: sb.proof,
128            binding_signature: sb.binding_signature,
129        },
130        public_note,
131        using_group_info,
132        identity_public_key,
133        identity_contract_nonce,
134        user_fee_increase,
135        signer,
136        platform_version,
137        None,
138    )
139    .await
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::group::GroupStateTransitionInfo;
146    use crate::shielded::builder::test_helpers::{
147        test_identity_key, test_orchard_address, test_spendable_note, DummyIdentitySigner,
148        TestProver,
149    };
150    use grovedb_commitment_tree::SpendingKey;
151
152    #[tokio::test]
153    async fn rejects_amount_above_spendable_value() {
154        let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
155        let fvk = FullViewingKey::from(&sk);
156        let ask = SpendAuthorizingKey::from(&sk);
157        let key = test_identity_key();
158        let err = build_token_burn_from_pool_transition(
159            Identifier::from([1u8; 32]),
160            Identifier::from([2u8; 32]),
161            Identifier::from([3u8; 32]),
162            0,
163            vec![test_spendable_note(100)],
164            1_000,
165            &test_orchard_address(),
166            &fvk,
167            &ask,
168            Anchor::empty_tree(),
169            [0u8; 36],
170            None,
171            None,
172            &key,
173            1,
174            0,
175            &DummyIdentitySigner,
176            &TestProver,
177            PlatformVersion::latest(),
178        )
179        .await
180        .expect_err("overspend must be rejected")
181        .to_string();
182        assert!(
183            err.contains("exceeds total spendable value"),
184            "unexpected error: {err}"
185        );
186    }
187
188    #[tokio::test]
189    async fn rejects_a_fresh_bundle_for_another_group_signer() {
190        let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes");
191        let fvk = FullViewingKey::from(&sk);
192        let ask = SpendAuthorizingKey::from(&sk);
193        let key = test_identity_key();
194        let err = build_token_burn_from_pool_transition(
195            Identifier::from([1u8; 32]),
196            Identifier::from([2u8; 32]),
197            Identifier::from([3u8; 32]),
198            0,
199            vec![test_spendable_note(1_000)],
200            100,
201            &test_orchard_address(),
202            &fvk,
203            &ask,
204            Anchor::empty_tree(),
205            [0u8; 36],
206            None,
207            Some(
208                GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(
209                    GroupStateTransitionInfo {
210                        group_contract_position: 0,
211                        action_id: Identifier::from([4u8; 32]),
212                        action_is_proposer: false,
213                    },
214                ),
215            ),
216            &key,
217            1,
218            0,
219            &DummyIdentitySigner,
220            &TestProver,
221            PlatformVersion::latest(),
222        )
223        .await
224        .expect_err("another signer must reuse the proposer's bundle")
225        .to_string();
226        assert!(
227            err.contains("proven once by the proposer"),
228            "unexpected error: {err}"
229        );
230    }
231}