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}