Skip to main content

drive/state_transition_action/batch/
mod.rs

1use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction;
2use crate::state_transition_action::batch::v0::BatchTransitionActionV0;
3use crate::state_transition_action::contract::moderators_pot_settlement::ModeratorsPotSettlement;
4use derive_more::From;
5use dpp::data_contract::accessors::v0::DataContractV0Getters;
6use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters;
7use crate::drive::contract_groups::types::ContractGroupMembershipsForContract;
8use dpp::fee::fee_result::FeeResult;
9use dpp::consensus::ConsensusError;
10use dpp::balances::credits::MAX_CREDITS;
11use dpp::fee::Credits;
12use dpp::identity::SecurityLevel;
13use dpp::platform_value::Identifier;
14use dpp::prelude::UserFeeIncrease;
15use dpp::ProtocolError;
16use std::collections::{BTreeMap, BTreeSet};
17use dpp::data_contract::document_type::action_fees::{
18    ActionFeePricing, ContractFeePot, DocumentActionFee, FEE_MULTIPLIER_PERMILLE_BASE,
19};
20use dpp::prelude::FeeMultiplier;
21use crate::util::batch::drive_op_batch::{ContractFeePotOperationType, IdentityOperationType};
22use crate::util::batch::DriveOperation;
23use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0;
24
25/// batched transition
26pub mod batched_transition;
27/// v0
28pub mod v0;
29
30#[cfg(test)]
31mod tests;
32
33/// A contract's group memberships as the batch transformer read them, with the fee of that
34/// read. The fee travels with the data, like a contract's fetch info: the transformer does not
35/// bill it, the check that uses the answer does, so a batch signed by an ordinary key pays
36/// nothing for it.
37#[derive(Debug, Clone, Default)]
38pub struct ResolvedContractGroupMemberships {
39    /// The groups the contract, its document types and its tokens belong to
40    pub memberships: ContractGroupMembershipsForContract,
41    /// What reading them cost
42    pub fee: FeeResult,
43}
44
45/// The contract owner a batch asks to pay its gas, with the balance the batch transformer read
46/// for them. Resolved from protocol version 14, and only when the contract owner is not the
47/// batch's own signer: fee validation checks the fee against this balance, and execution charges
48/// this identity instead of the signer when it covers the fee. The user fee increase is never
49/// theirs: the signer chose it and pays it. A batch that failed validation is never sponsored,
50/// whatever it asked for: its signer pays for the work that ran.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct ResolvedGasSponsor {
53    /// The contract owner
54    pub identity_id: Identifier,
55    /// Their balance when the batch was transformed
56    pub balance: Credits,
57    /// Whether the batch insists on the contract owner paying (`GasFeesPaidBy::ContractOwner`):
58    /// then a balance that does not cover the fee refuses the batch unpaid. Otherwise
59    /// (`PreferContractOwner`) the signer pays instead.
60    pub strict: bool,
61}
62
63impl ResolvedGasSponsor {
64    /// Whether the contract owner pays: their balance covers the whole fee. Fee validation and
65    /// execution both decide by this, on the same estimated fee, so they always agree.
66    pub fn covers(&self, required_balance: Credits) -> bool {
67        self.balance >= required_balance
68    }
69}
70
71/// The action fee one document transition of a batch owes (protocol version 14): what its
72/// document type declares for the action, priced for the epoch the batch executes in.
73///
74/// It names no payer. Whoever pays the batch's gas pays its action fees, and that is settled
75/// by fee validation: [`action_fee_operations`] turns the fees into operations once it is.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct ResolvedDocumentActionFee {
78    /// The contract the document type belongs to, whose pots the fee goes to
79    pub contract_id: Identifier,
80    /// The owner of that contract, who never pays into their own owner pot
81    pub contract_owner_id: Identifier,
82    /// The credits charged
83    pub fee: DocumentActionFee,
84}
85
86impl ResolvedDocumentActionFee {
87    /// What `payer_id` owes for this fee. The owner part is dropped when the payer is the
88    /// contract owner: it would travel through the owner pot back to them and only cost
89    /// writes. The moderators part is always owed.
90    pub fn owed_by(&self, payer_id: &Identifier) -> DocumentActionFee {
91        if *payer_id == self.contract_owner_id {
92            DocumentActionFee {
93                owner: 0,
94                moderators: self.fee.moderators,
95            }
96        } else {
97            self.fee
98        }
99    }
100}
101
102/// What `payer_id` owes for all of `action_fees`.
103pub fn action_fees_total(
104    payer_id: &Identifier,
105    action_fees: &[ResolvedDocumentActionFee],
106) -> Result<Credits, ProtocolError> {
107    Ok(action_fees.iter().fold(0 as Credits, |total, fee| {
108        saturating_credits(total, fee.owed_by(payer_id).saturating_total())
109    }))
110}
111
112/// The sum of two amounts of credits, held at `MAX_CREDITS`. Action fees that add up to more
113/// than any balance can hold are owed in full and paid by nobody: fee validation refuses the
114/// batch for an insufficient balance, a consensus error, where an overflow would have been an
115/// internal one that no client can act on.
116fn saturating_credits(a: Credits, b: Credits) -> Credits {
117    a.saturating_add(b).min(MAX_CREDITS)
118}
119
120/// The operations that charge `action_fees` to `payer_id`: one removal from the payer's
121/// balance and one addition per contract fee pot that receives something. Empty when nothing
122/// is owed.
123///
124/// The additions are summed per pot because a pot's new total is computed from the committed
125/// one: two additions to the same pot in one batch would lose the first. The credits only
126/// move, from an identity balance into pots under the prefunded balances sum tree, so the sum
127/// of all credits is unchanged.
128///
129/// The transition's own operations may write the payer's balance too (a purchase price, a
130/// contested document's voting fund, a sale paying a contract owner who sponsors the gas):
131/// `apply_drive_operations` merges those writes with this removal into one.
132///
133/// Fee validation and execution both build the operations here: fee validation to estimate
134/// the batch for the sponsor and, when the sponsor does not pay, for the identity; execution
135/// for the payer fee validation settled on.
136pub fn action_fee_operations(
137    payer_id: Identifier,
138    action_fees: &[ResolvedDocumentActionFee],
139) -> Result<Vec<DriveOperation<'static>>, ProtocolError> {
140    let mut per_pot: BTreeMap<(Identifier, ContractFeePot), Credits> = BTreeMap::new();
141    for action_fee in action_fees {
142        let owed = action_fee.owed_by(&payer_id);
143        for pot in [ContractFeePot::Owner, ContractFeePot::Moderators] {
144            let amount = owed.part(pot);
145            if amount == 0 {
146                continue;
147            }
148            let pot_total = per_pot.entry((action_fee.contract_id, pot)).or_default();
149            *pot_total = saturating_credits(*pot_total, amount);
150        }
151    }
152    // What leaves the payer is what reaches the pots, by construction.
153    let total = per_pot.values().fold(0 as Credits, |total, amount| {
154        saturating_credits(total, *amount)
155    });
156    if total == 0 {
157        return Ok(vec![]);
158    }
159    let mut operations = vec![DriveOperation::IdentityOperation(
160        IdentityOperationType::RemoveFromIdentityBalance {
161            identity_id: payer_id.to_buffer(),
162            balance_to_remove: total,
163        },
164    )];
165    operations.extend(per_pot.into_iter().map(|((contract_id, pot), amount)| {
166        DriveOperation::ContractFeePotOperation(ContractFeePotOperationType::AddToPot {
167            contract_id,
168            pot,
169            amount,
170        })
171    }));
172    Ok(operations)
173}
174
175/// Who pays the gas of a whole batch, as `GasFeesPaidBy::resolve` names it for each of its
176/// transitions
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum GasPayer {
179    /// The signer of the batch
180    DocumentOwner,
181    /// The owner of the contract every transition of the batch is on
182    ContractOwner {
183        /// The contract owner
184        identity_id: Identifier,
185        /// Whether any transition insists on the contract owner paying
186        strict: bool,
187    },
188}
189
190/// documents batch transition action
191#[derive(Debug, Clone, From)]
192pub enum BatchTransitionAction {
193    /// v0
194    V0(BatchTransitionActionV0),
195}
196
197impl BatchTransitionAction {
198    /// owner id
199    pub fn owner_id(&self) -> Identifier {
200        match self {
201            BatchTransitionAction::V0(v0) => v0.owner_id,
202        }
203    }
204
205    /// transitions
206    pub fn transitions(&self) -> &Vec<BatchedTransitionAction> {
207        match self {
208            BatchTransitionAction::V0(v0) => &v0.transitions,
209        }
210    }
211
212    /// transitions
213    pub fn transitions_mut(&mut self) -> &mut Vec<BatchedTransitionAction> {
214        match self {
215            BatchTransitionAction::V0(v0) => &mut v0.transitions,
216        }
217    }
218
219    /// transitions
220    pub fn transitions_take(&mut self) -> Vec<BatchedTransitionAction> {
221        match self {
222            BatchTransitionAction::V0(v0) => std::mem::take(&mut v0.transitions),
223        }
224    }
225
226    /// transitions owned
227    pub fn transitions_owned(self) -> Vec<BatchedTransitionAction> {
228        match self {
229            BatchTransitionAction::V0(v0) => v0.transitions,
230        }
231    }
232
233    /// set transitions
234    pub fn set_transitions(&mut self, transitions: Vec<BatchedTransitionAction>) {
235        match self {
236            BatchTransitionAction::V0(v0) => v0.transitions = transitions,
237        }
238    }
239
240    /// fee multiplier
241    pub fn user_fee_increase(&self) -> UserFeeIncrease {
242        match self {
243            BatchTransitionAction::V0(transition) => transition.user_fee_increase,
244        }
245    }
246
247    /// The group memberships the transformer resolved for a contract the batch touches
248    pub fn contract_group_memberships(
249        &self,
250        contract_id: &Identifier,
251    ) -> Option<&ResolvedContractGroupMemberships> {
252        match self {
253            BatchTransitionAction::V0(v0) => v0.contract_group_memberships.get(contract_id),
254        }
255    }
256
257    /// Records the group memberships of a contract the batch touches
258    pub fn set_contract_group_memberships(
259        &mut self,
260        contract_id: Identifier,
261        resolved: ResolvedContractGroupMemberships,
262    ) {
263        match self {
264            BatchTransitionAction::V0(v0) => {
265                v0.contract_group_memberships.insert(contract_id, resolved);
266            }
267        }
268    }
269
270    /// The contracts on which the batch sweeps its owner's lapsed suspension when it executes
271    pub fn lapsed_suspensions(&self) -> &BTreeSet<Identifier> {
272        match self {
273            BatchTransitionAction::V0(v0) => &v0.lapsed_suspensions,
274        }
275    }
276
277    /// The settles of moderators pots the batch forces before it changes a seated team
278    pub fn moderators_pot_settlements(&self) -> &[ModeratorsPotSettlement] {
279        match self {
280            BatchTransitionAction::V0(v0) => &v0.moderators_pot_settlements,
281        }
282    }
283
284    /// Takes the settles of moderators pots out of the batch, for its conversion to operations
285    pub fn take_moderators_pot_settlements(&mut self) -> Vec<ModeratorsPotSettlement> {
286        match self {
287            BatchTransitionAction::V0(v0) => std::mem::take(&mut v0.moderators_pot_settlements),
288        }
289    }
290
291    /// Records the settles of moderators pots the batch forces before it changes a seated
292    /// team
293    pub fn set_moderators_pot_settlements(&mut self, settlements: Vec<ModeratorsPotSettlement>) {
294        match self {
295            BatchTransitionAction::V0(v0) => v0.moderators_pot_settlements = settlements,
296        }
297    }
298}
299
300impl BatchTransitionAction {
301    /// The sum of all purchases amount and all conflicting index collateral voting funds
302    pub fn all_used_balances(&self) -> Result<Option<Credits>, ProtocolError> {
303        match self {
304            BatchTransitionAction::V0(v0) => v0.all_used_balances(),
305        }
306    }
307
308    /// The contract owner the batch transformer resolved as the gas sponsor, if any
309    pub fn gas_sponsor(&self) -> Option<&ResolvedGasSponsor> {
310        match self {
311            BatchTransitionAction::V0(v0) => v0.gas_sponsor.as_ref(),
312        }
313    }
314
315    /// The fee each document transition of the batch owes for its action, before the fee
316    /// multiplier, with the contract it goes to, that contract's owner, and how it is priced:
317    /// what its document type declares, with the moderators part it agreed to when that is a
318    /// discount the contract's seated moderation charter gives (advanced structure validation
319    /// refuses any other). A transition that became a nonce bump declares nothing: only an
320    /// action that executes is charged.
321    pub fn declared_action_fees(
322        &self,
323    ) -> Vec<(Identifier, Identifier, ActionFeePricing, DocumentActionFee)> {
324        match self {
325            BatchTransitionAction::V0(v0) => v0
326                .transitions
327                .iter()
328                .filter_map(|transition| match transition {
329                    BatchedTransitionAction::DocumentAction(document_action) => {
330                        let base = document_action.base();
331                        let declared = base.declared_action_fee_with_agreement()?;
332                        let contract = &base.data_contract_fetch_info_ref().contract;
333                        Some((
334                            contract.id(),
335                            contract.owner_id(),
336                            declared.pricing,
337                            declared.agreed_fee(),
338                        ))
339                    }
340                    _ => None,
341                })
342                .collect(),
343        }
344    }
345
346    /// The elected contracts on whose moderated document types some document transition of the
347    /// batch agrees to a discounted moderators part (protocol version 14): the contracts whose
348    /// seated moderation charter the batch transformer reads the moderators share of.
349    pub fn contracts_with_moderators_discounts(&self) -> BTreeSet<Identifier> {
350        match self {
351            BatchTransitionAction::V0(v0) => v0
352                .transitions
353                .iter()
354                .filter_map(|transition| match transition {
355                    BatchedTransitionAction::DocumentAction(document_action) => {
356                        let base = document_action.base();
357                        base.agrees_to_a_moderators_discount()
358                            .then(|| base.data_contract_id())
359                    }
360                    _ => None,
361                })
362                .collect(),
363        }
364    }
365
366    /// Records the moderators share of the seated moderation charter of `contract_id`, `None`
367    /// when no charter is seated on it
368    pub fn set_seated_moderators_share(&mut self, contract_id: Identifier, share: Option<u8>) {
369        match self {
370            BatchTransitionAction::V0(v0) => {
371                v0.seated_moderators_shares.insert(contract_id, share);
372            }
373        }
374    }
375
376    /// The fee multiplier, in permille, of the epoch the batch executes in, as the batch
377    /// transformer read it (protocol version 14). Only read when some document transition of
378    /// the batch declares an action fee priced by it.
379    pub fn action_fee_multiplier_permille(&self) -> Option<FeeMultiplier> {
380        match self {
381            BatchTransitionAction::V0(v0) => v0.action_fee_multiplier_permille,
382        }
383    }
384
385    /// Records the fee multiplier of the epoch the batch executes in
386    pub fn set_action_fee_multiplier_permille(&mut self, multiplier: Option<FeeMultiplier>) {
387        match self {
388            BatchTransitionAction::V0(v0) => v0.action_fee_multiplier_permille = multiplier,
389        }
390    }
391
392    /// The action fees the batch owes: what its document transitions declare, priced.
393    ///
394    /// They are read off the transitions as they are now, not as the transformer built them.
395    /// State validation replaces a transition that fails with a nonce bump after the
396    /// transformer ran, and a bump declares nothing, so a fee is only ever owed for an action
397    /// that executes.
398    pub fn resolved_action_fees(&self) -> Result<Vec<ResolvedDocumentActionFee>, ProtocolError> {
399        self.declared_action_fees()
400            .into_iter()
401            .map(|(contract_id, contract_owner_id, pricing, fee)| {
402                let fee_multiplier_permille = match pricing {
403                    ActionFeePricing::Fixed => FEE_MULTIPLIER_PERMILLE_BASE,
404                    ActionFeePricing::FeeMultiplier => self
405                        .action_fee_multiplier_permille()
406                        .ok_or(ProtocolError::CorruptedCodeExecution(
407                            "the batch transformer reads the fee multiplier of every batch that \
408                             declares an action fee priced by it"
409                                .to_string(),
410                        ))?,
411                };
412                Ok(ResolvedDocumentActionFee {
413                    contract_id,
414                    contract_owner_id,
415                    fee: fee.charged(pricing, fee_multiplier_permille)?,
416                })
417            })
418            .collect()
419    }
420
421    /// Whether every document transition that owes an action fee agreed to it: the transition
422    /// names the amounts and the pricing its document type declares, or, on a document type an
423    /// elected contract moderates, the declared owner part and pricing with the share of the
424    /// declared moderators part the contract's seated moderation charter takes; and, for a fee
425    /// priced by the fee multiplier, accepts the multiplier of the epoch the batch executes in.
426    /// The inner error is the consensus error of the first transition that did not. Everything
427    /// it is judged against travels on the action (the charter's share as the transformer read
428    /// it), so this reads no state.
429    pub fn validate_action_fee_agreements(
430        &self,
431    ) -> Result<Result<(), ConsensusError>, ProtocolError> {
432        match self {
433            BatchTransitionAction::V0(v0) => v0.validate_action_fee_agreements(),
434        }
435    }
436
437    /// Records the contract owner who sponsors the batch's gas, with their balance
438    pub fn set_gas_sponsor(&mut self, gas_sponsor: Option<ResolvedGasSponsor>) {
439        match self {
440            BatchTransitionAction::V0(v0) => v0.gas_sponsor = gas_sponsor,
441        }
442    }
443
444    /// Who pays the gas of the batch, or the consensus error explaining why the batch's
445    /// requests cannot be honoured: a transition asking for more than its document type's token
446    /// cost offers, or a batch whose transitions do not all name the same payer.
447    pub fn resolve_gas_payer(&self) -> Result<GasPayer, ConsensusError> {
448        match self {
449            BatchTransitionAction::V0(v0) => v0.resolve_gas_payer(),
450        }
451    }
452
453    /// The sum of all purchases amounts for all purchase transitions in the batch
454    pub fn all_purchases_amount(&self) -> Result<Option<Credits>, ProtocolError> {
455        match self {
456            BatchTransitionAction::V0(v0) => v0.all_purchases_amount(),
457        }
458    }
459
460    /// The sum of all conflicting index collateral voting funds for all document create transitions in the batch
461    pub fn all_conflicting_index_collateral_voting_funds(
462        &self,
463    ) -> Result<Option<Credits>, ProtocolError> {
464        match self {
465            BatchTransitionAction::V0(v0) => v0.all_conflicting_index_collateral_voting_funds(),
466        }
467    }
468
469    /// Determines the security level requirements for the batch transition action.
470    ///
471    /// This method performs the following steps:
472    ///
473    /// 1. Retrieves all document types associated with the state transitions (STs) in the batch.
474    /// 2. For each document type, fetches its schema to determine its security level requirement.
475    ///    - If the schema specifies a security level, that is used.
476    ///    - Otherwise, a default security level is used.
477    ///
478    /// The method then determines the highest security level (which corresponds to the lowest
479    /// integer value of the `SecurityLevel` enum) across all documents affected by the state transitions.
480    /// This highest level becomes the signature requirement for the entire batch transition action.
481    ///
482    /// # Returns
483    ///
484    /// - Returns a `Result` containing a `Vec<SecurityLevel>` which is the list of security
485    ///   levels required for the batch transition action.
486    /// - Returns an `Err` of type `ProtocolError` if any error occurs during the process.
487    ///
488    /// # Examples
489    ///
490    /// ```ignore
491    /// // Assuming `batch_transition_action` is an instance of `DocumentsBatchTransitionAction`
492    /// let required_levels = batch_transition_action.contract_based_security_level_requirement()?;
493    /// ```
494    ///
495    pub fn combined_security_level_requirement(&self) -> Result<Vec<SecurityLevel>, ProtocolError> {
496        // Step 1: Get all document types for the ST
497        // Step 2: Get document schema for every type
498        // If schema has security level, use that, if not, use the default security level
499        // Find the highest level (lowest int value) of all documents - the ST's signature
500        // requirement is the highest level across all documents affected by the ST./
501        let mut highest_security_level = SecurityLevel::lowest_level();
502
503        for transition in self.transitions().iter() {
504            match transition {
505                BatchedTransitionAction::DocumentAction(document_transition) => {
506                    let document_type_name = document_transition.base().document_type_name();
507                    let data_contract_info = document_transition.base().data_contract_fetch_info();
508
509                    let document_type = data_contract_info
510                        .contract
511                        .document_type_for_name(document_type_name)?;
512
513                    let document_security_level = document_type.security_level_requirement();
514
515                    // lower enum representation means higher in security
516                    if document_security_level < highest_security_level {
517                        highest_security_level = document_security_level
518                    }
519                }
520                BatchedTransitionAction::TokenAction(_) => {
521                    // lower enum representation means higher in security
522                    if highest_security_level != SecurityLevel::MASTER {
523                        highest_security_level = SecurityLevel::CRITICAL
524                    }
525                }
526                BatchedTransitionAction::BumpIdentityDataContractNonce(_) => {}
527            }
528        }
529        Ok(if highest_security_level == SecurityLevel::MASTER {
530            vec![SecurityLevel::MASTER]
531        } else {
532            // this might seem wrong until you realize that master is 0, critical 1, etc
533            (SecurityLevel::CRITICAL as u8..=highest_security_level as u8)
534                .map(|security_level| SecurityLevel::try_from(security_level).unwrap())
535                .collect()
536        })
537    }
538}