1use super::{
66 signed_command::{
67 self, PaymentPayload, SignedCommand, SignedCommandPayload, StakeDelegationPayload,
68 },
69 transaction_partially_applied::set_with_location,
70 Coinbase, CoinbaseFeeTransfer, Memo, SingleFeeTransfer, Transaction, TransactionFailure,
71 UserCommand,
72};
73use crate::{
74 decompress_pk,
75 proofs::{field::Boolean, witness::Witness},
76 scan_state::{
77 currency::{Amount, Balance, Fee, Index, Magnitude, Nonce, Slot},
78 scan_state::transaction_snark::OneOrTwo,
79 },
80 sparse_ledger::LedgerIntf,
81 zkapps::zkapp_logic::ZkAppCommandElt,
82 Account, AccountId, AppendToInputs, ReceiptChainHash, Timing, TokenId,
83};
84use ark_ff::PrimeField;
85use mina_curves::pasta::Fp;
86use mina_hasher::{Hashable, ROInput as LegacyInput};
87use mina_signer::{CompressedPubKey, NetworkId, PubKey, Signature};
88use poseidon::hash::{hash_with_kimchi, params::CODA_RECEIPT_UC, Inputs};
89
90#[derive(Clone)]
91pub struct Common {
92 pub fee: Fee,
93 pub fee_token: TokenId,
94 pub fee_payer_pk: CompressedPubKey,
95 pub nonce: Nonce,
96 pub valid_until: Slot,
97 pub memo: Memo,
98}
99
100#[derive(Clone, Debug)]
101pub enum Tag {
102 Payment = 0,
103 StakeDelegation = 1,
104 FeeTransfer = 2,
105 Coinbase = 3,
106}
107
108impl Tag {
109 pub fn is_user_command(&self) -> Boolean {
110 match self {
111 Tag::Payment | Tag::StakeDelegation => Boolean::True,
112 Tag::FeeTransfer | Tag::Coinbase => Boolean::False,
113 }
114 }
115
116 pub fn is_payment(&self) -> Boolean {
117 match self {
118 Tag::Payment => Boolean::True,
119 Tag::FeeTransfer | Tag::Coinbase | Tag::StakeDelegation => Boolean::False,
120 }
121 }
122
123 pub fn is_stake_delegation(&self) -> Boolean {
124 match self {
125 Tag::StakeDelegation => Boolean::True,
126 Tag::FeeTransfer | Tag::Coinbase | Tag::Payment => Boolean::False,
127 }
128 }
129
130 pub fn is_fee_transfer(&self) -> Boolean {
131 match self {
132 Tag::FeeTransfer => Boolean::True,
133 Tag::StakeDelegation | Tag::Coinbase | Tag::Payment => Boolean::False,
134 }
135 }
136
137 pub fn is_coinbase(&self) -> Boolean {
138 match self {
139 Tag::Coinbase => Boolean::True,
140 Tag::StakeDelegation | Tag::FeeTransfer | Tag::Payment => Boolean::False,
141 }
142 }
143
144 pub fn to_bits(&self) -> [bool; 3] {
145 let tag = self.clone() as u8;
146 let mut bits = [false; 3];
147 for (index, bit) in [4, 2, 1].iter().enumerate() {
148 bits[index] = tag & bit != 0;
149 }
150 bits
151 }
152
153 pub fn to_untagged_bits(&self) -> [bool; 5] {
154 let mut is_payment = false;
155 let mut is_stake_delegation = false;
156 let mut is_fee_transfer = false;
157 let mut is_coinbase = false;
158 let mut is_user_command = false;
159
160 match self {
161 Tag::Payment => {
162 is_payment = true;
163 is_user_command = true;
164 }
165 Tag::StakeDelegation => {
166 is_stake_delegation = true;
167 is_user_command = true;
168 }
169 Tag::FeeTransfer => is_fee_transfer = true,
170 Tag::Coinbase => is_coinbase = true,
171 }
172
173 [
174 is_payment,
175 is_stake_delegation,
176 is_fee_transfer,
177 is_coinbase,
178 is_user_command,
179 ]
180 }
181}
182
183#[derive(Clone)]
184pub struct Body {
185 pub tag: Tag,
186 pub source_pk: CompressedPubKey,
187 pub receiver_pk: CompressedPubKey,
188 pub token_id: TokenId,
189 pub amount: Amount,
190}
191
192#[derive(Clone)]
193pub struct TransactionUnionPayload {
194 pub common: Common,
195 pub body: Body,
196}
197
198impl Hashable for TransactionUnionPayload {
199 type D = NetworkId;
200
201 fn to_roinput(&self) -> LegacyInput {
202 let fee_token_id = self.common.fee_token.0.into_bigint().0[0];
212 let token_id = self.body.token_id.0.into_bigint().0[0];
213
214 let mut roi = LegacyInput::new()
215 .append_field(self.common.fee_payer_pk.x)
216 .append_field(self.body.source_pk.x)
217 .append_field(self.body.receiver_pk.x)
218 .append_u64(self.common.fee.as_u64())
219 .append_u64(fee_token_id)
220 .append_bool(self.common.fee_payer_pk.is_odd)
221 .append_u32(self.common.nonce.as_u32())
222 .append_u32(self.common.valid_until.as_u32())
223 .append_bytes(&self.common.memo.0);
224
225 let tag = self.body.tag.clone() as u8;
226 for bit in [4, 2, 1] {
227 roi = roi.append_bool(tag & bit != 0);
228 }
229
230 roi.append_bool(self.body.source_pk.is_odd)
231 .append_bool(self.body.receiver_pk.is_odd)
232 .append_u64(token_id)
233 .append_u64(self.body.amount.as_u64())
234 .append_bool(false) }
236
237 fn domain_string(network_id: NetworkId) -> Option<String> {
239 match network_id {
241 NetworkId::MAINNET => mina_core::network::mainnet::SIGNATURE_PREFIX,
242 NetworkId::TESTNET => mina_core::network::devnet::SIGNATURE_PREFIX,
243 }
244 .to_string()
245 .into()
246 }
247}
248
249impl TransactionUnionPayload {
250 pub fn of_user_command_payload(payload: &SignedCommandPayload) -> Self {
251 use signed_command::Body::{Payment, StakeDelegation};
252
253 Self {
254 common: Common {
255 fee: payload.common.fee,
256 fee_token: TokenId::default(),
257 fee_payer_pk: payload.common.fee_payer_pk.clone(),
258 nonce: payload.common.nonce,
259 valid_until: payload.common.valid_until,
260 memo: payload.common.memo.clone(),
261 },
262 body: match &payload.body {
263 Payment(PaymentPayload {
264 receiver_pk,
265 amount,
266 }) => Body {
267 tag: Tag::Payment,
268 source_pk: payload.common.fee_payer_pk.clone(),
269 receiver_pk: receiver_pk.clone(),
270 token_id: TokenId::default(),
271 amount: *amount,
272 },
273 StakeDelegation(StakeDelegationPayload::SetDelegate { new_delegate }) => Body {
274 tag: Tag::StakeDelegation,
275 source_pk: payload.common.fee_payer_pk.clone(),
276 receiver_pk: new_delegate.clone(),
277 token_id: TokenId::default(),
278 amount: Amount::zero(),
279 },
280 },
281 }
282 }
283
284 pub fn to_input_legacy(&self) -> ::poseidon::hash::legacy::Inputs<Fp> {
286 let mut roi = ::poseidon::hash::legacy::Inputs::new();
287
288 {
290 roi.append_u64(self.common.fee.0);
291
292 roi.append_bool(true);
295 for _ in 0..63 {
296 roi.append_bool(false);
297 }
298
299 roi.append_field(self.common.fee_payer_pk.x);
301 roi.append_bool(self.common.fee_payer_pk.is_odd);
302
303 roi.append_u32(self.common.nonce.0);
305
306 roi.append_u32(self.common.valid_until.0);
308
309 roi.append_bytes(&self.common.memo.0);
311 }
312
313 {
315 let tag = self.body.tag.clone() as u8;
317 for bit in [4, 2, 1] {
318 roi.append_bool(tag & bit != 0);
319 }
320
321 roi.append_field(self.body.source_pk.x);
323 roi.append_bool(self.body.source_pk.is_odd);
324
325 roi.append_field(self.body.receiver_pk.x);
327 roi.append_bool(self.body.receiver_pk.is_odd);
328
329 roi.append_u64(1);
331
332 roi.append_u64(self.body.amount.0);
334
335 roi.append_bool(false);
337 }
338
339 roi
340 }
341}
342
343pub struct TransactionUnion {
344 pub payload: TransactionUnionPayload,
345 pub signer: PubKey,
346 pub signature: Signature,
347}
348
349impl TransactionUnion {
350 pub fn of_transaction(tx: &Transaction) -> Self {
362 match tx {
363 Transaction::Command(cmd) => {
364 let UserCommand::SignedCommand(cmd) = cmd else {
365 unreachable!();
366 };
367
368 let SignedCommand {
369 payload,
370 signer,
371 signature,
372 } = cmd.as_ref();
373
374 TransactionUnion {
375 payload: TransactionUnionPayload::of_user_command_payload(payload),
376 signer: decompress_pk(signer).unwrap(),
377 signature: signature.clone(),
378 }
379 }
380 Transaction::Coinbase(Coinbase {
381 receiver,
382 amount,
383 fee_transfer,
384 }) => {
385 let CoinbaseFeeTransfer {
386 receiver_pk: other_pk,
387 fee: other_amount,
388 } = fee_transfer
389 .clone()
390 .unwrap_or_else(|| CoinbaseFeeTransfer::create(receiver.clone(), Fee::zero()));
391
392 let signer = decompress_pk(&other_pk).unwrap();
393 let payload = TransactionUnionPayload {
394 common: Common {
395 fee: other_amount,
396 fee_token: TokenId::default(),
397 fee_payer_pk: other_pk.clone(),
398 nonce: Nonce::zero(),
399 valid_until: Slot::max(),
400 memo: Memo::empty(),
401 },
402 body: Body {
403 source_pk: other_pk,
404 receiver_pk: receiver.clone(),
405 token_id: TokenId::default(),
406 amount: *amount,
407 tag: Tag::Coinbase,
408 },
409 };
410
411 TransactionUnion {
412 payload,
413 signer,
414 signature: Signature::dummy(),
415 }
416 }
417 Transaction::FeeTransfer(tr) => {
418 let two = |SingleFeeTransfer {
419 receiver_pk: pk1,
420 fee: fee1,
421 fee_token,
422 },
423 SingleFeeTransfer {
424 receiver_pk: pk2,
425 fee: fee2,
426 fee_token: token_id,
427 }| {
428 let signer = decompress_pk(&pk2).unwrap();
429 let payload = TransactionUnionPayload {
430 common: Common {
431 fee: fee2,
432 fee_token,
433 fee_payer_pk: pk2.clone(),
434 nonce: Nonce::zero(),
435 valid_until: Slot::max(),
436 memo: Memo::empty(),
437 },
438 body: Body {
439 source_pk: pk2,
440 receiver_pk: pk1,
441 token_id,
442 amount: Amount::of_fee(&fee1),
443 tag: Tag::FeeTransfer,
444 },
445 };
446
447 TransactionUnion {
448 payload,
449 signer,
450 signature: Signature::dummy(),
451 }
452 };
453
454 match tr.0.clone() {
455 OneOrTwo::One(t) => {
456 let other = SingleFeeTransfer::create(
457 t.receiver_pk.clone(),
458 Fee::zero(),
459 t.fee_token.clone(),
460 );
461 two(t, other)
462 }
463 OneOrTwo::Two((t1, t2)) => two(t1, t2),
464 }
465 }
466 }
467 }
468}
469
470pub fn cons_signed_command_payload(
472 command_payload: &SignedCommandPayload,
473 last_receipt_chain_hash: ReceiptChainHash,
474) -> ReceiptChainHash {
475 use poseidon::hash::legacy;
478
479 let ReceiptChainHash(last_receipt_chain_hash) = last_receipt_chain_hash;
480 let union = TransactionUnionPayload::of_user_command_payload(command_payload);
481
482 let mut inputs = union.to_input_legacy();
483 inputs.append_field(last_receipt_chain_hash);
484 let hash = legacy::hash_with_kimchi(&legacy::params::CODA_RECEIPT_UC, &inputs.to_fields());
485
486 ReceiptChainHash(hash)
487}
488
489pub fn checked_cons_signed_command_payload(
491 payload: &TransactionUnionPayload,
492 last_receipt_chain_hash: ReceiptChainHash,
493 w: &mut Witness<Fp>,
494) -> ReceiptChainHash {
495 use crate::proofs::transaction::{
496 legacy_input::CheckedLegacyInput, transaction_snark::checked_legacy_hash,
497 };
498 use poseidon::hash::legacy;
499
500 let mut inputs = payload.to_checked_legacy_input_owned(w);
501 inputs.append_field(last_receipt_chain_hash.0);
502
503 let receipt_chain_hash = checked_legacy_hash(&legacy::params::CODA_RECEIPT_UC, inputs, w);
504
505 ReceiptChainHash(receipt_chain_hash)
506}
507
508pub fn cons_zkapp_command_commitment(
512 index: Index,
513 e: ZkAppCommandElt,
514 receipt_hash: &ReceiptChainHash,
515) -> ReceiptChainHash {
516 let ZkAppCommandElt::ZkAppCommandCommitment(x) = e;
517
518 let mut inputs = Inputs::new();
519
520 inputs.append(&index);
521 inputs.append_field(x.0);
522 inputs.append(receipt_hash);
523
524 ReceiptChainHash(hash_with_kimchi(&CODA_RECEIPT_UC, &inputs.to_fields()))
525}
526
527pub fn validate_nonces(txn_nonce: Nonce, account_nonce: Nonce) -> Result<(), String> {
528 if account_nonce == txn_nonce {
529 return Ok(());
530 }
531
532 Err(format!(
533 "Nonce in account {:?} different from nonce in transaction {:?}",
534 account_nonce, txn_nonce,
535 ))
536}
537
538pub fn validate_timing(
539 account: &Account,
540 txn_amount: Amount,
541 txn_global_slot: &Slot,
542) -> Result<Timing, String> {
543 let (timing, _) = validate_timing_with_min_balance(account, txn_amount, txn_global_slot)?;
544
545 Ok(timing)
546}
547
548pub fn account_check_timing(
549 txn_global_slot: &Slot,
550 account: &Account,
551) -> (TimingValidation<bool>, Timing) {
552 let (invalid_timing, timing, _) =
553 validate_timing_with_min_balance_impl(account, Amount::from_u64(0), txn_global_slot);
554 (invalid_timing, timing)
556}
557
558fn validate_timing_with_min_balance(
559 account: &Account,
560 txn_amount: Amount,
561 txn_global_slot: &Slot,
562) -> Result<(Timing, MinBalance), String> {
563 use TimingValidation::*;
564
565 let (possibly_error, timing, min_balance) =
566 validate_timing_with_min_balance_impl(account, txn_amount, txn_global_slot);
567
568 match possibly_error {
569 InsufficientBalance(true) => Err(format!(
570 "For timed account, the requested transaction for amount {:?} \
571 at global slot {:?}, the balance {:?} \
572 is insufficient",
573 txn_amount, txn_global_slot, account.balance
574 )),
575 InvalidTiming(true) => Err(format!(
576 "For timed account {}, the requested transaction for amount {:?} \
577 at global slot {:?}, applying the transaction would put the \
578 balance below the calculated minimum balance of {:?}",
579 account.public_key.into_address(),
580 txn_amount,
581 txn_global_slot,
582 min_balance.0
583 )),
584 InsufficientBalance(false) => {
585 panic!("Broken invariant in validate_timing_with_min_balance'")
586 }
587 InvalidTiming(false) => Ok((timing, min_balance)),
588 }
589}
590
591pub fn timing_error_to_user_command_status(
592 timing_result: Result<Timing, String>,
593) -> Result<Timing, TransactionFailure> {
594 match timing_result {
595 Ok(timing) => Ok(timing),
596 Err(err_str) => {
597 if err_str.contains("minimum balance") {
602 return Err(TransactionFailure::SourceMinimumBalanceViolation);
603 }
604
605 if err_str.contains("is insufficient") {
606 return Err(TransactionFailure::SourceInsufficientBalance);
607 }
608
609 panic!("Unexpected timed account validation error")
610 }
611 }
612}
613
614pub enum TimingValidation<B> {
615 InsufficientBalance(B),
616 InvalidTiming(B),
617}
618
619#[derive(Debug)]
620struct MinBalance(Balance);
621
622fn validate_timing_with_min_balance_impl(
623 account: &Account,
624 txn_amount: Amount,
625 txn_global_slot: &Slot,
626) -> (TimingValidation<bool>, Timing, MinBalance) {
627 use crate::Timing::*;
628 use TimingValidation::*;
629
630 match &account.timing {
631 Untimed => {
632 match account.balance.sub_amount(txn_amount) {
634 None => (
635 InsufficientBalance(true),
636 Untimed,
637 MinBalance(Balance::zero()),
638 ),
639 Some(_) => (InvalidTiming(false), Untimed, MinBalance(Balance::zero())),
640 }
641 }
642 Timed {
643 initial_minimum_balance,
644 ..
645 } => {
646 let account_balance = account.balance;
647
648 let (invalid_balance, invalid_timing, curr_min_balance) =
649 match account_balance.sub_amount(txn_amount) {
650 None => {
651 (true, false, *initial_minimum_balance)
656 }
657 Some(proposed_new_balance) => {
658 let curr_min_balance = account.min_balance_at_slot(*txn_global_slot);
659
660 if proposed_new_balance < curr_min_balance {
661 (false, true, curr_min_balance)
662 } else {
663 (false, false, curr_min_balance)
664 }
665 }
666 };
667
668 let possibly_error = if invalid_balance {
670 InsufficientBalance(invalid_balance)
671 } else {
672 InvalidTiming(invalid_timing)
673 };
674
675 if curr_min_balance > Balance::zero() {
676 (
677 possibly_error,
678 account.timing.clone(),
679 MinBalance(curr_min_balance),
680 )
681 } else {
682 (possibly_error, Untimed, MinBalance(Balance::zero()))
683 }
684 }
685 }
686}
687
688pub fn sub_amount(balance: Balance, amount: Amount) -> Result<Balance, String> {
689 balance
690 .sub_amount(amount)
691 .ok_or_else(|| "insufficient funds".to_string())
692}
693
694pub fn add_amount(balance: Balance, amount: Amount) -> Result<Balance, String> {
695 balance
696 .add_amount(amount)
697 .ok_or_else(|| "overflow".to_string())
698}
699
700#[derive(Clone, Debug)]
701pub enum ExistingOrNew<Loc> {
702 Existing(Loc),
703 New,
704}
705
706pub fn get_with_location<L>(
707 ledger: &mut L,
708 account_id: &AccountId,
709) -> Result<(ExistingOrNew<L::Location>, Box<Account>), String>
710where
711 L: LedgerIntf,
712{
713 match ledger.location_of_account(account_id) {
714 Some(location) => match ledger.get(&location) {
715 Some(account) => Ok((ExistingOrNew::Existing(location), account)),
716 None => panic!("Ledger location with no account"),
717 },
718 None => Ok((
719 ExistingOrNew::New,
720 Box::new(Account::create_with(account_id.clone(), Balance::zero())),
721 )),
722 }
723}
724
725pub fn get_account<L>(
726 ledger: &mut L,
727 account_id: AccountId,
728) -> (Box<Account>, ExistingOrNew<L::Location>)
729where
730 L: LedgerIntf,
731{
732 let (loc, account) = get_with_location(ledger, &account_id).unwrap();
733 (account, loc)
734}
735
736pub fn set_account<'a, L>(
737 l: &'a mut L,
738 (a, loc): (Box<Account>, &ExistingOrNew<L::Location>),
739) -> &'a mut L
740where
741 L: LedgerIntf,
742{
743 set_with_location(l, loc, a).unwrap();
744 l
745}