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