1use 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#[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#[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#[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#[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#[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 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 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 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#[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 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 .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 pub fn receiver(&self) -> AccountId {
506 AccountId::new(self.receiver.clone(), TokenId::default())
507 }
508
509 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 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#[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 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; const DIGEST_LENGTH_BYTE: u8 = Self::DIGEST_LENGTH as u8;
601
602 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 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 pub fn dummy() -> Self {
641 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 #[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 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 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 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 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 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 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 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 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 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, }
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#[derive(Clone, Debug, derive_more::From)]
1093pub enum Transaction {
1094 Command(UserCommand),
1096 FeeTransfer(FeeTransfer),
1098 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 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 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 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;