Skip to main content

mina_tree/scan_state/transaction_logic/
mod.rs

1//! Transaction logic module
2//!
3//! This module implements the core logic for applying and validating all
4//! transaction types in the Mina protocol. It is a direct port of the OCaml
5//! implementation from `src/lib/transaction_logic/mina_transaction_logic.ml`
6//! and maintains identical business logic.
7//!
8//! # Transaction Types
9//!
10//! The module handles three main categories of transactions:
11//!
12//! ## User Commands
13//! - **Signed Commands**: Payments and stake delegations
14//! - **zkApp Commands**: Complex multi-account zero-knowledge operations
15//!
16//! ## Protocol Transactions
17//! - **Fee Transfers**: Distribution of transaction fees to block producers
18//! - **Coinbase**: Block rewards for successful block production
19//!
20//! # Two-Phase Application
21//!
22//! Transaction application follows a two-phase model to enable efficient proof
23//! generation:
24//!
25//! 1. **First Pass** ([`apply_transaction_first_pass`]): Validates
26//!    preconditions and begins application. For zkApp commands, applies the fee
27//!    payer and first phase of account updates.
28//!
29//! 2. **Second Pass** ([`apply_transaction_second_pass`]): Completes
30//!    application. For zkApp commands, applies the second phase of account
31//!    updates and finalizes state.
32//!
33//! # Key Types
34//!
35//! - [`Transaction`]: Top-level enum for all transaction types
36//! - [`UserCommand`]: User-initiated transactions (signed or zkApp)
37//! - [`TransactionStatus`]: Applied or failed with specific error codes
38//! - [`TransactionFailure`]: 50+ specific failure reasons
39//! - [`FeeTransfer`]: Fee distribution transaction
40//! - [`Coinbase`]: Block reward transaction
41//!
42//! # Module Organization
43//!
44//! - [`local_state`]: Local state management during zkApp application
45//! - [`protocol_state`]: Protocol state views for transaction application
46//! - [`signed_command`]: Payment and stake delegation logic
47//! - [`transaction_applied`]: Final transaction application results
48//! - [`transaction_partially_applied`]: Two-phase transaction application
49//! - [`transaction_union_payload`]: Unified transaction representation for SNARK circuits
50//! - [`transaction_witness`]: Witness generation for transaction proofs
51//! - [`valid`]: Valid (but not yet verified) user commands
52//! - [`verifiable`]: Verifiable user commands ready for proof verification
53//! - [`zkapp_command`]: zkApp command processing
54//! - [`zkapp_statement`]: zkApp statement types for proof generation
55
56use self::{
57    local_state::{apply_zkapp_command_first_pass, apply_zkapp_command_second_pass, LocalStateEnv},
58    protocol_state::{GlobalState, ProtocolStateView},
59    signed_command::{SignedCommand, SignedCommandPayload},
60    transaction_applied::{
61        signed_command_applied::{self, SignedCommandApplied},
62        TransactionApplied,
63    },
64    zkapp_command::{AccessedOrNot, ZkAppCommand},
65};
66use super::{
67    currency::{Amount, Balance, Fee, Magnitude, Nonce, Signed, Slot},
68    fee_excess::FeeExcess,
69    fee_rate::FeeRate,
70    scan_state::transaction_snark::OneOrTwo,
71};
72use crate::{
73    scan_state::transaction_logic::{
74        transaction_applied::{CommandApplied, Varying},
75        zkapp_command::MaybeWithStatus,
76    },
77    sparse_ledger::LedgerIntf,
78    zkapps::non_snark::LedgerNonSnark,
79    Account, AccountId, BaseLedger, ControlTag, Timing, TokenId, VerificationKeyWire,
80};
81use mina_core::constants::ConstraintConstants;
82use mina_curves::pasta::Fp;
83use mina_macros::SerdeYojsonEnum;
84use mina_p2p_messages::{
85    bigint::InvalidBigInt,
86    binprot,
87    v2::{MinaBaseUserCommandStableV2, MinaTransactionTransactionStableV2},
88};
89use mina_signer::CompressedPubKey;
90use poseidon::hash::params::MINA_ZKAPP_MEMO;
91use std::{
92    collections::{BTreeMap, HashMap, HashSet},
93    fmt::Display,
94};
95
96pub mod local_state;
97pub mod protocol_state;
98pub mod signed_command;
99pub mod transaction_applied;
100pub mod transaction_partially_applied;
101pub mod transaction_union_payload;
102pub mod transaction_witness;
103pub mod valid;
104pub mod verifiable;
105pub mod zkapp_command;
106pub mod zkapp_statement;
107pub use transaction_partially_applied::{
108    apply_transaction_first_pass, apply_transaction_second_pass, apply_transactions,
109    apply_user_command, set_with_location, AccountState,
110};
111pub use transaction_union_payload::{
112    account_check_timing, add_amount, checked_cons_signed_command_payload,
113    cons_signed_command_payload, cons_zkapp_command_commitment, get_with_location, sub_amount,
114    timing_error_to_user_command_status, validate_nonces, validate_timing, Body, Common,
115    ExistingOrNew, Tag, TimingValidation, TransactionUnion, TransactionUnionPayload,
116};
117
118/// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/transaction_status.ml#L9-L51>
119#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
120pub enum TransactionFailure {
121    Predicate,
122    SourceNotPresent,
123    ReceiverNotPresent,
124    AmountInsufficientToCreateAccount,
125    CannotPayCreationFeeInToken,
126    SourceInsufficientBalance,
127    SourceMinimumBalanceViolation,
128    ReceiverAlreadyExists,
129    TokenOwnerNotCaller,
130    Overflow,
131    GlobalExcessOverflow,
132    LocalExcessOverflow,
133    LocalSupplyIncreaseOverflow,
134    GlobalSupplyIncreaseOverflow,
135    SignedCommandOnZkappAccount,
136    ZkappAccountNotPresent,
137    UpdateNotPermittedBalance,
138    UpdateNotPermittedAccess,
139    UpdateNotPermittedTiming,
140    UpdateNotPermittedDelegate,
141    UpdateNotPermittedAppState,
142    UpdateNotPermittedVerificationKey,
143    UpdateNotPermittedActionState,
144    UpdateNotPermittedZkappUri,
145    UpdateNotPermittedTokenSymbol,
146    UpdateNotPermittedPermissions,
147    UpdateNotPermittedNonce,
148    UpdateNotPermittedVotingFor,
149    ZkappCommandReplayCheckFailed,
150    FeePayerNonceMustIncrease,
151    FeePayerMustBeSigned,
152    AccountBalancePreconditionUnsatisfied,
153    AccountNoncePreconditionUnsatisfied,
154    AccountReceiptChainHashPreconditionUnsatisfied,
155    AccountDelegatePreconditionUnsatisfied,
156    AccountActionStatePreconditionUnsatisfied,
157    AccountAppStatePreconditionUnsatisfied(u64),
158    AccountProvedStatePreconditionUnsatisfied,
159    AccountIsNewPreconditionUnsatisfied,
160    ProtocolStatePreconditionUnsatisfied,
161    UnexpectedVerificationKeyHash,
162    ValidWhilePreconditionUnsatisfied,
163    IncorrectNonce,
164    InvalidFeeExcess,
165    Cancelled,
166}
167
168impl Display for TransactionFailure {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        let message = match self {
171            Self::Predicate => "Predicate",
172            Self::SourceNotPresent => "Source_not_present",
173            Self::ReceiverNotPresent => "Receiver_not_present",
174            Self::AmountInsufficientToCreateAccount => "Amount_insufficient_to_create_account",
175            Self::CannotPayCreationFeeInToken => "Cannot_pay_creation_fee_in_token",
176            Self::SourceInsufficientBalance => "Source_insufficient_balance",
177            Self::SourceMinimumBalanceViolation => "Source_minimum_balance_violation",
178            Self::ReceiverAlreadyExists => "Receiver_already_exists",
179            Self::TokenOwnerNotCaller => "Token_owner_not_caller",
180            Self::Overflow => "Overflow",
181            Self::GlobalExcessOverflow => "Global_excess_overflow",
182            Self::LocalExcessOverflow => "Local_excess_overflow",
183            Self::LocalSupplyIncreaseOverflow => "Local_supply_increase_overflow",
184            Self::GlobalSupplyIncreaseOverflow => "Global_supply_increase_overflow",
185            Self::SignedCommandOnZkappAccount => "Signed_command_on_zkapp_account",
186            Self::ZkappAccountNotPresent => "Zkapp_account_not_present",
187            Self::UpdateNotPermittedBalance => "Update_not_permitted_balance",
188            Self::UpdateNotPermittedAccess => "Update_not_permitted_access",
189            Self::UpdateNotPermittedTiming => "Update_not_permitted_timing",
190            Self::UpdateNotPermittedDelegate => "update_not_permitted_delegate",
191            Self::UpdateNotPermittedAppState => "Update_not_permitted_app_state",
192            Self::UpdateNotPermittedVerificationKey => "Update_not_permitted_verification_key",
193            Self::UpdateNotPermittedActionState => "Update_not_permitted_action_state",
194            Self::UpdateNotPermittedZkappUri => "Update_not_permitted_zkapp_uri",
195            Self::UpdateNotPermittedTokenSymbol => "Update_not_permitted_token_symbol",
196            Self::UpdateNotPermittedPermissions => "Update_not_permitted_permissions",
197            Self::UpdateNotPermittedNonce => "Update_not_permitted_nonce",
198            Self::UpdateNotPermittedVotingFor => "Update_not_permitted_voting_for",
199            Self::ZkappCommandReplayCheckFailed => "Zkapp_command_replay_check_failed",
200            Self::FeePayerNonceMustIncrease => "Fee_payer_nonce_must_increase",
201            Self::FeePayerMustBeSigned => "Fee_payer_must_be_signed",
202            Self::AccountBalancePreconditionUnsatisfied => {
203                "Account_balance_precondition_unsatisfied"
204            }
205            Self::AccountNoncePreconditionUnsatisfied => "Account_nonce_precondition_unsatisfied",
206            Self::AccountReceiptChainHashPreconditionUnsatisfied => {
207                "Account_receipt_chain_hash_precondition_unsatisfied"
208            }
209            Self::AccountDelegatePreconditionUnsatisfied => {
210                "Account_delegate_precondition_unsatisfied"
211            }
212            Self::AccountActionStatePreconditionUnsatisfied => {
213                "Account_action_state_precondition_unsatisfied"
214            }
215            Self::AccountAppStatePreconditionUnsatisfied(i) => {
216                return write!(f, "Account_app_state_{}_precondition_unsatisfied", i);
217            }
218            Self::AccountProvedStatePreconditionUnsatisfied => {
219                "Account_proved_state_precondition_unsatisfied"
220            }
221            Self::AccountIsNewPreconditionUnsatisfied => "Account_is_new_precondition_unsatisfied",
222            Self::ProtocolStatePreconditionUnsatisfied => "Protocol_state_precondition_unsatisfied",
223            Self::IncorrectNonce => "Incorrect_nonce",
224            Self::InvalidFeeExcess => "Invalid_fee_excess",
225            Self::Cancelled => "Cancelled",
226            Self::UnexpectedVerificationKeyHash => "Unexpected_verification_key_hash",
227            Self::ValidWhilePreconditionUnsatisfied => "Valid_while_precondition_unsatisfied",
228        };
229
230        write!(f, "{}", message)
231    }
232}
233
234/// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/transaction_status.ml#L452-L454>
235#[derive(SerdeYojsonEnum, Debug, Clone, PartialEq, Eq)]
236pub enum TransactionStatus {
237    Applied,
238    Failed(Vec<Vec<TransactionFailure>>),
239}
240
241impl TransactionStatus {
242    pub fn is_applied(&self) -> bool {
243        matches!(self, Self::Applied)
244    }
245    pub fn is_failed(&self) -> bool {
246        matches!(self, Self::Failed(_))
247    }
248}
249
250/// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/with_status.ml#L6-L10>
251#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
252pub struct WithStatus<T> {
253    pub data: T,
254    pub status: TransactionStatus,
255}
256
257impl<T> WithStatus<T> {
258    pub fn applied(data: T) -> Self {
259        Self {
260            data,
261            status: TransactionStatus::Applied,
262        }
263    }
264
265    pub fn failed(data: T, failures: Vec<Vec<TransactionFailure>>) -> Self {
266        Self {
267            data,
268            status: TransactionStatus::Failed(failures),
269        }
270    }
271
272    pub fn map<F, R>(&self, fun: F) -> WithStatus<R>
273    where
274        F: Fn(&T) -> R,
275    {
276        WithStatus {
277            data: fun(&self.data),
278            status: self.status.clone(),
279        }
280    }
281
282    pub fn into_map<F, R>(self, fun: F) -> WithStatus<R>
283    where
284        F: Fn(T) -> R,
285    {
286        WithStatus {
287            data: fun(self.data),
288            status: self.status,
289        }
290    }
291}
292
293pub trait GenericCommand {
294    fn fee(&self) -> Fee;
295
296    fn forget(&self) -> UserCommand;
297}
298
299pub trait GenericTransaction: Sized {
300    fn is_fee_transfer(&self) -> bool;
301
302    fn is_coinbase(&self) -> bool;
303
304    fn is_command(&self) -> bool;
305}
306
307impl<T> GenericCommand for WithStatus<T>
308where
309    T: GenericCommand,
310{
311    fn fee(&self) -> Fee {
312        self.data.fee()
313    }
314
315    fn forget(&self) -> UserCommand {
316        self.data.forget()
317    }
318}
319
320/// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/fee_transfer.ml#L76-L80>
321#[derive(Debug, Clone, PartialEq)]
322pub struct SingleFeeTransfer {
323    pub receiver_pk: CompressedPubKey,
324    pub fee: Fee,
325    pub fee_token: TokenId,
326}
327
328impl SingleFeeTransfer {
329    pub fn receiver(&self) -> AccountId {
330        AccountId {
331            public_key: self.receiver_pk.clone(),
332            token_id: self.fee_token.clone(),
333        }
334    }
335
336    pub fn create(receiver_pk: CompressedPubKey, fee: Fee, fee_token: TokenId) -> Self {
337        Self {
338            receiver_pk,
339            fee,
340            fee_token,
341        }
342    }
343}
344
345/// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/fee_transfer.ml#L68-L69>
346#[derive(Debug, Clone, PartialEq)]
347pub struct FeeTransfer(pub(super) OneOrTwo<SingleFeeTransfer>);
348
349impl std::ops::Deref for FeeTransfer {
350    type Target = OneOrTwo<SingleFeeTransfer>;
351
352    fn deref(&self) -> &Self::Target {
353        &self.0
354    }
355}
356
357impl FeeTransfer {
358    pub fn fee_tokens(&self) -> impl Iterator<Item = &TokenId> {
359        self.0.iter().map(|fee_transfer| &fee_transfer.fee_token)
360    }
361
362    pub fn receiver_pks(&self) -> impl Iterator<Item = &CompressedPubKey> {
363        self.0.iter().map(|fee_transfer| &fee_transfer.receiver_pk)
364    }
365
366    pub fn receivers(&self) -> impl Iterator<Item = AccountId> + '_ {
367        self.0.iter().map(|fee_transfer| AccountId {
368            public_key: fee_transfer.receiver_pk.clone(),
369            token_id: fee_transfer.fee_token.clone(),
370        })
371    }
372
373    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/fee_transfer.ml#L110-L114>
374    pub fn fee_excess(&self) -> Result<FeeExcess, String> {
375        let one_or_two = self.0.map(|SingleFeeTransfer { fee, fee_token, .. }| {
376            (fee_token.clone(), Signed::<Fee>::of_unsigned(*fee).negate())
377        });
378        FeeExcess::of_one_or_two(one_or_two)
379    }
380
381    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/fee_transfer.ml#L85-L97>
382    pub fn of_singles(singles: OneOrTwo<SingleFeeTransfer>) -> Result<Self, String> {
383        match singles {
384            OneOrTwo::One(a) => Ok(Self(OneOrTwo::One(a))),
385            OneOrTwo::Two((one, two)) => {
386                if one.fee_token == two.fee_token {
387                    Ok(Self(OneOrTwo::Two((one, two))))
388                } else {
389                    // Necessary invariant for the transaction snark: we should never have
390                    // fee excesses in multiple tokens simultaneously.
391                    Err(format!(
392                        "Cannot combine single fee transfers with incompatible tokens: {:?} <> {:?}",
393                        one, two
394                    ))
395                }
396            }
397        }
398    }
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub struct CoinbaseFeeTransfer {
403    pub receiver_pk: CompressedPubKey,
404    pub fee: Fee,
405}
406
407impl CoinbaseFeeTransfer {
408    pub fn create(receiver_pk: CompressedPubKey, fee: Fee) -> Self {
409        Self { receiver_pk, fee }
410    }
411
412    pub fn receiver(&self) -> AccountId {
413        AccountId {
414            public_key: self.receiver_pk.clone(),
415            token_id: TokenId::default(),
416        }
417    }
418}
419
420/// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/coinbase.ml#L17-L21>
421#[derive(Debug, Clone, PartialEq)]
422pub struct Coinbase {
423    pub receiver: CompressedPubKey,
424    pub amount: Amount,
425    pub fee_transfer: Option<CoinbaseFeeTransfer>,
426}
427
428impl Coinbase {
429    fn is_valid(&self) -> bool {
430        match &self.fee_transfer {
431            None => true,
432            Some(CoinbaseFeeTransfer { fee, .. }) => Amount::of_fee(fee) <= self.amount,
433        }
434    }
435
436    pub fn create(
437        amount: Amount,
438        receiver: CompressedPubKey,
439        fee_transfer: Option<CoinbaseFeeTransfer>,
440    ) -> Result<Coinbase, String> {
441        let mut this = Self {
442            receiver: receiver.clone(),
443            amount,
444            fee_transfer,
445        };
446
447        if this.is_valid() {
448            let adjusted_fee_transfer = this.fee_transfer.as_ref().and_then(|ft| {
449                if receiver != ft.receiver_pk {
450                    Some(ft.clone())
451                } else {
452                    None
453                }
454            });
455            this.fee_transfer = adjusted_fee_transfer;
456            Ok(this)
457        } else {
458            Err("Coinbase.create: invalid coinbase".to_string())
459        }
460    }
461
462    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/coinbase.ml#L92-L100>
463    fn expected_supply_increase(&self) -> Result<Amount, String> {
464        let Self {
465            amount,
466            fee_transfer,
467            ..
468        } = self;
469
470        match fee_transfer {
471            None => Ok(*amount),
472            Some(CoinbaseFeeTransfer { fee, .. }) => amount
473                .checked_sub(&Amount::of_fee(fee))
474                // The substraction result is ignored here
475                .map(|_| *amount)
476                .ok_or_else(|| "Coinbase underflow".to_string()),
477        }
478    }
479
480    pub fn fee_excess(&self) -> Result<FeeExcess, String> {
481        self.expected_supply_increase().map(|_| FeeExcess::empty())
482    }
483
484    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/coinbase.ml#L39-L39>
485    pub fn receiver(&self) -> AccountId {
486        AccountId::new(self.receiver.clone(), TokenId::default())
487    }
488
489    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/coinbase.ml#L51-L65>
490    pub fn account_access_statuses(
491        &self,
492        status: &TransactionStatus,
493    ) -> Vec<(AccountId, zkapp_command::AccessedOrNot)> {
494        let access_status = match status {
495            TransactionStatus::Applied => zkapp_command::AccessedOrNot::Accessed,
496            TransactionStatus::Failed(_) => zkapp_command::AccessedOrNot::NotAccessed,
497        };
498
499        let mut ids = Vec::with_capacity(2);
500
501        if let Some(fee_transfer) = self.fee_transfer.as_ref() {
502            ids.push((fee_transfer.receiver(), access_status.clone()));
503        };
504
505        ids.push((self.receiver(), access_status));
506
507        ids
508    }
509
510    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/coinbase.ml#L67-L69>
511    pub fn accounts_referenced(&self) -> Vec<AccountId> {
512        self.account_access_statuses(&TransactionStatus::Applied)
513            .into_iter()
514            .map(|(id, _status)| id)
515            .collect()
516    }
517}
518
519/// 0th byte is a tag to distinguish digests from other data
520/// 1st byte is length, always 32 for digests
521/// bytes 2 to 33 are data, 0-right-padded if length is less than 32
522///
523#[derive(Clone, PartialEq)]
524pub struct Memo(pub [u8; 34]);
525
526impl std::fmt::Debug for Memo {
527    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        use crate::staged_ledger::hash::OCamlString;
529
530        // Display like OCaml
531        // Example: "\000 \014WQ\192&\229C\178\232\171.\176`\153\218\161\209\229\223Gw\143w\135\250\171E\205\241/\227\168"
532
533        f.write_fmt(format_args!("\"{}\"", self.0.to_ocaml_str()))
534    }
535}
536
537impl std::str::FromStr for Memo {
538    type Err = ();
539
540    fn from_str(s: &str) -> Result<Self, Self::Err> {
541        let length = std::cmp::min(s.len(), Self::DIGEST_LENGTH) as u8;
542        let mut memo: [u8; Self::MEMO_LENGTH] = std::array::from_fn(|i| (i == 0) as u8);
543        memo[Self::TAG_INDEX] = Self::BYTES_TAG;
544        memo[Self::LENGTH_INDEX] = length;
545        let padded = format!("{s:\0<32}");
546        memo[2..].copy_from_slice(
547            &padded.as_bytes()[..std::cmp::min(padded.len(), Self::DIGEST_LENGTH)],
548        );
549        Ok(Memo(memo))
550    }
551}
552
553impl std::fmt::Display for Memo {
554    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555        if self.0[0] != Self::BYTES_TAG {
556            return Err(std::fmt::Error);
557        }
558
559        let length = self.0[1] as usize;
560        let memo_slice = &self.0[2..2 + length];
561        let memo_str = String::from_utf8_lossy(memo_slice).to_string();
562        let trimmed = memo_str.trim_end_matches('\0').to_string();
563
564        write!(f, "{trimmed}")
565    }
566}
567
568impl Memo {
569    const TAG_INDEX: usize = 0;
570    const LENGTH_INDEX: usize = 1;
571
572    const DIGEST_TAG: u8 = 0x00;
573    const BYTES_TAG: u8 = 0x01;
574
575    const DIGEST_LENGTH: usize = 32; // Blake2.digest_size_in_bytes
576    const DIGEST_LENGTH_BYTE: u8 = Self::DIGEST_LENGTH as u8;
577
578    /// +2 for tag and length bytes
579    const MEMO_LENGTH: usize = Self::DIGEST_LENGTH + 2;
580
581    const MAX_INPUT_LENGTH: usize = Self::DIGEST_LENGTH;
582
583    const MAX_DIGESTIBLE_STRING_LENGTH: usize = 1000;
584
585    pub fn to_bits(&self) -> [bool; std::mem::size_of::<Self>() * 8] {
586        use crate::proofs::transaction::legacy_input::BitsIterator;
587
588        const NBYTES: usize = 34;
589        const NBITS: usize = NBYTES * 8;
590        assert_eq!(std::mem::size_of::<Self>(), NBYTES);
591
592        let mut iter = BitsIterator {
593            index: 0,
594            number: self.0,
595        }
596        .take(NBITS);
597        std::array::from_fn(|_| iter.next().unwrap())
598    }
599
600    pub fn hash(&self) -> Fp {
601        use poseidon::hash::{hash_with_kimchi, legacy};
602
603        // For some reason we are mixing legacy inputs and "new" hashing
604        let mut inputs = legacy::Inputs::new();
605        inputs.append_bytes(&self.0);
606        hash_with_kimchi(&MINA_ZKAPP_MEMO, &inputs.to_fields())
607    }
608
609    pub fn as_slice(&self) -> &[u8] {
610        self.0.as_slice()
611    }
612
613    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/signed_command_memo.ml#L156-L156>
614    pub fn dummy() -> Self {
615        // TODO
616        Self([0; 34])
617    }
618
619    pub fn empty() -> Self {
620        let mut array = [0; 34];
621        array[0] = 1;
622        Self(array)
623    }
624
625    /// Example:
626    /// "\000 \014WQ\192&\229C\178\232\171.\176`\153\218\161\209\229\223Gw\143w\135\250\171E\205\241/\227\168"
627    #[cfg(test)]
628    pub fn from_ocaml_str(s: &str) -> Self {
629        use crate::staged_ledger::hash::OCamlString;
630
631        Self(<[u8; 34]>::from_ocaml_str(s))
632    }
633
634    pub fn with_number(number: usize) -> Self {
635        let s = format!("{:034}", number);
636        assert_eq!(s.len(), 34);
637        Self(s.into_bytes().try_into().unwrap())
638    }
639
640    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/signed_command_memo.ml#L117-L120>
641    fn create_by_digesting_string_exn(s: &str) -> Self {
642        if s.len() > Self::MAX_DIGESTIBLE_STRING_LENGTH {
643            panic!("Too_long_digestible_string");
644        }
645
646        let mut memo = [0; 34];
647        memo[Self::TAG_INDEX] = Self::DIGEST_TAG;
648        memo[Self::LENGTH_INDEX] = Self::DIGEST_LENGTH_BYTE;
649
650        use blake2::{
651            digest::{Update, VariableOutput},
652            Blake2bVar,
653        };
654        let mut hasher = Blake2bVar::new(32).expect("Invalid Blake2bVar output size");
655        hasher.update(s.as_bytes());
656        hasher.finalize_variable(&mut memo[2..]).unwrap();
657
658        Self(memo)
659    }
660
661    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/signed_command_memo.ml#L205-L207>
662    pub fn gen() -> Self {
663        use rand::distributions::{Alphanumeric, DistString};
664        let random_string = Alphanumeric.sample_string(&mut rand::thread_rng(), 50);
665
666        Self::create_by_digesting_string_exn(&random_string)
667    }
668}
669
670#[derive(Clone, Debug, PartialEq)]
671pub enum UserCommand {
672    SignedCommand(Box<signed_command::SignedCommand>),
673    ZkAppCommand(Box<zkapp_command::ZkAppCommand>),
674}
675
676impl From<&UserCommand> for MinaBaseUserCommandStableV2 {
677    fn from(user_command: &UserCommand) -> Self {
678        match user_command {
679            UserCommand::SignedCommand(signed_command) => {
680                MinaBaseUserCommandStableV2::SignedCommand((&(*(signed_command.clone()))).into())
681            }
682            UserCommand::ZkAppCommand(zkapp_command) => {
683                MinaBaseUserCommandStableV2::ZkappCommand((&(*(zkapp_command.clone()))).into())
684            }
685        }
686    }
687}
688
689impl TryFrom<&MinaBaseUserCommandStableV2> for UserCommand {
690    type Error = InvalidBigInt;
691
692    fn try_from(user_command: &MinaBaseUserCommandStableV2) -> Result<Self, Self::Error> {
693        match user_command {
694            MinaBaseUserCommandStableV2::SignedCommand(signed_command) => Ok(
695                UserCommand::SignedCommand(Box::new(signed_command.try_into()?)),
696            ),
697            MinaBaseUserCommandStableV2::ZkappCommand(zkapp_command) => Ok(
698                UserCommand::ZkAppCommand(Box::new(zkapp_command.try_into()?)),
699            ),
700        }
701    }
702}
703
704impl binprot::BinProtWrite for UserCommand {
705    fn binprot_write<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
706        let p2p: MinaBaseUserCommandStableV2 = self.into();
707        p2p.binprot_write(w)
708    }
709}
710
711impl binprot::BinProtRead for UserCommand {
712    fn binprot_read<R: std::io::Read + ?Sized>(r: &mut R) -> Result<Self, binprot::Error> {
713        let p2p = MinaBaseUserCommandStableV2::binprot_read(r)?;
714        match UserCommand::try_from(&p2p) {
715            Ok(cmd) => Ok(cmd),
716            Err(e) => Err(binprot::Error::CustomError(Box::new(e))),
717        }
718    }
719}
720
721impl UserCommand {
722    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/user_command.ml#L239>
723    pub fn account_access_statuses(
724        &self,
725        status: &TransactionStatus,
726    ) -> Vec<(AccountId, AccessedOrNot)> {
727        match self {
728            UserCommand::SignedCommand(cmd) => cmd.account_access_statuses(status).to_vec(),
729            UserCommand::ZkAppCommand(cmd) => cmd.account_access_statuses(status),
730        }
731    }
732
733    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/user_command.ml#L306-L307>
734    pub fn accounts_referenced(&self) -> Vec<AccountId> {
735        self.account_access_statuses(&TransactionStatus::Applied)
736            .into_iter()
737            .map(|(id, _status)| id)
738            .collect()
739    }
740
741    pub fn fee_payer(&self) -> AccountId {
742        match self {
743            UserCommand::SignedCommand(cmd) => cmd.fee_payer(),
744            UserCommand::ZkAppCommand(cmd) => cmd.fee_payer(),
745        }
746    }
747
748    pub fn valid_until(&self) -> Slot {
749        match self {
750            UserCommand::SignedCommand(cmd) => cmd.valid_until(),
751            UserCommand::ZkAppCommand(cmd) => {
752                let ZkAppCommand { fee_payer, .. } = &**cmd;
753                fee_payer.body.valid_until.unwrap_or_else(Slot::max)
754            }
755        }
756    }
757
758    pub fn applicable_at_nonce(&self) -> Nonce {
759        match self {
760            UserCommand::SignedCommand(cmd) => cmd.nonce(),
761            UserCommand::ZkAppCommand(cmd) => cmd.applicable_at_nonce(),
762        }
763    }
764
765    pub fn expected_target_nonce(&self) -> Nonce {
766        self.applicable_at_nonce().succ()
767    }
768
769    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/user_command.ml#L283-L287>
770    pub fn fee(&self) -> Fee {
771        match self {
772            UserCommand::SignedCommand(cmd) => cmd.fee(),
773            UserCommand::ZkAppCommand(cmd) => cmd.fee(),
774        }
775    }
776
777    pub fn weight(&self) -> u64 {
778        match self {
779            UserCommand::SignedCommand(cmd) => cmd.weight(),
780            UserCommand::ZkAppCommand(cmd) => cmd.weight(),
781        }
782    }
783
784    /// Fee per weight unit
785    pub fn fee_per_wu(&self) -> FeeRate {
786        FeeRate::make_exn(self.fee(), self.weight())
787    }
788
789    pub fn fee_token(&self) -> TokenId {
790        match self {
791            UserCommand::SignedCommand(cmd) => cmd.fee_token(),
792            UserCommand::ZkAppCommand(cmd) => cmd.fee_token(),
793        }
794    }
795
796    pub fn extract_vks(&self) -> Vec<(AccountId, VerificationKeyWire)> {
797        match self {
798            UserCommand::SignedCommand(_) => vec![],
799            UserCommand::ZkAppCommand(zkapp) => zkapp.extract_vks(),
800        }
801    }
802
803    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/user_command.ml#L388-L401>
804    pub fn to_valid_unsafe(self) -> valid::UserCommand {
805        match self {
806            UserCommand::SignedCommand(cmd) => valid::UserCommand::SignedCommand(cmd),
807            UserCommand::ZkAppCommand(cmd) => {
808                valid::UserCommand::ZkAppCommand(Box::new(zkapp_command::valid::ZkAppCommand {
809                    zkapp_command: *cmd,
810                }))
811            }
812        }
813    }
814
815    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/mina_base/user_command.ml#L220-L226>
816    pub fn to_verifiable<F>(
817        &self,
818        status: &TransactionStatus,
819        find_vk: F,
820    ) -> Result<verifiable::UserCommand, String>
821    where
822        F: Fn(Fp, &AccountId) -> Result<VerificationKeyWire, String>,
823    {
824        use verifiable::UserCommand::{SignedCommand, ZkAppCommand};
825        match self {
826            UserCommand::SignedCommand(cmd) => Ok(SignedCommand(cmd.clone())),
827            UserCommand::ZkAppCommand(zkapp) => Ok(ZkAppCommand(Box::new(
828                zkapp_command::verifiable::create(zkapp, status.is_failed(), find_vk)?,
829            ))),
830        }
831    }
832
833    pub fn load_vks_from_ledger(
834        account_ids: HashSet<AccountId>,
835        ledger: &crate::Mask,
836    ) -> HashMap<AccountId, VerificationKeyWire> {
837        let ids: Vec<_> = account_ids.iter().cloned().collect();
838        let locations: Vec<_> = ledger
839            .location_of_account_batch(&ids)
840            .into_iter()
841            .filter_map(|(_, addr)| addr)
842            .collect();
843        ledger
844            .get_batch(&locations)
845            .into_iter()
846            .filter_map(|(_, account)| {
847                let account = account.unwrap();
848                let zkapp = account.zkapp.as_ref()?;
849                let vk = zkapp.verification_key.clone()?;
850                Some((account.id(), vk))
851            })
852            .collect()
853    }
854
855    pub fn load_vks_from_ledger_accounts(
856        accounts: &BTreeMap<AccountId, Account>,
857    ) -> HashMap<AccountId, VerificationKeyWire> {
858        accounts
859            .iter()
860            .filter_map(|(_, account)| {
861                let zkapp = account.zkapp.as_ref()?;
862                let vk = zkapp.verification_key.clone()?;
863                Some((account.id(), vk))
864            })
865            .collect()
866    }
867
868    pub fn to_all_verifiable<S, F>(
869        ts: Vec<MaybeWithStatus<UserCommand>>,
870        load_vk_cache: F,
871    ) -> Result<Vec<MaybeWithStatus<verifiable::UserCommand>>, String>
872    where
873        S: zkapp_command::ToVerifiableStrategy,
874        F: Fn(HashSet<AccountId>) -> S::Cache,
875    {
876        let accounts_referenced: HashSet<AccountId> = ts
877            .iter()
878            .flat_map(|cmd| match cmd.cmd() {
879                UserCommand::SignedCommand(_) => Vec::new(),
880                UserCommand::ZkAppCommand(cmd) => cmd.accounts_referenced(),
881            })
882            .collect();
883        let mut vk_cache = load_vk_cache(accounts_referenced);
884
885        ts.into_iter()
886            .map(|cmd| {
887                let is_failed = cmd.is_failed();
888                let MaybeWithStatus { cmd, status } = cmd;
889                match cmd {
890                    UserCommand::SignedCommand(c) => Ok(MaybeWithStatus {
891                        cmd: verifiable::UserCommand::SignedCommand(c),
892                        status,
893                    }),
894                    UserCommand::ZkAppCommand(c) => {
895                        let zkapp_verifiable = S::create_all(&c, is_failed, &mut vk_cache)?;
896                        Ok(MaybeWithStatus {
897                            cmd: verifiable::UserCommand::ZkAppCommand(Box::new(zkapp_verifiable)),
898                            status,
899                        })
900                    }
901                }
902            })
903            .collect()
904    }
905
906    fn has_insufficient_fee(&self) -> bool {
907        /// `minimum_user_command_fee`
908        const MINIMUM_USER_COMMAND_FEE: Fee = Fee::from_u64(1000000);
909        self.fee() < MINIMUM_USER_COMMAND_FEE
910    }
911
912    fn has_zero_vesting_period(&self) -> bool {
913        match self {
914            UserCommand::SignedCommand(_cmd) => false,
915            UserCommand::ZkAppCommand(cmd) => cmd.has_zero_vesting_period(),
916        }
917    }
918
919    fn is_incompatible_version(&self) -> bool {
920        match self {
921            UserCommand::SignedCommand(_cmd) => false,
922            UserCommand::ZkAppCommand(cmd) => cmd.is_incompatible_version(),
923        }
924    }
925
926    fn is_disabled(&self) -> bool {
927        match self {
928            UserCommand::SignedCommand(_cmd) => false,
929            UserCommand::ZkAppCommand(_cmd) => false, // Mina_compile_config.zkapps_disabled
930        }
931    }
932
933    fn valid_size(&self) -> Result<(), String> {
934        match self {
935            UserCommand::SignedCommand(_cmd) => Ok(()),
936            UserCommand::ZkAppCommand(cmd) => cmd.valid_size(),
937        }
938    }
939
940    pub fn check_well_formedness(&self) -> Result<(), Vec<WellFormednessError>> {
941        let mut errors: Vec<_> = [
942            (
943                Self::has_insufficient_fee as fn(_) -> _,
944                WellFormednessError::InsufficientFee,
945            ),
946            (
947                Self::has_zero_vesting_period,
948                WellFormednessError::ZeroVestingPeriod,
949            ),
950            (
951                Self::is_incompatible_version,
952                WellFormednessError::IncompatibleVersion,
953            ),
954            (
955                Self::is_disabled,
956                WellFormednessError::TransactionTypeDisabled,
957            ),
958        ]
959        .iter()
960        .filter_map(|(fun, e)| if fun(self) { Some(e.clone()) } else { None })
961        .collect();
962
963        if let Err(e) = self.valid_size() {
964            errors.push(WellFormednessError::ZkappTooBig(e));
965        }
966
967        if errors.is_empty() {
968            Ok(())
969        } else {
970            Err(errors)
971        }
972    }
973}
974
975#[derive(Debug, Clone, Hash, PartialEq, Eq, thiserror::Error)]
976pub enum WellFormednessError {
977    #[error("Insufficient Fee")]
978    InsufficientFee,
979    #[error("Zero vesting period")]
980    ZeroVestingPeriod,
981    #[error("Zkapp too big: {0}")]
982    ZkappTooBig(String),
983    #[error("Transaction type disabled")]
984    TransactionTypeDisabled,
985    #[error("Incompatible version")]
986    IncompatibleVersion,
987}
988
989impl GenericCommand for UserCommand {
990    fn fee(&self) -> Fee {
991        match self {
992            UserCommand::SignedCommand(cmd) => cmd.fee(),
993            UserCommand::ZkAppCommand(cmd) => cmd.fee(),
994        }
995    }
996
997    fn forget(&self) -> UserCommand {
998        self.clone()
999    }
1000}
1001
1002impl GenericTransaction for Transaction {
1003    fn is_fee_transfer(&self) -> bool {
1004        matches!(self, Transaction::FeeTransfer(_))
1005    }
1006    fn is_coinbase(&self) -> bool {
1007        matches!(self, Transaction::Coinbase(_))
1008    }
1009    fn is_command(&self) -> bool {
1010        matches!(self, Transaction::Command(_))
1011    }
1012}
1013
1014/// Top-level transaction type representing all possible transactions in the
1015/// Mina protocol.
1016///
1017/// Transactions in Mina fall into two categories:
1018///
1019/// ## User-initiated transactions
1020///
1021/// - [`Command`](Transaction::Command): User-initiated transactions that can be
1022///   either signed commands (payments and stake delegations) or zkApp commands
1023///   (complex multi-account zero-knowledge operations). These transactions are
1024///   submitted by users, require signatures, and pay fees to block producers.
1025///
1026/// ## Protocol transactions
1027///
1028/// - [`FeeTransfer`](Transaction::FeeTransfer): System-generated transaction
1029///   that distributes collected transaction fees to block producers. Created
1030///   automatically during block production and does not require user signatures.
1031/// - [`Coinbase`](Transaction::Coinbase): System-generated transaction that
1032///   rewards block producers for successfully producing a block. May include an
1033///   optional fee transfer component to split rewards.
1034///
1035/// # Transaction processing
1036///
1037/// All transactions are processed through the two-phase application model
1038/// ([`apply_transaction_first_pass`] and [`apply_transaction_second_pass`]) to
1039/// enable efficient proof generation. Protocol transactions (fee transfers and
1040/// coinbase) complete entirely in the first pass, while user commands may
1041/// require both passes.
1042///
1043/// # Serialization
1044///
1045/// The type uses [`derive_more::From`] for automatic conversion from variant
1046/// types and implements conversion to/from the p2p wire format
1047/// [`MinaTransactionTransactionStableV2`].
1048///
1049/// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/transaction/transaction.ml#L8-L11>
1050#[derive(Clone, Debug, derive_more::From)]
1051pub enum Transaction {
1052    /// User-initiated transaction: signed command or zkApp command
1053    Command(UserCommand),
1054    /// System-generated fee distribution to block producers
1055    FeeTransfer(FeeTransfer),
1056    /// System-generated block reward for block producer
1057    Coinbase(Coinbase),
1058}
1059
1060impl Transaction {
1061    pub fn is_zkapp(&self) -> bool {
1062        matches!(self, Self::Command(UserCommand::ZkAppCommand(_)))
1063    }
1064
1065    pub fn fee_excess(&self) -> Result<FeeExcess, String> {
1066        use Transaction::*;
1067        use UserCommand::*;
1068
1069        match self {
1070            Command(SignedCommand(cmd)) => Ok(cmd.fee_excess()),
1071            Command(ZkAppCommand(cmd)) => Ok(cmd.fee_excess()),
1072            FeeTransfer(ft) => ft.fee_excess(),
1073            Coinbase(cb) => cb.fee_excess(),
1074        }
1075    }
1076
1077    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/transaction/transaction.ml#L98-L110>
1078    pub fn public_keys(&self) -> Vec<CompressedPubKey> {
1079        use Transaction::*;
1080        use UserCommand::*;
1081
1082        let to_pks = |ids: Vec<AccountId>| ids.into_iter().map(|id| id.public_key).collect();
1083
1084        match self {
1085            Command(SignedCommand(cmd)) => to_pks(cmd.accounts_referenced()),
1086            Command(ZkAppCommand(cmd)) => to_pks(cmd.accounts_referenced()),
1087            FeeTransfer(ft) => ft.receiver_pks().cloned().collect(),
1088            Coinbase(cb) => to_pks(cb.accounts_referenced()),
1089        }
1090    }
1091
1092    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/transaction/transaction.ml#L112-L124>
1093    pub fn account_access_statuses(
1094        &self,
1095        status: &TransactionStatus,
1096    ) -> Vec<(AccountId, zkapp_command::AccessedOrNot)> {
1097        use Transaction::*;
1098        use UserCommand::*;
1099
1100        match self {
1101            Command(SignedCommand(cmd)) => cmd.account_access_statuses(status).to_vec(),
1102            Command(ZkAppCommand(cmd)) => cmd.account_access_statuses(status),
1103            FeeTransfer(ft) => ft
1104                .receivers()
1105                .map(|account_id| (account_id, AccessedOrNot::Accessed))
1106                .collect(),
1107            Coinbase(cb) => cb.account_access_statuses(status),
1108        }
1109    }
1110
1111    /// OCaml: <https://github.com/MinaProtocol/mina/blob/5da42ccd72e791f164d4d200cf1ce300262873b3/src/lib/transaction/transaction.ml#L126-L128>
1112    pub fn accounts_referenced(&self) -> Vec<AccountId> {
1113        self.account_access_statuses(&TransactionStatus::Applied)
1114            .into_iter()
1115            .map(|(id, _status)| id)
1116            .collect()
1117    }
1118}
1119
1120impl From<&Transaction> for MinaTransactionTransactionStableV2 {
1121    fn from(value: &Transaction) -> Self {
1122        match value {
1123            Transaction::Command(v) => Self::Command(Box::new(v.into())),
1124            Transaction::FeeTransfer(v) => Self::FeeTransfer(v.into()),
1125            Transaction::Coinbase(v) => Self::Coinbase(v.into()),
1126        }
1127    }
1128}
1129
1130#[cfg(any(test, feature = "fuzzing"))]
1131pub mod for_tests;