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