Skip to main content

drive/state_transition_action/batch/v0/
mod.rs

1use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0;
2use crate::state_transition_action::batch::{
3    GasPayer, ResolvedContractGroupMemberships, ResolvedGasSponsor,
4};
5use crate::state_transition_action::contract::moderators_pot_settlement::ModeratorsPotSettlement;
6use dpp::prelude::FeeMultiplier;
7use dpp::consensus::state::document::document_action_fee_agreement_mismatch_error::DocumentActionFeeAgreementMismatchError;
8use dpp::consensus::state::document::document_action_fee_agreement_not_set_error::DocumentActionFeeAgreementNotSetError;
9use dpp::consensus::state::document::document_action_fee_moderators_share_mismatch_error::DocumentActionFeeModeratorsShareMismatchError;
10use dpp::moderation_charter::moderators_share_of;
11use dpp::consensus::state::document::document_action_fee_multiplier_not_tolerated_error::DocumentActionFeeMultiplierNotToleratedError;
12use dpp::consensus::state::token::{GasFeesPaidByNotAllowedError, InconsistentGasFeesPaidByInBatchError};
13use dpp::consensus::ConsensusError;
14use dpp::data_contract::accessors::v0::DataContractV0Getters;
15use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy;
16use dpp::fee::Credits;
17use std::collections::{BTreeMap, BTreeSet};
18use crate::state_transition_action::batch::batched_transition::document_transition::DocumentTransitionAction;
19use dpp::identifier::Identifier;
20use dpp::prelude::UserFeeIncrease;
21use dpp::ProtocolError;
22use crate::state_transition_action::batch::batched_transition::BatchedTransitionAction;
23use crate::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0;
24use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0;
25use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionActionAccessorsV0;
26use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::TokenDirectPurchaseToPoolTransitionActionAccessorsV0;
27use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction;
28
29/// action v0
30#[derive(Default, Debug, Clone)]
31pub struct BatchTransitionActionV0 {
32    /// The owner making the transitions
33    pub owner_id: Identifier,
34    /// The inner transitions
35    pub transitions: Vec<BatchedTransitionAction>,
36    /// fee multiplier
37    pub user_fee_increase: UserFeeIncrease,
38    /// The contract group memberships of every contract the batch touches, keyed by contract
39    /// id, as the transformer read them from state (protocol version 14). Resolved only when
40    /// the batch is signed by a key bound to a contract group, so that key can be judged from
41    /// the action alone; empty for every other batch and under earlier transformer versions.
42    pub contract_group_memberships: BTreeMap<Identifier, ResolvedContractGroupMemberships>,
43    /// The contract owner sponsoring the batch's gas, resolved by the batch transformer from
44    /// protocol version 14 (see `ResolvedGasSponsor`)
45    pub gas_sponsor: Option<ResolvedGasSponsor>,
46    /// The fee multiplier, in permille, of the epoch the batch executes in, read by the batch
47    /// transformer from protocol version 14 when some document transition declares an action
48    /// fee priced by it. The fees themselves are not kept: they are read off the transitions
49    /// when the batch executes, after state validation had its say on each of them.
50    pub action_fee_multiplier_permille: Option<FeeMultiplier>,
51    /// The moderators share of the seated moderation charter of each elected contract on whose
52    /// moderated document types some document transition of the batch agrees to a discounted
53    /// moderators part, read by the batch transformer from protocol version 14: `None` when no
54    /// charter is seated on the contract. Empty for a batch that asks for no discount, which
55    /// reads nothing.
56    pub seated_moderators_shares: BTreeMap<Identifier, Option<u8>>,
57
58    /// The contracts, among those the batch touches, on which the transformer found the batch
59    /// owner's suspension lapsed (protocol version 14). Each such suspension is deleted when
60    /// the batch executes: the first document transition after a suspension lapses sweeps it.
61    /// Only ever the owner's own: the identity is not stored, so nothing can queue another's.
62    pub lapsed_suspensions: BTreeSet<Identifier>,
63
64    /// The settles of elected contracts' moderators pots the batch forces before it changes a
65    /// seated team (protocol version 14): an `addedModerator` or `removedModerator` of the
66    /// moderation charters contract created or deleted pays the pot out to the team as it was
67    /// before the change, by its proposal's reward split, and resets the action counts. Set
68    /// by the batch's state validation, which reads the team, the pot and the counts; empty
69    /// for every other batch.
70    pub moderators_pot_settlements: Vec<ModeratorsPotSettlement>,
71}
72
73impl BatchTransitionActionV0 {
74    /// See `BatchTransitionAction::validate_action_fee_agreements`
75    pub(in crate::state_transition_action) fn validate_action_fee_agreements(
76        &self,
77    ) -> Result<Result<(), ConsensusError>, ProtocolError> {
78        for transition in &self.transitions {
79            let BatchedTransitionAction::DocumentAction(document_action) = transition else {
80                continue;
81            };
82            let base = document_action.base();
83            let Some(declared) = base.declared_action_fee_with_agreement() else {
84                continue;
85            };
86            let document_type_name = || base.document_type_name().clone();
87            let action = || document_action.action_name().to_string();
88            let Some(agreement) = declared.agreement else {
89                return Ok(Err(DocumentActionFeeAgreementNotSetError::new(
90                    document_type_name(),
91                    action(),
92                    declared.pricing,
93                    declared.fee,
94                )
95                .into()));
96            };
97            if base.agrees_to_a_moderators_discount() {
98                // Less than the declared moderators part, on a type an elected contract
99                // moderates: exactly the share the contract's seated charter takes of it, and
100                // nothing without a seated charter.
101                let moderators_share = *self
102                    .seated_moderators_shares
103                    .get(&base.data_contract_id())
104                    .ok_or(ProtocolError::CorruptedCodeExecution(
105                        "the batch transformer reads the seated moderators share of every \
106                         contract a document transition agrees to a discount on"
107                            .to_string(),
108                    ))?;
109                let offered = moderators_share
110                    .map(|share| moderators_share_of(declared.fee.moderators, share));
111                if offered != Some(agreement.moderators()) {
112                    return Ok(Err(DocumentActionFeeModeratorsShareMismatchError::new(
113                        document_type_name(),
114                        action(),
115                        declared.fee.moderators,
116                        agreement.moderators(),
117                        moderators_share,
118                    )
119                    .into()));
120                }
121            } else if !agreement.matches_declared(declared.pricing, declared.fee) {
122                return Ok(Err(DocumentActionFeeAgreementMismatchError::new(
123                    document_type_name(),
124                    action(),
125                    declared.pricing,
126                    declared.fee,
127                    &agreement,
128                )
129                .into()));
130            }
131            // A matching agreement names the fee multiplier exactly when the fee follows it.
132            if let Some(agreed_fee_multiplier) = agreement.fee_multiplier() {
133                let current = self.action_fee_multiplier_permille.ok_or(
134                    ProtocolError::CorruptedCodeExecution(
135                        "the batch transformer reads the fee multiplier of every batch that \
136                         declares an action fee priced by it"
137                            .to_string(),
138                    ),
139                )?;
140                if !agreed_fee_multiplier.tolerates(current) {
141                    return Ok(Err(DocumentActionFeeMultiplierNotToleratedError::new(
142                        document_type_name(),
143                        action(),
144                        agreed_fee_multiplier,
145                        current,
146                    )
147                    .into()));
148                }
149            }
150        }
151        Ok(Ok(()))
152    }
153
154    /// See `BatchTransitionAction::resolve_gas_payer`
155    pub(in crate::state_transition_action) fn resolve_gas_payer(
156        &self,
157    ) -> Result<GasPayer, ConsensusError> {
158        // `None` is the document owner; `Some` a contract owner.
159        let mut payer: Option<Option<Identifier>> = None;
160        let mut strict = false;
161        for transition in &self.transitions {
162            let transition_payer = match transition {
163                BatchedTransitionAction::DocumentAction(document_action) => {
164                    let base = document_action.base();
165                    let requested = base.gas_fees_paid_by();
166                    let offered = base.contract_gas_fees_paid_by();
167                    match GasFeesPaidBy::resolve(offered, requested) {
168                        None => {
169                            return Err(GasFeesPaidByNotAllowedError::new(
170                                base.document_type_name().clone(),
171                                document_action.action_name().to_string(),
172                                requested,
173                                offered,
174                            )
175                            .into())
176                        }
177                        Some(GasFeesPaidBy::DocumentOwner) => None,
178                        Some(GasFeesPaidBy::PreferContractOwner) => {
179                            Some(base.data_contract_fetch_info_ref().contract.owner_id())
180                        }
181                        Some(GasFeesPaidBy::ContractOwner) => {
182                            strict = true;
183                            Some(base.data_contract_fetch_info_ref().contract.owner_id())
184                        }
185                    }
186                }
187                // Token transitions are never sponsored.
188                BatchedTransitionAction::TokenAction(_) => None,
189                // A transition that already failed names no payer of its own: the execution
190                // event drops the sponsor of a batch that carries one, so its signer pays, and
191                // the error that replaced it must not be masked by an inconsistency here.
192                BatchedTransitionAction::BumpIdentityDataContractNonce(_) => continue,
193            };
194            match payer {
195                None => payer = Some(transition_payer),
196                Some(expected) if expected != transition_payer => {
197                    return Err(InconsistentGasFeesPaidByInBatchError::new(
198                        expected,
199                        transition_payer,
200                    )
201                    .into())
202                }
203                Some(_) => {}
204            }
205        }
206        Ok(match payer.flatten() {
207            None => GasPayer::DocumentOwner,
208            Some(identity_id) => GasPayer::ContractOwner {
209                identity_id,
210                strict,
211            },
212        })
213    }
214
215    pub(in crate::state_transition_action) fn all_used_balances(
216        &self,
217    ) -> Result<Option<Credits>, ProtocolError> {
218        Ok(match (self.all_purchases_amount()?, self.all_conflicting_index_collateral_voting_funds()?) {
219            (Some(all_purchases_amount), Some(all_conflicting_index_collateral_voting_funds)) => Some(all_purchases_amount.checked_add(all_conflicting_index_collateral_voting_funds).ok_or(ProtocolError::Overflow("overflow between all_purchases_amount and all_conflicting_index_collateral_voting_funds"))?),
220            (Some(all_purchases_amount), None) => Some(all_purchases_amount),
221            (None, Some(all_conflicting_index_collateral_voting_funds)) => Some(all_conflicting_index_collateral_voting_funds),
222            (None, None) => None,
223        })
224    }
225    pub(in crate::state_transition_action) fn all_purchases_amount(
226        &self,
227    ) -> Result<Option<Credits>, ProtocolError> {
228        let (total, any_purchases): (Option<Credits>, bool) = self
229            .transitions
230            .iter()
231            .filter_map(|transition| match transition {
232                BatchedTransitionAction::DocumentAction(
233                    DocumentTransitionAction::PurchaseAction(document_purchase),
234                ) => Some(document_purchase.price()),
235                BatchedTransitionAction::TokenAction(
236                    TokenTransitionAction::DirectPurchaseAction(token_purchase),
237                ) => Some(token_purchase.total_agreed_price()),
238                BatchedTransitionAction::TokenAction(
239                    // Previously swallowed by the catch-all below. The action cannot be in a
240                    // batch on the released protocol versions: the purchase-into-pool kind is
241                    // refused unpaid at the batch's `is_allowed` gate before the batch becomes
242                    // actions, so the fold sees the same sequence of prices there.
243                    TokenTransitionAction::DirectPurchaseToPoolAction(token_purchase),
244                ) => Some(token_purchase.total_agreed_price()),
245                _ => None,
246            })
247            .fold((None, false), |(acc, _), price| match acc {
248                Some(acc_val) => acc_val
249                    .checked_add(price)
250                    .map_or((None, true), |sum| (Some(sum), true)),
251                None => (Some(price), true),
252            });
253
254        match (total, any_purchases) {
255            (Some(total), _) => Ok(Some(total)),
256            (None, true) => Err(ProtocolError::Overflow("overflow in all purchases amount")), // Overflow occurred
257            _ => Ok(None), // No purchases were found
258        }
259    }
260
261    pub(in crate::state_transition_action) fn all_conflicting_index_collateral_voting_funds(
262        &self,
263    ) -> Result<Option<Credits>, ProtocolError> {
264        let (total, any_voting_funds): (Option<Credits>, bool) = self
265            .transitions
266            .iter()
267            .filter_map(|transition| match transition {
268                BatchedTransitionAction::DocumentAction(
269                    DocumentTransitionAction::CreateAction(document_create_transition_action),
270                ) => document_create_transition_action
271                    .prefunded_voting_balance()
272                    .iter()
273                    .try_fold(0u64, |acc, &(_, val)| acc.checked_add(val)),
274                _ => None,
275            })
276            .fold((None, false), |(acc, _), price| match acc {
277                Some(acc_val) => acc_val
278                    .checked_add(price)
279                    .map_or((None, true), |sum| (Some(sum), true)),
280                None => (Some(price), true),
281            });
282
283        match (total, any_voting_funds) {
284            (Some(total), _) => Ok(Some(total)),
285            (None, true) => Err(ProtocolError::Overflow(
286                "overflow in all voting funds amount",
287            )), // Overflow occurred
288            _ => Ok(None),
289        }
290    }
291}