mina_tree/scan_state/transaction_logic/
transaction_partially_applied.rs

1//! Two-phase transaction application
2//!
3//! This module implements the two-phase transaction application model used in Mina.
4//! This approach enables efficient proof generation, particularly for zkApp commands.
5//!
6//! # Application Phases
7//!
8//! ## First Pass
9//!
10//! The first pass ([`apply_transaction_first_pass`]) performs:
11//! - Transaction validation (nonces, balances, permissions)
12//! - Fee payment
13//! - For zkApp commands: applies fee payer and begins account update processing
14//! - For other transactions: completes the entire application
15//! - Records the previous ledger hash
16//!
17//! ## Second Pass
18//!
19//! The second pass ([`apply_transaction_second_pass`]) performs:
20//! - For zkApp commands: completes account update processing and emits events/actions
21//! - For other transactions: simply packages the results from first pass
22//!
23//! # Key Types
24//!
25//! - [`TransactionPartiallyApplied`]: Intermediate state between passes
26//! - [`ZkappCommandPartiallyApplied`]: zkApp-specific intermediate state
27//! - [`FullyApplied`]: Wrapper for non-zkApp transactions that complete in first pass
28//!
29//! # Fee Transfers and Coinbase
30//!
31//! Fee transfers and coinbase transactions also use helper functions in this module:
32//! - [`apply_fee_transfer`]: Distributes fees to block producers
33//! - [`apply_coinbase`]: Handles block rewards and optional fee transfers
34//! - [`process_fee_transfer`]: Core logic for fee distribution with permission checks
35//!
36//! Both transactions have structured failure status to indicate which part failed:
37//! - Single transfer: `[[failure]]`
38//! - Two transfers both fail: `[[failure1]; [failure2]]`
39//! - First succeeds, second fails: `[[]; [failure2]]`
40//! - First fails, second succeeds: `[[failure1]; []]`
41
42use super::{
43    transaction_applied::{CoinbaseApplied, FeeTransferApplied},
44    *,
45};
46
47#[derive(Clone, Debug)]
48pub struct ZkappCommandPartiallyApplied<L: LedgerNonSnark> {
49    pub command: ZkAppCommand,
50    pub previous_hash: Fp,
51    pub original_first_pass_account_states: Vec<(AccountId, Option<(L::Location, Box<Account>)>)>,
52    pub constraint_constants: ConstraintConstants,
53    pub state_view: ProtocolStateView,
54    pub global_state: GlobalState<L>,
55    pub local_state: LocalStateEnv<L>,
56}
57
58#[derive(Clone, Debug)]
59pub struct FullyApplied<T> {
60    pub previous_hash: Fp,
61    pub applied: T,
62}
63
64#[derive(Clone, Debug)]
65pub enum TransactionPartiallyApplied<L: LedgerNonSnark> {
66    SignedCommand(FullyApplied<SignedCommandApplied>),
67    ZkappCommand(Box<ZkappCommandPartiallyApplied<L>>),
68    FeeTransfer(FullyApplied<FeeTransferApplied>),
69    Coinbase(FullyApplied<CoinbaseApplied>),
70}
71
72impl<L> TransactionPartiallyApplied<L>
73where
74    L: LedgerNonSnark,
75{
76    pub fn command(self) -> Transaction {
77        use Transaction as T;
78
79        match self {
80            Self::SignedCommand(s) => T::Command(UserCommand::SignedCommand(Box::new(
81                s.applied.common.user_command.data,
82            ))),
83            Self::ZkappCommand(z) => T::Command(UserCommand::ZkAppCommand(Box::new(z.command))),
84            Self::FeeTransfer(ft) => T::FeeTransfer(ft.applied.fee_transfer.data),
85            Self::Coinbase(cb) => T::Coinbase(cb.applied.coinbase.data),
86        }
87    }
88}
89
90/// Applies the first pass of transaction application.
91///
92/// This function performs the initial phase of transaction processing, which includes
93/// validation, fee payment, and partial application. The behavior differs based on
94/// transaction type:
95///
96/// # Transaction Type Handling
97///
98/// - **Signed Commands** (payments, stake delegations): Fully applied in the first pass.
99///   The result is wrapped in [`FullyApplied`] since no second pass work is needed.
100///
101/// - **zkApp Commands**: Partially applied. The first pass:
102///   - Validates the zkApp command structure and permissions
103///   - Applies the fee payer account update
104///   - Begins processing the first phase of account updates
105///   - Records intermediate state in [`ZkappCommandPartiallyApplied`]
106///
107/// - **Fee Transfers**: Fully applied in the first pass, distributing fees to block
108///   producers according to the protocol rules.
109///
110/// - **Coinbase**: Fully applied in the first pass, crediting block rewards and any
111///   associated fee transfers to the designated accounts.
112///
113/// # Ledger State Changes
114///
115/// The ledger is mutated during the first pass as follows:
116///
117/// - **Signed Commands**:
118///   - Fee payer balance decreased by fee amount
119///   - Fee payer nonce incremented
120///   - Fee payer receipt chain hash updated
121///   - Fee payer timing updated based on vesting schedule
122///   - For payments: sender balance decreased, receiver balance increased
123///   - For payments: new account created if receiver doesn't exist
124///   - For stake delegations: delegate field updated
125///
126/// - **zkApp Commands**:
127///   - Fee payer account fully updated (balance, nonce, receipt chain, timing)
128///   - First phase account updates applied to ledger
129///   - New accounts may be created
130///
131/// - **Fee Transfers**:
132///   - Receiver account balances increased by fee amounts
133///   - Timing updated when balances increase
134///   - New accounts created if receivers don't exist
135///
136/// - **Coinbase**:
137///   - Block producer balance increased by reward amount
138///   - Fee transfer recipient balance increased (if applicable)
139///   - Timing updated when balances increase
140///   - New accounts created if recipients don't exist
141///
142/// # Parameters
143///
144/// - `constraint_constants`: Protocol constants including account creation fees and limits
145/// - `global_slot`: Current global slot number for timing validation
146/// - `txn_state_view`: View of the protocol state for validating transaction preconditions
147/// - `ledger`: Mutable reference to the ledger being updated
148/// - `transaction`: The transaction to apply
149///
150/// # Returns
151///
152/// Returns a [`TransactionPartiallyApplied`] containing either:
153/// - [`FullyApplied`] result for transactions that complete in first pass
154/// - [`ZkappCommandPartiallyApplied`] for zkApp commands needing second pass
155///
156/// # Errors
157///
158/// Returns an error if:
159/// - Transaction validation fails (invalid nonce, insufficient balance, etc.)
160/// - Fee payment fails
161/// - Account permissions are insufficient
162/// - Timing constraints are violated
163///
164/// ## Error Side Effects
165///
166/// When an error occurs, the ledger state depends on where the error occurred:
167///
168/// - **Errors during fee payment** (invalid nonce, nonexistent fee payer): Ledger
169///   remains completely unchanged.
170///
171/// - **Errors after fee payment** (insufficient balance for payment, permission
172///   errors): The fee has already been charged to ensure network compensation. The
173///   fee payer's account will have: balance decreased by fee, nonce incremented,
174///   receipt chain hash updated. However, the actual payment/operation is NOT
175///   performed.
176///
177/// # Tests
178///
179/// Test coverage (in `ledger/tests/test_transaction_logic_first_pass.rs`):
180///
181/// - [`test_apply_payment_success`]: successful payment with ledger state verification
182/// - [`test_apply_payment_insufficient_balance`]: payment exceeding sender balance
183/// - [`test_apply_payment_invalid_nonce`]: payment with incorrect nonce
184/// - [`test_apply_payment_nonexistent_fee_payer`]: payment from nonexistent account
185///
186/// [`test_apply_payment_success`]: ../../tests/test_transaction_logic_first_pass.rs
187/// [`test_apply_payment_insufficient_balance`]: ../../tests/test_transaction_logic_first_pass.rs
188/// [`test_apply_payment_invalid_nonce`]: ../../tests/test_transaction_logic_first_pass.rs
189/// [`test_apply_payment_nonexistent_fee_payer`]: ../../tests/test_transaction_logic_first_pass.rs
190pub fn apply_transaction_first_pass<L>(
191    constraint_constants: &ConstraintConstants,
192    global_slot: Slot,
193    txn_state_view: &ProtocolStateView,
194    ledger: &mut L,
195    transaction: &Transaction,
196) -> Result<TransactionPartiallyApplied<L>, String>
197where
198    L: LedgerNonSnark,
199{
200    use Transaction::*;
201    use UserCommand::*;
202
203    let previous_hash = ledger.merkle_root();
204    let txn_global_slot = &global_slot;
205
206    match transaction {
207        Command(SignedCommand(cmd)) => apply_user_command(
208            constraint_constants,
209            txn_state_view,
210            txn_global_slot,
211            ledger,
212            cmd,
213        )
214        .map(|applied| {
215            TransactionPartiallyApplied::SignedCommand(FullyApplied {
216                previous_hash,
217                applied,
218            })
219        }),
220        Command(ZkAppCommand(txn)) => apply_zkapp_command_first_pass(
221            constraint_constants,
222            global_slot,
223            txn_state_view,
224            None,
225            None,
226            ledger,
227            txn,
228        )
229        .map(Box::new)
230        .map(TransactionPartiallyApplied::ZkappCommand),
231        FeeTransfer(fee_transfer) => {
232            apply_fee_transfer(constraint_constants, txn_global_slot, ledger, fee_transfer).map(
233                |applied| {
234                    TransactionPartiallyApplied::FeeTransfer(FullyApplied {
235                        previous_hash,
236                        applied,
237                    })
238                },
239            )
240        }
241        Coinbase(coinbase) => {
242            apply_coinbase(constraint_constants, txn_global_slot, ledger, coinbase).map(|applied| {
243                TransactionPartiallyApplied::Coinbase(FullyApplied {
244                    previous_hash,
245                    applied,
246                })
247            })
248        }
249    }
250}
251
252/// Completes the second pass of transaction application.
253///
254/// This function finalizes transaction processing by completing any remaining work
255/// from the first pass. The behavior differs based on transaction type:
256///
257/// # Transaction Type Handling
258///
259/// - **Signed Commands**: No additional work needed. Simply unwraps the [`FullyApplied`]
260///   result from the first pass and packages it into a [`TransactionApplied`].
261///
262/// - **zkApp Commands**: Completes the second phase of application:
263///   - Processes the second phase of account updates
264///   - Emits events and actions from the zkApp execution
265///   - Updates the zkApp's on-chain state
266///   - Validates all preconditions are satisfied
267///
268/// - **Fee Transfers**: No additional work needed. Simply packages the first pass result.
269///
270/// - **Coinbase**: No additional work needed. Simply packages the first pass result.
271///
272/// # Ledger State Changes
273///
274/// The ledger is mutated during the second pass only for zkApp commands:
275///
276/// - **Signed Commands**: No ledger changes (all modifications completed in first pass)
277///
278/// - **zkApp Commands**:
279///   - Second phase account updates applied
280///   - Account balances modified based on zkApp logic
281///   - Account app state fields updated
282///   - Account permissions may be modified
283///   - Action state and event sequence numbers updated
284///   - New accounts may be created
285///
286/// - **Fee Transfers**: No ledger changes (all modifications completed in first pass)
287///
288/// - **Coinbase**: No ledger changes (all modifications completed in first pass)
289///
290/// # Parameters
291///
292/// - `constraint_constants`: Protocol constants including account creation fees and limits
293/// - `ledger`: Mutable reference to the ledger being updated
294/// - `partial_transaction`: The partially applied transaction from the first pass
295///
296/// # Returns
297///
298/// Returns a [`TransactionApplied`] containing the complete application result with:
299/// - Previous ledger hash (recorded during first pass)
300/// - Full transaction status (Applied or Failed with specific error codes)
301/// - Account updates and new account information
302/// - Events and actions (for zkApp commands)
303///
304/// # Errors
305///
306/// Returns an error if:
307/// - Second phase zkApp account updates fail
308/// - zkApp preconditions fail during second pass
309/// - Account permissions are insufficient
310///
311/// # Notes
312///
313/// For non-zkApp transactions, this function performs minimal work since the first
314/// pass already completed the application. The two-phase model exists primarily to
315/// enable efficient zkApp proof generation where different account updates can be
316/// processed in separate circuit phases.
317pub fn apply_transaction_second_pass<L>(
318    constraint_constants: &ConstraintConstants,
319    ledger: &mut L,
320    partial_transaction: TransactionPartiallyApplied<L>,
321) -> Result<TransactionApplied, String>
322where
323    L: LedgerNonSnark,
324{
325    use TransactionPartiallyApplied as P;
326
327    match partial_transaction {
328        P::SignedCommand(FullyApplied {
329            previous_hash,
330            applied,
331        }) => Ok(TransactionApplied {
332            previous_hash,
333            varying: Varying::Command(CommandApplied::SignedCommand(Box::new(applied))),
334        }),
335        P::ZkappCommand(partially_applied) => {
336            // TODO(OCaml): either here or in second phase of apply, need to update the
337            // prior global state statement for the fee payer segment to add the
338            // second phase ledger at the end
339
340            let previous_hash = partially_applied.previous_hash;
341            let applied =
342                apply_zkapp_command_second_pass(constraint_constants, ledger, *partially_applied)?;
343
344            Ok(TransactionApplied {
345                previous_hash,
346                varying: Varying::Command(CommandApplied::ZkappCommand(Box::new(applied))),
347            })
348        }
349        P::FeeTransfer(FullyApplied {
350            previous_hash,
351            applied,
352        }) => Ok(TransactionApplied {
353            previous_hash,
354            varying: Varying::FeeTransfer(applied),
355        }),
356        P::Coinbase(FullyApplied {
357            previous_hash,
358            applied,
359        }) => Ok(TransactionApplied {
360            previous_hash,
361            varying: Varying::Coinbase(applied),
362        }),
363    }
364}
365
366pub fn apply_transactions<L>(
367    constraint_constants: &ConstraintConstants,
368    global_slot: Slot,
369    txn_state_view: &ProtocolStateView,
370    ledger: &mut L,
371    txns: &[Transaction],
372) -> Result<Vec<TransactionApplied>, String>
373where
374    L: LedgerNonSnark,
375{
376    let first_pass: Vec<_> = txns
377        .iter()
378        .map(|txn| {
379            apply_transaction_first_pass(
380                constraint_constants,
381                global_slot,
382                txn_state_view,
383                ledger,
384                txn,
385            )
386        })
387        .collect::<Result<Vec<TransactionPartiallyApplied<_>>, _>>()?;
388
389    first_pass
390        .into_iter()
391        .map(|partial_transaction| {
392            apply_transaction_second_pass(constraint_constants, ledger, partial_transaction)
393        })
394        .collect()
395}
396
397pub struct FailureCollection {
398    inner: Vec<Vec<TransactionFailure>>,
399}
400
401/// <https://github.com/MinaProtocol/mina/blob/bfd1009abdbee78979ff0343cc73a3480e862f58/src/lib/transaction_logic/mina_transaction_logic.ml#L2197C1-L2210C53>
402impl FailureCollection {
403    fn empty() -> Self {
404        Self {
405            inner: Vec::default(),
406        }
407    }
408
409    fn no_failure() -> Vec<TransactionFailure> {
410        vec![]
411    }
412
413    /// <https://github.com/MinaProtocol/mina/blob/bfd1009abdbee78979ff0343cc73a3480e862f58/src/lib/transaction_logic/mina_transaction_logic.ml#L2204>
414    fn single_failure() -> Self {
415        Self {
416            inner: vec![vec![TransactionFailure::UpdateNotPermittedBalance]],
417        }
418    }
419
420    fn update_failed() -> Vec<TransactionFailure> {
421        vec![TransactionFailure::UpdateNotPermittedBalance]
422    }
423
424    /// <https://github.com/MinaProtocol/mina/blob/bfd1009abdbee78979ff0343cc73a3480e862f58/src/lib/transaction_logic/mina_transaction_logic.ml#L2208>
425    fn append_entry(list: Vec<TransactionFailure>, mut s: Self) -> Self {
426        if s.inner.is_empty() {
427            Self { inner: vec![list] }
428        } else {
429            s.inner.insert(1, list);
430            s
431        }
432    }
433
434    fn is_empty(&self) -> bool {
435        self.inner.iter().all(Vec::is_empty)
436    }
437
438    fn take(self) -> Vec<Vec<TransactionFailure>> {
439        self.inner
440    }
441}
442
443/// Structure of the failure status:
444///  I. No fee transfer and coinbase transfer fails: `[[failure]]`
445///  II. With fee transfer-
446///   Both fee transfer and coinbase fails:
447///     `[[failure-of-fee-transfer]; [failure-of-coinbase]]`
448///   Fee transfer succeeds and coinbase fails:
449///     `[[];[failure-of-coinbase]]`
450///   Fee transfer fails and coinbase succeeds:
451///     `[[failure-of-fee-transfer];[]]`
452///
453/// <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/transaction_logic/mina_transaction_logic.ml#L2022>
454pub fn apply_coinbase<L>(
455    constraint_constants: &ConstraintConstants,
456    txn_global_slot: &Slot,
457    ledger: &mut L,
458    coinbase: &Coinbase,
459) -> Result<transaction_applied::CoinbaseApplied, String>
460where
461    L: LedgerIntf,
462{
463    let Coinbase {
464        receiver,
465        amount: coinbase_amount,
466        fee_transfer,
467    } = &coinbase;
468
469    let (
470        receiver_reward,
471        new_accounts1,
472        transferee_update,
473        transferee_timing_prev,
474        failures1,
475        burned_tokens1,
476    ) = match fee_transfer {
477        None => (
478            *coinbase_amount,
479            None,
480            None,
481            None,
482            FailureCollection::empty(),
483            Amount::zero(),
484        ),
485        Some(
486            ft @ CoinbaseFeeTransfer {
487                receiver_pk: transferee,
488                fee,
489            },
490        ) => {
491            assert_ne!(transferee, receiver);
492
493            let transferee_id = ft.receiver();
494            let fee = Amount::of_fee(fee);
495
496            let receiver_reward = coinbase_amount
497                .checked_sub(&fee)
498                .ok_or_else(|| "Coinbase fee transfer too large".to_string())?;
499
500            let (transferee_account, action, can_receive) =
501                has_permission_to_receive(ledger, &transferee_id);
502            let new_accounts = get_new_accounts(action, transferee_id.clone());
503
504            let timing = update_timing_when_no_deduction(txn_global_slot, &transferee_account)?;
505
506            let balance = {
507                let amount = sub_account_creation_fee(constraint_constants, action, fee)?;
508                add_amount(transferee_account.balance, amount)?
509            };
510
511            if can_receive.0 {
512                let (_, mut transferee_account, transferee_location) =
513                    ledger.get_or_create(&transferee_id)?;
514
515                transferee_account.balance = balance;
516                transferee_account.timing = timing;
517
518                let timing = transferee_account.timing.clone();
519
520                (
521                    receiver_reward,
522                    new_accounts,
523                    Some((transferee_location, transferee_account)),
524                    Some(timing),
525                    FailureCollection::append_entry(
526                        FailureCollection::no_failure(),
527                        FailureCollection::empty(),
528                    ),
529                    Amount::zero(),
530                )
531            } else {
532                (
533                    receiver_reward,
534                    None,
535                    None,
536                    None,
537                    FailureCollection::single_failure(),
538                    fee,
539                )
540            }
541        }
542    };
543
544    let receiver_id = AccountId::new(receiver.clone(), TokenId::default());
545    let (receiver_account, action2, can_receive) = has_permission_to_receive(ledger, &receiver_id);
546    let new_accounts2 = get_new_accounts(action2, receiver_id.clone());
547
548    // Note: Updating coinbase receiver timing only if there is no fee transfer.
549    // This is so as to not add any extra constraints in transaction snark for checking
550    // "receiver" timings. This is OK because timing rules will not be violated when
551    // balance increases and will be checked whenever an amount is deducted from the
552    // account (#5973)
553
554    let coinbase_receiver_timing = match transferee_timing_prev {
555        None => update_timing_when_no_deduction(txn_global_slot, &receiver_account)?,
556        Some(_) => receiver_account.timing.clone(),
557    };
558
559    let receiver_balance = {
560        let amount = sub_account_creation_fee(constraint_constants, action2, receiver_reward)?;
561        add_amount(receiver_account.balance, amount)?
562    };
563
564    let (failures, burned_tokens2) = if can_receive.0 {
565        let (_action2, mut receiver_account, receiver_location) =
566            ledger.get_or_create(&receiver_id)?;
567
568        receiver_account.balance = receiver_balance;
569        receiver_account.timing = coinbase_receiver_timing;
570
571        ledger.set(&receiver_location, receiver_account);
572
573        (
574            FailureCollection::append_entry(FailureCollection::no_failure(), failures1),
575            Amount::zero(),
576        )
577    } else {
578        (
579            FailureCollection::append_entry(FailureCollection::update_failed(), failures1),
580            receiver_reward,
581        )
582    };
583
584    if let Some((addr, account)) = transferee_update {
585        ledger.set(&addr, account);
586    };
587
588    let burned_tokens = burned_tokens1
589        .checked_add(&burned_tokens2)
590        .ok_or_else(|| "burned tokens overflow".to_string())?;
591
592    let status = if failures.is_empty() {
593        TransactionStatus::Applied
594    } else {
595        TransactionStatus::Failed(failures.take())
596    };
597
598    let new_accounts: Vec<_> = [new_accounts1, new_accounts2]
599        .into_iter()
600        .flatten()
601        .collect();
602
603    Ok(transaction_applied::CoinbaseApplied {
604        coinbase: WithStatus {
605            data: coinbase.clone(),
606            status,
607        },
608        new_accounts,
609        burned_tokens,
610    })
611}
612
613/// <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/transaction_logic/mina_transaction_logic.ml#L1991>
614pub fn apply_fee_transfer<L>(
615    constraint_constants: &ConstraintConstants,
616    txn_global_slot: &Slot,
617    ledger: &mut L,
618    fee_transfer: &FeeTransfer,
619) -> Result<transaction_applied::FeeTransferApplied, String>
620where
621    L: LedgerIntf,
622{
623    let (new_accounts, failures, burned_tokens) = process_fee_transfer(
624        ledger,
625        fee_transfer,
626        |action, _, balance, fee| {
627            let amount = {
628                let amount = Amount::of_fee(fee);
629                sub_account_creation_fee(constraint_constants, action, amount)?
630            };
631            add_amount(balance, amount)
632        },
633        |account| update_timing_when_no_deduction(txn_global_slot, account),
634    )?;
635
636    let status = if failures.is_empty() {
637        TransactionStatus::Applied
638    } else {
639        TransactionStatus::Failed(failures.take())
640    };
641
642    Ok(transaction_applied::FeeTransferApplied {
643        fee_transfer: WithStatus {
644            data: fee_transfer.clone(),
645            status,
646        },
647        new_accounts,
648        burned_tokens,
649    })
650}
651
652/// <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/transaction_logic/mina_transaction_logic.ml#L607>
653fn sub_account_creation_fee(
654    constraint_constants: &ConstraintConstants,
655    action: AccountState,
656    amount: Amount,
657) -> Result<Amount, String> {
658    let account_creation_fee = Amount::from_u64(constraint_constants.account_creation_fee);
659
660    match action {
661        AccountState::Added => {
662            if let Some(amount) = amount.checked_sub(&account_creation_fee) {
663                return Ok(amount);
664            }
665            Err(format!(
666                "Error subtracting account creation fee {:?}; transaction amount {:?} insufficient",
667                account_creation_fee, amount
668            ))
669        }
670        AccountState::Existed => Ok(amount),
671    }
672}
673
674fn update_timing_when_no_deduction(
675    txn_global_slot: &Slot,
676    account: &Account,
677) -> Result<Timing, String> {
678    validate_timing(account, Amount::zero(), txn_global_slot)
679}
680
681fn get_new_accounts<T>(action: AccountState, data: T) -> Option<T> {
682    match action {
683        AccountState::Added => Some(data),
684        AccountState::Existed => None,
685    }
686}
687
688/// Structure of the failure status:
689///  I. Only one fee transfer in the transaction (`One) and it fails:
690///     [[failure]]
691///  II. Two fee transfers in the transaction (`Two)-
692///   Both fee transfers fail:
693///     [[failure-of-first-fee-transfer]; [failure-of-second-fee-transfer]]
694///   First succeeds and second one fails:
695///     [[];[failure-of-second-fee-transfer]]
696///   First fails and second succeeds:
697///     [[failure-of-first-fee-transfer];[]]
698pub fn process_fee_transfer<L, FunBalance, FunTiming>(
699    ledger: &mut L,
700    fee_transfer: &FeeTransfer,
701    modify_balance: FunBalance,
702    modify_timing: FunTiming,
703) -> Result<(Vec<AccountId>, FailureCollection, Amount), String>
704where
705    L: LedgerIntf,
706    FunTiming: Fn(&Account) -> Result<Timing, String>,
707    FunBalance: Fn(AccountState, &AccountId, Balance, &Fee) -> Result<Balance, String>,
708{
709    if !fee_transfer.fee_tokens().all(TokenId::is_default) {
710        return Err("Cannot pay fees in non-default tokens.".to_string());
711    }
712
713    match &**fee_transfer {
714        OneOrTwo::One(fee_transfer) => {
715            let account_id = fee_transfer.receiver();
716            let (a, action, can_receive) = has_permission_to_receive(ledger, &account_id);
717
718            let timing = modify_timing(&a)?;
719            let balance = modify_balance(action, &account_id, a.balance, &fee_transfer.fee)?;
720
721            if can_receive.0 {
722                let (_, mut account, loc) = ledger.get_or_create(&account_id)?;
723                let new_accounts = get_new_accounts(action, account_id.clone());
724
725                account.balance = balance;
726                account.timing = timing;
727
728                ledger.set(&loc, account);
729
730                let new_accounts: Vec<_> = new_accounts.into_iter().collect();
731                Ok((new_accounts, FailureCollection::empty(), Amount::zero()))
732            } else {
733                Ok((
734                    vec![],
735                    FailureCollection::single_failure(),
736                    Amount::of_fee(&fee_transfer.fee),
737                ))
738            }
739        }
740        OneOrTwo::Two((fee_transfer1, fee_transfer2)) => {
741            let account_id1 = fee_transfer1.receiver();
742            let (a1, action1, can_receive1) = has_permission_to_receive(ledger, &account_id1);
743
744            let account_id2 = fee_transfer2.receiver();
745
746            if account_id1 == account_id2 {
747                let fee = fee_transfer1
748                    .fee
749                    .checked_add(&fee_transfer2.fee)
750                    .ok_or_else(|| "Overflow".to_string())?;
751
752                let timing = modify_timing(&a1)?;
753                let balance = modify_balance(action1, &account_id1, a1.balance, &fee)?;
754
755                if can_receive1.0 {
756                    let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?;
757                    let new_accounts1 = get_new_accounts(action1, account_id1);
758
759                    a1.balance = balance;
760                    a1.timing = timing;
761
762                    ledger.set(&l1, a1);
763
764                    let new_accounts: Vec<_> = new_accounts1.into_iter().collect();
765                    Ok((new_accounts, FailureCollection::empty(), Amount::zero()))
766                } else {
767                    // failure for each fee transfer single
768
769                    Ok((
770                        vec![],
771                        FailureCollection::append_entry(
772                            FailureCollection::update_failed(),
773                            FailureCollection::single_failure(),
774                        ),
775                        Amount::of_fee(&fee),
776                    ))
777                }
778            } else {
779                let (a2, action2, can_receive2) = has_permission_to_receive(ledger, &account_id2);
780
781                let balance1 =
782                    modify_balance(action1, &account_id1, a1.balance, &fee_transfer1.fee)?;
783
784                // Note: Not updating the timing field of a1 to avoid additional check
785                // in transactions snark (check_timing for "receiver"). This is OK
786                // because timing rules will not be violated when balance increases
787                // and will be checked whenever an amount is deducted from the account. (#5973)*)
788
789                let timing2 = modify_timing(&a2)?;
790                let balance2 =
791                    modify_balance(action2, &account_id2, a2.balance, &fee_transfer2.fee)?;
792
793                let (new_accounts1, failures, burned_tokens1) = if can_receive1.0 {
794                    let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?;
795                    let new_accounts1 = get_new_accounts(action1, account_id1);
796
797                    a1.balance = balance1;
798                    ledger.set(&l1, a1);
799
800                    (
801                        new_accounts1,
802                        FailureCollection::append_entry(
803                            FailureCollection::no_failure(),
804                            FailureCollection::empty(),
805                        ),
806                        Amount::zero(),
807                    )
808                } else {
809                    (
810                        None,
811                        FailureCollection::single_failure(),
812                        Amount::of_fee(&fee_transfer1.fee),
813                    )
814                };
815
816                let (new_accounts2, failures, burned_tokens2) = if can_receive2.0 {
817                    let (_, mut a2, l2) = ledger.get_or_create(&account_id2)?;
818                    let new_accounts2 = get_new_accounts(action2, account_id2);
819
820                    a2.balance = balance2;
821                    a2.timing = timing2;
822
823                    ledger.set(&l2, a2);
824
825                    (
826                        new_accounts2,
827                        FailureCollection::append_entry(FailureCollection::no_failure(), failures),
828                        Amount::zero(),
829                    )
830                } else {
831                    (
832                        None,
833                        FailureCollection::append_entry(
834                            FailureCollection::update_failed(),
835                            failures,
836                        ),
837                        Amount::of_fee(&fee_transfer2.fee),
838                    )
839                };
840
841                let burned_tokens = burned_tokens1
842                    .checked_add(&burned_tokens2)
843                    .ok_or_else(|| "burned tokens overflow".to_string())?;
844
845                let new_accounts: Vec<_> = [new_accounts1, new_accounts2]
846                    .into_iter()
847                    .flatten()
848                    .collect();
849
850                Ok((new_accounts, failures, burned_tokens))
851            }
852        }
853    }
854}
855
856#[derive(Copy, Clone, Debug)]
857pub enum AccountState {
858    Added,
859    Existed,
860}
861
862#[derive(Debug)]
863struct HasPermissionToReceive(bool);
864
865/// <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/transaction_logic/mina_transaction_logic.ml#L1852>
866fn has_permission_to_receive<L>(
867    ledger: &mut L,
868    receiver_account_id: &AccountId,
869) -> (Box<Account>, AccountState, HasPermissionToReceive)
870where
871    L: LedgerIntf,
872{
873    use crate::PermissionTo::*;
874    use AccountState::*;
875
876    let init_account = Account::initialize(receiver_account_id);
877
878    match ledger.location_of_account(receiver_account_id) {
879        None => {
880            // new account, check that default permissions allow receiving
881            let perm = init_account.has_permission_to(ControlTag::NoneGiven, Receive);
882            (Box::new(init_account), Added, HasPermissionToReceive(perm))
883        }
884        Some(location) => match ledger.get(&location) {
885            None => panic!("Ledger location with no account"),
886            Some(receiver_account) => {
887                let perm = receiver_account.has_permission_to(ControlTag::NoneGiven, Receive);
888                (receiver_account, Existed, HasPermissionToReceive(perm))
889            }
890        },
891    }
892}
893
894pub fn validate_time(valid_until: &Slot, current_global_slot: &Slot) -> Result<(), String> {
895    if current_global_slot <= valid_until {
896        return Ok(());
897    }
898
899    Err(format!(
900        "Current global slot {:?} greater than transaction expiry slot {:?}",
901        current_global_slot, valid_until
902    ))
903}
904
905pub fn is_timed(a: &Account) -> bool {
906    matches!(&a.timing, Timing::Timed { .. })
907}
908
909pub fn set_with_location<L>(
910    ledger: &mut L,
911    location: &ExistingOrNew<L::Location>,
912    account: Box<Account>,
913) -> Result<(), String>
914where
915    L: LedgerIntf,
916{
917    match location {
918        ExistingOrNew::Existing(location) => {
919            ledger.set(location, account);
920            Ok(())
921        }
922        ExistingOrNew::New => ledger
923            .create_new_account(account.id(), *account)
924            .map_err(|_| "set_with_location".to_string()),
925    }
926}
927
928pub struct Updates<Location> {
929    pub located_accounts: Vec<(ExistingOrNew<Location>, Box<Account>)>,
930    pub applied_body: signed_command_applied::Body,
931}
932
933pub fn compute_updates<L>(
934    constraint_constants: &ConstraintConstants,
935    receiver: AccountId,
936    ledger: &mut L,
937    current_global_slot: &Slot,
938    user_command: &SignedCommand,
939    fee_payer: &AccountId,
940    fee_payer_account: &Account,
941    fee_payer_location: &ExistingOrNew<L::Location>,
942    reject_command: &mut bool,
943) -> Result<Updates<L::Location>, TransactionFailure>
944where
945    L: LedgerIntf,
946{
947    match &user_command.payload.body {
948        signed_command::Body::StakeDelegation(_) => {
949            let (receiver_location, _) = get_with_location(ledger, &receiver).unwrap();
950
951            if let ExistingOrNew::New = receiver_location {
952                return Err(TransactionFailure::ReceiverNotPresent);
953            }
954            if !fee_payer_account.has_permission_to_set_delegate() {
955                return Err(TransactionFailure::UpdateNotPermittedDelegate);
956            }
957
958            let previous_delegate = fee_payer_account.delegate.clone();
959
960            // Timing is always valid, but we need to record any switch from
961            // timed to untimed here to stay in sync with the snark.
962            let fee_payer_account = {
963                let timing = timing_error_to_user_command_status(validate_timing(
964                    fee_payer_account,
965                    Amount::zero(),
966                    current_global_slot,
967                ))?;
968
969                Box::new(Account {
970                    delegate: Some(receiver.public_key.clone()),
971                    timing,
972                    ..fee_payer_account.clone()
973                })
974            };
975
976            Ok(Updates {
977                located_accounts: vec![(fee_payer_location.clone(), fee_payer_account)],
978                applied_body: signed_command_applied::Body::StakeDelegation { previous_delegate },
979            })
980        }
981        signed_command::Body::Payment(payment) => {
982            let get_fee_payer_account = || {
983                let balance = fee_payer_account
984                    .balance
985                    .sub_amount(payment.amount)
986                    .ok_or(TransactionFailure::SourceInsufficientBalance)?;
987
988                let timing = timing_error_to_user_command_status(validate_timing(
989                    fee_payer_account,
990                    payment.amount,
991                    current_global_slot,
992                ))?;
993
994                Ok(Box::new(Account {
995                    balance,
996                    timing,
997                    ..fee_payer_account.clone()
998                }))
999            };
1000
1001            let fee_payer_account = match get_fee_payer_account() {
1002                Ok(fee_payer_account) => fee_payer_account,
1003                Err(e) => {
1004                    // OCaml throw an exception when an error occurs here
1005                    // Here in Rust we set `reject_command` to differentiate the 3 cases (Ok, Err, exception)
1006                    //
1007                    // <https://github.com/MinaProtocol/mina/blob/bfd1009abdbee78979ff0343cc73a3480e862f58/src/lib/transaction_logic/mina_transaction_logic.ml#L962>
1008
1009                    // Don't accept transactions with insufficient balance from the fee-payer.
1010                    // TODO(OCaml): eliminate this condition and accept transaction with failed status
1011                    *reject_command = true;
1012                    return Err(e);
1013                }
1014            };
1015
1016            let (receiver_location, mut receiver_account) = if fee_payer == &receiver {
1017                (fee_payer_location.clone(), fee_payer_account.clone())
1018            } else {
1019                get_with_location(ledger, &receiver).unwrap()
1020            };
1021
1022            if !fee_payer_account.has_permission_to_send() {
1023                return Err(TransactionFailure::UpdateNotPermittedBalance);
1024            }
1025
1026            if !receiver_account.has_permission_to_receive() {
1027                return Err(TransactionFailure::UpdateNotPermittedBalance);
1028            }
1029
1030            let receiver_amount = match &receiver_location {
1031                ExistingOrNew::Existing(_) => payment.amount,
1032                ExistingOrNew::New => {
1033                    match payment
1034                        .amount
1035                        .checked_sub(&Amount::from_u64(constraint_constants.account_creation_fee))
1036                    {
1037                        Some(amount) => amount,
1038                        None => return Err(TransactionFailure::AmountInsufficientToCreateAccount),
1039                    }
1040                }
1041            };
1042
1043            let balance = match receiver_account.balance.add_amount(receiver_amount) {
1044                Some(balance) => balance,
1045                None => return Err(TransactionFailure::Overflow),
1046            };
1047
1048            let new_accounts = match receiver_location {
1049                ExistingOrNew::New => vec![receiver.clone()],
1050                ExistingOrNew::Existing(_) => vec![],
1051            };
1052
1053            receiver_account.balance = balance;
1054
1055            let updated_accounts = if fee_payer == &receiver {
1056                // [receiver_account] at this point has all the updates
1057                vec![(receiver_location, receiver_account)]
1058            } else {
1059                vec![
1060                    (receiver_location, receiver_account),
1061                    (fee_payer_location.clone(), fee_payer_account),
1062                ]
1063            };
1064
1065            Ok(Updates {
1066                located_accounts: updated_accounts,
1067                applied_body: signed_command_applied::Body::Payments { new_accounts },
1068            })
1069        }
1070    }
1071}
1072
1073pub fn apply_user_command_unchecked<L>(
1074    constraint_constants: &ConstraintConstants,
1075    _txn_state_view: &ProtocolStateView,
1076    txn_global_slot: &Slot,
1077    ledger: &mut L,
1078    user_command: &SignedCommand,
1079) -> Result<SignedCommandApplied, String>
1080where
1081    L: LedgerIntf,
1082{
1083    let SignedCommand {
1084        payload: _,
1085        signer: signer_pk,
1086        signature: _,
1087    } = &user_command;
1088    let current_global_slot = txn_global_slot;
1089
1090    let valid_until = user_command.valid_until();
1091    validate_time(&valid_until, current_global_slot)?;
1092
1093    // Fee-payer information
1094    let fee_payer = user_command.fee_payer();
1095    let (fee_payer_location, fee_payer_account) =
1096        pay_fee(user_command, signer_pk, ledger, current_global_slot)?;
1097
1098    if !fee_payer_account.has_permission_to_send() {
1099        return Err(TransactionFailure::UpdateNotPermittedBalance.to_string());
1100    }
1101    if !fee_payer_account.has_permission_to_increment_nonce() {
1102        return Err(TransactionFailure::UpdateNotPermittedNonce.to_string());
1103    }
1104
1105    // Charge the fee. This must happen, whether or not the command itself
1106    // succeeds, to ensure that the network is compensated for processing this
1107    // command.
1108    set_with_location(ledger, &fee_payer_location, fee_payer_account.clone())?;
1109
1110    let receiver = user_command.receiver();
1111
1112    let mut reject_command = false;
1113
1114    match compute_updates(
1115        constraint_constants,
1116        receiver,
1117        ledger,
1118        current_global_slot,
1119        user_command,
1120        &fee_payer,
1121        &fee_payer_account,
1122        &fee_payer_location,
1123        &mut reject_command,
1124    ) {
1125        Ok(Updates {
1126            located_accounts,
1127            applied_body,
1128        }) => {
1129            for (location, account) in located_accounts {
1130                set_with_location(ledger, &location, account)?;
1131            }
1132
1133            Ok(SignedCommandApplied {
1134                common: signed_command_applied::Common {
1135                    user_command: WithStatus::<SignedCommand> {
1136                        data: user_command.clone(),
1137                        status: TransactionStatus::Applied,
1138                    },
1139                },
1140                body: applied_body,
1141            })
1142        }
1143        Err(failure) if !reject_command => Ok(SignedCommandApplied {
1144            common: signed_command_applied::Common {
1145                user_command: WithStatus::<SignedCommand> {
1146                    data: user_command.clone(),
1147                    status: TransactionStatus::Failed(vec![vec![failure]]),
1148                },
1149            },
1150            body: signed_command_applied::Body::Failed,
1151        }),
1152        Err(failure) => {
1153            // This case occurs when an exception is throwned in OCaml
1154            // <https://github.com/MinaProtocol/mina/blob/3753a8593cc1577bcf4da16620daf9946d88e8e5/src/lib/transaction_logic/mina_transaction_logic.ml#L964>
1155            assert!(reject_command);
1156            Err(failure.to_string())
1157        }
1158    }
1159}
1160
1161pub fn apply_user_command<L>(
1162    constraint_constants: &ConstraintConstants,
1163    txn_state_view: &ProtocolStateView,
1164    txn_global_slot: &Slot,
1165    ledger: &mut L,
1166    user_command: &SignedCommand,
1167) -> Result<SignedCommandApplied, String>
1168where
1169    L: LedgerIntf,
1170{
1171    apply_user_command_unchecked(
1172        constraint_constants,
1173        txn_state_view,
1174        txn_global_slot,
1175        ledger,
1176        user_command,
1177    )
1178}
1179
1180pub fn pay_fee<L, Loc>(
1181    user_command: &SignedCommand,
1182    signer_pk: &CompressedPubKey,
1183    ledger: &mut L,
1184    current_global_slot: &Slot,
1185) -> Result<(ExistingOrNew<Loc>, Box<Account>), String>
1186where
1187    L: LedgerIntf<Location = Loc>,
1188{
1189    let nonce = user_command.nonce();
1190    let fee_payer = user_command.fee_payer();
1191    let fee_token = user_command.fee_token();
1192
1193    if &fee_payer.public_key != signer_pk {
1194        return Err("Cannot pay fees from a public key that did not sign the transaction".into());
1195    }
1196
1197    if fee_token != TokenId::default() {
1198        return Err("Cannot create transactions with fee_token different from the default".into());
1199    }
1200
1201    pay_fee_impl(
1202        &user_command.payload,
1203        nonce,
1204        fee_payer,
1205        user_command.fee(),
1206        ledger,
1207        current_global_slot,
1208    )
1209}
1210
1211pub fn pay_fee_impl<L>(
1212    command: &SignedCommandPayload,
1213    nonce: Nonce,
1214    fee_payer: AccountId,
1215    fee: Fee,
1216    ledger: &mut L,
1217    current_global_slot: &Slot,
1218) -> Result<(ExistingOrNew<L::Location>, Box<Account>), String>
1219where
1220    L: LedgerIntf,
1221{
1222    // Fee-payer information
1223    let (location, mut account) = get_with_location(ledger, &fee_payer)?;
1224
1225    if let ExistingOrNew::New = location {
1226        return Err("The fee-payer account does not exist".to_string());
1227    };
1228
1229    let fee = Amount::of_fee(&fee);
1230    let balance = sub_amount(account.balance, fee)?;
1231
1232    validate_nonces(nonce, account.nonce)?;
1233    let timing = validate_timing(&account, fee, current_global_slot)?;
1234
1235    account.balance = balance;
1236    account.nonce = account.nonce.incr(); // TODO: Not sure if OCaml wraps
1237    account.receipt_chain_hash = cons_signed_command_payload(command, account.receipt_chain_hash);
1238    account.timing = timing;
1239
1240    Ok((location, account))
1241}