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}