From ee3d3329fe8e38f20681c9285a43a802e1a355c1 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 17:17:28 +0200 Subject: [PATCH 01/16] Ledger/transaction_logic: extract valid module to separate file Convert transaction_logic.rs into a directory with mod.rs and extract the valid module into valid.rs. This is the first step in splitting the large transaction_logic module into smaller, more manageable files. Changes: - Rename transaction_logic.rs to transaction_logic/mod.rs - Extract valid module to transaction_logic/valid.rs - Use explicit imports in valid.rs instead of 'use super::*' --- .../mod.rs} | 93 +----------------- .../src/scan_state/transaction_logic/valid.rs | 97 +++++++++++++++++++ 2 files changed, 98 insertions(+), 92 deletions(-) rename ledger/src/scan_state/{transaction_logic.rs => transaction_logic/mod.rs} (98%) create mode 100644 ledger/src/scan_state/transaction_logic/valid.rs diff --git a/ledger/src/scan_state/transaction_logic.rs b/ledger/src/scan_state/transaction_logic/mod.rs similarity index 98% rename from ledger/src/scan_state/transaction_logic.rs rename to ledger/src/scan_state/transaction_logic/mod.rs index c61f635e1..985afa71e 100644 --- a/ledger/src/scan_state/transaction_logic.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -253,98 +253,7 @@ where } } -pub mod valid { - use super::*; - - #[derive(Clone, Debug, Hash, PartialEq, Eq)] - pub struct VerificationKeyHash(pub Fp); - - pub type SignedCommand = super::signed_command::SignedCommand; - - use serde::{Deserialize, Serialize}; - - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] - #[serde(into = "MinaBaseUserCommandStableV2")] - #[serde(try_from = "MinaBaseUserCommandStableV2")] - pub enum UserCommand { - SignedCommand(Box), - ZkAppCommand(Box), - } - - impl UserCommand { - /// - pub fn forget_check(&self) -> super::UserCommand { - match self { - UserCommand::SignedCommand(cmd) => super::UserCommand::SignedCommand(cmd.clone()), - UserCommand::ZkAppCommand(cmd) => { - super::UserCommand::ZkAppCommand(Box::new(cmd.zkapp_command.clone())) - } - } - } - - pub fn fee_payer(&self) -> AccountId { - match self { - UserCommand::SignedCommand(cmd) => cmd.fee_payer(), - UserCommand::ZkAppCommand(cmd) => cmd.zkapp_command.fee_payer(), - } - } - - pub fn nonce(&self) -> Option { - match self { - UserCommand::SignedCommand(cmd) => Some(cmd.nonce()), - UserCommand::ZkAppCommand(_) => None, - } - } - } - - impl GenericCommand for UserCommand { - fn fee(&self) -> Fee { - match self { - UserCommand::SignedCommand(cmd) => cmd.fee(), - UserCommand::ZkAppCommand(cmd) => cmd.zkapp_command.fee(), - } - } - - fn forget(&self) -> super::UserCommand { - match self { - UserCommand::SignedCommand(cmd) => super::UserCommand::SignedCommand(cmd.clone()), - UserCommand::ZkAppCommand(cmd) => { - super::UserCommand::ZkAppCommand(Box::new(cmd.zkapp_command.clone())) - } - } - } - } - - impl GenericTransaction for Transaction { - fn is_fee_transfer(&self) -> bool { - matches!(self, Transaction::FeeTransfer(_)) - } - fn is_coinbase(&self) -> bool { - matches!(self, Transaction::Coinbase(_)) - } - fn is_command(&self) -> bool { - matches!(self, Transaction::Command(_)) - } - } - - #[derive(Debug, derive_more::From)] - pub enum Transaction { - Command(UserCommand), - FeeTransfer(super::FeeTransfer), - Coinbase(super::Coinbase), - } - - impl Transaction { - /// - pub fn forget(&self) -> super::Transaction { - match self { - Transaction::Command(cmd) => super::Transaction::Command(cmd.forget_check()), - Transaction::FeeTransfer(ft) => super::Transaction::FeeTransfer(ft.clone()), - Transaction::Coinbase(cb) => super::Transaction::Coinbase(cb.clone()), - } - } - } -} +pub mod valid; /// #[derive(Debug, Clone, PartialEq)] diff --git a/ledger/src/scan_state/transaction_logic/valid.rs b/ledger/src/scan_state/transaction_logic/valid.rs new file mode 100644 index 000000000..a1cc84820 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/valid.rs @@ -0,0 +1,97 @@ +use mina_hasher::Fp; +use mina_p2p_messages::v2::MinaBaseUserCommandStableV2; +use serde::{Deserialize, Serialize}; + +use crate::{ + scan_state::currency::{Fee, Nonce}, + AccountId, +}; + +use super::{GenericCommand, GenericTransaction}; + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct VerificationKeyHash(pub Fp); + +pub type SignedCommand = super::signed_command::SignedCommand; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(into = "MinaBaseUserCommandStableV2")] +#[serde(try_from = "MinaBaseUserCommandStableV2")] +pub enum UserCommand { + SignedCommand(Box), + ZkAppCommand(Box), +} + +impl UserCommand { + /// + pub fn forget_check(&self) -> super::UserCommand { + match self { + UserCommand::SignedCommand(cmd) => super::UserCommand::SignedCommand(cmd.clone()), + UserCommand::ZkAppCommand(cmd) => { + super::UserCommand::ZkAppCommand(Box::new(cmd.zkapp_command.clone())) + } + } + } + + pub fn fee_payer(&self) -> AccountId { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee_payer(), + UserCommand::ZkAppCommand(cmd) => cmd.zkapp_command.fee_payer(), + } + } + + pub fn nonce(&self) -> Option { + match self { + UserCommand::SignedCommand(cmd) => Some(cmd.nonce()), + UserCommand::ZkAppCommand(_) => None, + } + } +} + +impl GenericCommand for UserCommand { + fn fee(&self) -> Fee { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee(), + UserCommand::ZkAppCommand(cmd) => cmd.zkapp_command.fee(), + } + } + + fn forget(&self) -> super::UserCommand { + match self { + UserCommand::SignedCommand(cmd) => super::UserCommand::SignedCommand(cmd.clone()), + UserCommand::ZkAppCommand(cmd) => { + super::UserCommand::ZkAppCommand(Box::new(cmd.zkapp_command.clone())) + } + } + } +} + +impl GenericTransaction for Transaction { + fn is_fee_transfer(&self) -> bool { + matches!(self, Transaction::FeeTransfer(_)) + } + fn is_coinbase(&self) -> bool { + matches!(self, Transaction::Coinbase(_)) + } + fn is_command(&self) -> bool { + matches!(self, Transaction::Command(_)) + } +} + +#[derive(Debug, derive_more::From)] +pub enum Transaction { + Command(UserCommand), + FeeTransfer(super::FeeTransfer), + Coinbase(super::Coinbase), +} + +impl Transaction { + /// + pub fn forget(&self) -> super::Transaction { + match self { + Transaction::Command(cmd) => super::Transaction::Command(cmd.forget_check()), + Transaction::FeeTransfer(ft) => super::Transaction::FeeTransfer(ft.clone()), + Transaction::Coinbase(cb) => super::Transaction::Coinbase(cb.clone()), + } + } +} From c8156eff4a0f2be385fe18b824902d41082f86a5 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 17:38:24 +0200 Subject: [PATCH 02/16] Ledger/transaction_logic: extract signed_command module to separate file Extract the signed_command module from transaction_logic/mod.rs into its own file. This module contains signed command types including payment and stake delegation. Changes: - Extract signed_command module to transaction_logic/signed_command.rs - Use explicit imports instead of 'use super::*' - Update mod.rs to reference the new module file --- .../src/scan_state/transaction_logic/mod.rs | 211 +---------------- .../transaction_logic/signed_command.rs | 215 ++++++++++++++++++ 2 files changed, 216 insertions(+), 210 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/signed_command.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 985afa71e..6df9e1a20 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -605,216 +605,7 @@ impl Memo { } } -pub mod signed_command { - use mina_p2p_messages::v2::MinaBaseSignedCommandStableV2; - use mina_signer::Signature; - - use crate::decompress_pk; - - use super::*; - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct Common { - pub fee: Fee, - pub fee_payer_pk: CompressedPubKey, - pub nonce: Nonce, - pub valid_until: Slot, - pub memo: Memo, - } - - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct PaymentPayload { - pub receiver_pk: CompressedPubKey, - pub amount: Amount, - } - - /// - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum StakeDelegationPayload { - SetDelegate { new_delegate: CompressedPubKey }, - } - - impl StakeDelegationPayload { - /// - pub fn receiver(&self) -> AccountId { - let Self::SetDelegate { new_delegate } = self; - AccountId::new(new_delegate.clone(), TokenId::default()) - } - - /// - pub fn receiver_pk(&self) -> &CompressedPubKey { - let Self::SetDelegate { new_delegate } = self; - new_delegate - } - } - - /// - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum Body { - Payment(PaymentPayload), - StakeDelegation(StakeDelegationPayload), - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct SignedCommandPayload { - pub common: Common, - pub body: Body, - } - - impl SignedCommandPayload { - pub fn create( - fee: Fee, - fee_payer_pk: CompressedPubKey, - nonce: Nonce, - valid_until: Option, - memo: Memo, - body: Body, - ) -> Self { - Self { - common: Common { - fee, - fee_payer_pk, - nonce, - valid_until: valid_until.unwrap_or_else(Slot::max), - memo, - }, - body, - } - } - } - - /// - mod weight { - use super::*; - - fn payment(_: &PaymentPayload) -> u64 { - 1 - } - fn stake_delegation(_: &StakeDelegationPayload) -> u64 { - 1 - } - pub fn of_body(body: &Body) -> u64 { - match body { - Body::Payment(p) => payment(p), - Body::StakeDelegation(s) => stake_delegation(s), - } - } - } - - #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] - #[serde(into = "MinaBaseSignedCommandStableV2")] - #[serde(try_from = "MinaBaseSignedCommandStableV2")] - pub struct SignedCommand { - pub payload: SignedCommandPayload, - pub signer: CompressedPubKey, // TODO: This should be a `mina_signer::PubKey` - pub signature: Signature, - } - - impl SignedCommand { - pub fn valid_until(&self) -> Slot { - self.payload.common.valid_until - } - - /// - pub fn fee_payer(&self) -> AccountId { - let public_key = self.payload.common.fee_payer_pk.clone(); - AccountId::new(public_key, TokenId::default()) - } - - /// - pub fn fee_payer_pk(&self) -> &CompressedPubKey { - &self.payload.common.fee_payer_pk - } - - pub fn weight(&self) -> u64 { - let Self { - payload: SignedCommandPayload { common: _, body }, - signer: _, - signature: _, - } = self; - weight::of_body(body) - } - - /// - pub fn fee_token(&self) -> TokenId { - TokenId::default() - } - - pub fn fee(&self) -> Fee { - self.payload.common.fee - } - - /// - pub fn receiver(&self) -> AccountId { - match &self.payload.body { - Body::Payment(payload) => { - AccountId::new(payload.receiver_pk.clone(), TokenId::default()) - } - Body::StakeDelegation(payload) => payload.receiver(), - } - } - - /// - pub fn receiver_pk(&self) -> &CompressedPubKey { - match &self.payload.body { - Body::Payment(payload) => &payload.receiver_pk, - Body::StakeDelegation(payload) => payload.receiver_pk(), - } - } - - pub fn amount(&self) -> Option { - match &self.payload.body { - Body::Payment(payload) => Some(payload.amount), - Body::StakeDelegation(_) => None, - } - } - - pub fn nonce(&self) -> Nonce { - self.payload.common.nonce - } - - pub fn fee_excess(&self) -> FeeExcess { - FeeExcess::of_single((self.fee_token(), Signed::::of_unsigned(self.fee()))) - } - - /// - pub fn account_access_statuses( - &self, - status: &TransactionStatus, - ) -> Vec<(AccountId, AccessedOrNot)> { - use AccessedOrNot::*; - use TransactionStatus::*; - - match status { - Applied => vec![(self.fee_payer(), Accessed), (self.receiver(), Accessed)], - // Note: The fee payer is always accessed, even if the transaction fails - // - Failed(_) => vec![(self.fee_payer(), Accessed), (self.receiver(), NotAccessed)], - } - } - - pub fn accounts_referenced(&self) -> Vec { - self.account_access_statuses(&TransactionStatus::Applied) - .into_iter() - .map(|(id, _status)| id) - .collect() - } - - /// - pub fn public_keys(&self) -> [&CompressedPubKey; 2] { - [self.fee_payer_pk(), self.receiver_pk()] - } - - /// - pub fn check_valid_keys(&self) -> bool { - self.public_keys() - .into_iter() - .all(|pk| decompress_pk(pk).is_some()) - } - } -} +pub mod signed_command; pub mod zkapp_command { use std::sync::Arc; diff --git a/ledger/src/scan_state/transaction_logic/signed_command.rs b/ledger/src/scan_state/transaction_logic/signed_command.rs new file mode 100644 index 000000000..ad38350e9 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/signed_command.rs @@ -0,0 +1,215 @@ +use mina_p2p_messages::v2::MinaBaseSignedCommandStableV2; +use mina_signer::{CompressedPubKey, Signature}; + +use crate::{ + decompress_pk, + scan_state::{ + currency::{Amount, Fee, Nonce, Signed, Slot}, + fee_excess::FeeExcess, + }, + AccountId, TokenId, +}; + +use super::{zkapp_command::AccessedOrNot, Memo, TransactionStatus}; + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct Common { + pub fee: Fee, + pub fee_payer_pk: CompressedPubKey, + pub nonce: Nonce, + pub valid_until: Slot, + pub memo: Memo, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaymentPayload { + pub receiver_pk: CompressedPubKey, + pub amount: Amount, +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum StakeDelegationPayload { + SetDelegate { new_delegate: CompressedPubKey }, +} + +impl StakeDelegationPayload { + /// + pub fn receiver(&self) -> AccountId { + let Self::SetDelegate { new_delegate } = self; + AccountId::new(new_delegate.clone(), TokenId::default()) + } + + /// + pub fn receiver_pk(&self) -> &CompressedPubKey { + let Self::SetDelegate { new_delegate } = self; + new_delegate + } +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Body { + Payment(PaymentPayload), + StakeDelegation(StakeDelegationPayload), +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct SignedCommandPayload { + pub common: Common, + pub body: Body, +} + +impl SignedCommandPayload { + pub fn create( + fee: Fee, + fee_payer_pk: CompressedPubKey, + nonce: Nonce, + valid_until: Option, + memo: Memo, + body: Body, + ) -> Self { + Self { + common: Common { + fee, + fee_payer_pk, + nonce, + valid_until: valid_until.unwrap_or_else(Slot::max), + memo, + }, + body, + } + } +} + +/// +mod weight { + use super::*; + + fn payment(_: &PaymentPayload) -> u64 { + 1 + } + fn stake_delegation(_: &StakeDelegationPayload) -> u64 { + 1 + } + pub fn of_body(body: &Body) -> u64 { + match body { + Body::Payment(p) => payment(p), + Body::StakeDelegation(s) => stake_delegation(s), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(into = "MinaBaseSignedCommandStableV2")] +#[serde(try_from = "MinaBaseSignedCommandStableV2")] +pub struct SignedCommand { + pub payload: SignedCommandPayload, + pub signer: CompressedPubKey, // TODO: This should be a `mina_signer::PubKey` + pub signature: Signature, +} + +impl SignedCommand { + pub fn valid_until(&self) -> Slot { + self.payload.common.valid_until + } + + /// + pub fn fee_payer(&self) -> AccountId { + let public_key = self.payload.common.fee_payer_pk.clone(); + AccountId::new(public_key, TokenId::default()) + } + + /// + pub fn fee_payer_pk(&self) -> &CompressedPubKey { + &self.payload.common.fee_payer_pk + } + + pub fn weight(&self) -> u64 { + let Self { + payload: SignedCommandPayload { common: _, body }, + signer: _, + signature: _, + } = self; + weight::of_body(body) + } + + /// + pub fn fee_token(&self) -> TokenId { + TokenId::default() + } + + pub fn fee(&self) -> Fee { + self.payload.common.fee + } + + /// + pub fn receiver(&self) -> AccountId { + match &self.payload.body { + Body::Payment(payload) => { + AccountId::new(payload.receiver_pk.clone(), TokenId::default()) + } + Body::StakeDelegation(payload) => payload.receiver(), + } + } + + /// + pub fn receiver_pk(&self) -> &CompressedPubKey { + match &self.payload.body { + Body::Payment(payload) => &payload.receiver_pk, + Body::StakeDelegation(payload) => payload.receiver_pk(), + } + } + + pub fn amount(&self) -> Option { + match &self.payload.body { + Body::Payment(payload) => Some(payload.amount), + Body::StakeDelegation(_) => None, + } + } + + pub fn nonce(&self) -> Nonce { + self.payload.common.nonce + } + + pub fn fee_excess(&self) -> FeeExcess { + FeeExcess::of_single((self.fee_token(), Signed::::of_unsigned(self.fee()))) + } + + /// + pub fn account_access_statuses( + &self, + status: &TransactionStatus, + ) -> Vec<(AccountId, AccessedOrNot)> { + use AccessedOrNot::*; + use TransactionStatus::*; + + match status { + Applied => vec![(self.fee_payer(), Accessed), (self.receiver(), Accessed)], + // Note: The fee payer is always accessed, even if the transaction fails + // + Failed(_) => vec![(self.fee_payer(), Accessed), (self.receiver(), NotAccessed)], + } + } + + pub fn accounts_referenced(&self) -> Vec { + self.account_access_statuses(&TransactionStatus::Applied) + .into_iter() + .map(|(id, _status)| id) + .collect() + } + + /// + pub fn public_keys(&self) -> [&CompressedPubKey; 2] { + [self.fee_payer_pk(), self.receiver_pk()] + } + + /// + pub fn check_valid_keys(&self) -> bool { + self.public_keys() + .into_iter() + .all(|pk| decompress_pk(pk).is_some()) + } +} From 2ec919611ad41cd3648f4eafb9cd47c084528837 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 17:53:49 +0200 Subject: [PATCH 03/16] Ledger/transaction_logic: extract zkapp_command module to separate file Extract the zkapp_command module from transaction_logic/mod.rs into its own file. This is a large module (~3300 lines) containing zkApp command types, account updates, events, actions, and verification logic. Changes: - Extract zkapp_command module to transaction_logic/zkapp_command.rs - Use explicit imports instead of 'use super::*' - Add required trait imports (Zero, Magnitude, Itertools, AppendToInputs) - Update mod.rs to reference the new module file --- .../src/scan_state/transaction_logic/mod.rs | 3304 +---------------- .../transaction_logic/zkapp_command.rs | 3300 ++++++++++++++++ 2 files changed, 3303 insertions(+), 3301 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 6df9e1a20..bac589fe2 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -15,7 +15,7 @@ use mina_p2p_messages::{ }; use mina_signer::{CompressedPubKey, NetworkId}; use poseidon::hash::{ - hash_noinputs, hash_with_kimchi, + hash_with_kimchi, params::{CODA_RECEIPT_UC, MINA_ZKAPP_MEMO}, Inputs, }; @@ -47,7 +47,7 @@ use self::{ }; use super::{ - currency::{Amount, Balance, Fee, Index, Length, Magnitude, Nonce, Signed, Slot, SlotSpan}, + currency::{Amount, Balance, Fee, Index, Length, Magnitude, Nonce, Signed, Slot}, fee_excess::FeeExcess, fee_rate::FeeRate, scan_state::transaction_snark::OneOrTwo, @@ -607,3305 +607,7 @@ impl Memo { pub mod signed_command; -pub mod zkapp_command { - use std::sync::Arc; - - use ark_ff::UniformRand; - use mina_p2p_messages::v2::MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA; - use mina_signer::Signature; - use poseidon::hash::params::{ - MINA_ACCOUNT_UPDATE_CONS, MINA_ACCOUNT_UPDATE_NODE, MINA_ZKAPP_EVENT, MINA_ZKAPP_EVENTS, - MINA_ZKAPP_SEQ_EVENTS, NO_INPUT_MINA_ZKAPP_ACTIONS_EMPTY, NO_INPUT_MINA_ZKAPP_EVENTS_EMPTY, - }; - use rand::{seq::SliceRandom, Rng}; - - use crate::{ - dummy, gen_compressed, gen_keypair, - proofs::{ - field::{Boolean, ToBoolean}, - to_field_elements::ToFieldElements, - transaction::Check, - }, - scan_state::{ - currency::{MinMax, Sgn}, - GenesisConstant, GENESIS_CONSTANT, - }, - zkapps::checks::{ZkappCheck, ZkappCheckOps}, - AuthRequired, MutableFp, MyCow, Permissions, SetVerificationKey, ToInputs, TokenSymbol, - VerificationKey, VerificationKeyWire, VotingFor, ZkAppAccount, ZkAppUri, - }; - - use super::{zkapp_statement::TransactionCommitment, *}; - - #[derive(Debug, Clone, PartialEq)] - pub struct Event(pub Vec); - - impl Event { - pub fn empty() -> Self { - Self(Vec::new()) - } - pub fn hash(&self) -> Fp { - hash_with_kimchi(&MINA_ZKAPP_EVENT, &self.0[..]) - } - pub fn len(&self) -> usize { - let Self(list) = self; - list.len() - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct Events(pub Vec); - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct Actions(pub Vec); - - pub fn gen_events() -> Vec { - let mut rng = rand::thread_rng(); - - let n = rng.gen_range(0..=5); - - (0..=n) - .map(|_| { - let n = rng.gen_range(0..=3); - let event = (0..=n).map(|_| Fp::rand(&mut rng)).collect(); - Event(event) - }) - .collect() - } - - use poseidon::hash::LazyParam; - - /// - pub trait MakeEvents { - const DERIVER_NAME: (); // Unused here for now - - fn get_salt_phrase() -> &'static LazyParam; - fn get_hash_prefix() -> &'static LazyParam; - fn events(&self) -> &[Event]; - fn empty_hash() -> Fp; - } - - /// - impl MakeEvents for Events { - const DERIVER_NAME: () = (); - fn get_salt_phrase() -> &'static LazyParam { - &NO_INPUT_MINA_ZKAPP_EVENTS_EMPTY - } - fn get_hash_prefix() -> &'static poseidon::hash::LazyParam { - &MINA_ZKAPP_EVENTS - } - fn events(&self) -> &[Event] { - self.0.as_slice() - } - fn empty_hash() -> Fp { - cache_one!(Fp, events_to_field(&Events::empty())) - } - } - - /// - impl MakeEvents for Actions { - const DERIVER_NAME: () = (); - fn get_salt_phrase() -> &'static LazyParam { - &NO_INPUT_MINA_ZKAPP_ACTIONS_EMPTY - } - fn get_hash_prefix() -> &'static poseidon::hash::LazyParam { - &MINA_ZKAPP_SEQ_EVENTS - } - fn events(&self) -> &[Event] { - self.0.as_slice() - } - fn empty_hash() -> Fp { - cache_one!(Fp, events_to_field(&Actions::empty())) - } - } - - /// - pub fn events_to_field(e: &E) -> Fp - where - E: MakeEvents, - { - let init = hash_noinputs(E::get_salt_phrase()); - - e.events().iter().rfold(init, |accum, elem| { - hash_with_kimchi(E::get_hash_prefix(), &[accum, elem.hash()]) - }) - } - - impl ToInputs for Events { - fn to_inputs(&self, inputs: &mut Inputs) { - inputs.append(&events_to_field(self)); - } - } - - impl ToInputs for Actions { - fn to_inputs(&self, inputs: &mut Inputs) { - inputs.append(&events_to_field(self)); - } - } - - impl ToFieldElements for Events { - fn to_field_elements(&self, fields: &mut Vec) { - events_to_field(self).to_field_elements(fields); - } - } - - impl ToFieldElements for Actions { - fn to_field_elements(&self, fields: &mut Vec) { - events_to_field(self).to_field_elements(fields); - } - } - - /// Note: It's a different one than in the normal `Account` - /// - /// - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct Timing { - pub initial_minimum_balance: Balance, - pub cliff_time: Slot, - pub cliff_amount: Amount, - pub vesting_period: SlotSpan, - pub vesting_increment: Amount, - } - - impl Timing { - /// - fn dummy() -> Self { - Self { - initial_minimum_balance: Balance::zero(), - cliff_time: Slot::zero(), - cliff_amount: Amount::zero(), - vesting_period: SlotSpan::zero(), - vesting_increment: Amount::zero(), - } - } - - /// - /// - pub fn of_account_timing(timing: crate::account::Timing) -> Option { - match timing { - crate::Timing::Untimed => None, - crate::Timing::Timed { - initial_minimum_balance, - cliff_time, - cliff_amount, - vesting_period, - vesting_increment, - } => Some(Self { - initial_minimum_balance, - cliff_time, - cliff_amount, - vesting_period, - vesting_increment, - }), - } - } - - /// - pub fn to_account_timing(self) -> crate::account::Timing { - let Self { - initial_minimum_balance, - cliff_time, - cliff_amount, - vesting_period, - vesting_increment, - } = self; - - crate::account::Timing::Timed { - initial_minimum_balance, - cliff_time, - cliff_amount, - vesting_period, - vesting_increment, - } - } - } - - impl ToFieldElements for Timing { - fn to_field_elements(&self, fields: &mut Vec) { - let Self { - initial_minimum_balance, - cliff_time, - cliff_amount, - vesting_period, - vesting_increment, - } = self; - - initial_minimum_balance.to_field_elements(fields); - cliff_time.to_field_elements(fields); - cliff_amount.to_field_elements(fields); - vesting_period.to_field_elements(fields); - vesting_increment.to_field_elements(fields); - } - } - - impl Check for Timing { - fn check(&self, w: &mut Witness) { - let Self { - initial_minimum_balance, - cliff_time, - cliff_amount, - vesting_period, - vesting_increment, - } = self; - - initial_minimum_balance.check(w); - cliff_time.check(w); - cliff_amount.check(w); - vesting_period.check(w); - vesting_increment.check(w); - } - } - - impl ToInputs for Timing { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let Timing { - initial_minimum_balance, - cliff_time, - cliff_amount, - vesting_period, - vesting_increment, - } = self; - - inputs.append_u64(initial_minimum_balance.as_u64()); - inputs.append_u32(cliff_time.as_u32()); - inputs.append_u64(cliff_amount.as_u64()); - inputs.append_u32(vesting_period.as_u32()); - inputs.append_u64(vesting_increment.as_u64()); - } - } - - impl Events { - pub fn empty() -> Self { - Self(Vec::new()) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn push_event(acc: Fp, event: Event) -> Fp { - hash_with_kimchi(Self::get_hash_prefix(), &[acc, event.hash()]) - } - - pub fn push_events(&self, acc: Fp) -> Fp { - let hash = self - .0 - .iter() - .rfold(hash_noinputs(Self::get_salt_phrase()), |acc, e| { - Self::push_event(acc, e.clone()) - }); - hash_with_kimchi(Self::get_hash_prefix(), &[acc, hash]) - } - } - - impl Actions { - pub fn empty() -> Self { - Self(Vec::new()) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn push_event(acc: Fp, event: Event) -> Fp { - hash_with_kimchi(Self::get_hash_prefix(), &[acc, event.hash()]) - } - - pub fn push_events(&self, acc: Fp) -> Fp { - let hash = self - .0 - .iter() - .rfold(hash_noinputs(Self::get_salt_phrase()), |acc, e| { - Self::push_event(acc, e.clone()) - }); - hash_with_kimchi(Self::get_hash_prefix(), &[acc, hash]) - } - } - - /// - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum SetOrKeep { - Set(T), - Keep, - } - - impl SetOrKeep { - fn map<'a, F, U>(&'a self, fun: F) -> SetOrKeep - where - F: FnOnce(&'a T) -> U, - U: Clone, - { - match self { - SetOrKeep::Set(v) => SetOrKeep::Set(fun(v)), - SetOrKeep::Keep => SetOrKeep::Keep, - } - } - - pub fn into_map(self, fun: F) -> SetOrKeep - where - F: FnOnce(T) -> U, - U: Clone, - { - match self { - SetOrKeep::Set(v) => SetOrKeep::Set(fun(v)), - SetOrKeep::Keep => SetOrKeep::Keep, - } - } - - pub fn set_or_keep(&self, x: T) -> T { - match self { - Self::Set(data) => data.clone(), - Self::Keep => x, - } - } - - pub fn is_keep(&self) -> bool { - match self { - Self::Keep => true, - Self::Set(_) => false, - } - } - - pub fn is_set(&self) -> bool { - !self.is_keep() - } - - pub fn gen(mut fun: F) -> Self - where - F: FnMut() -> T, - { - let mut rng = rand::thread_rng(); - - if rng.gen() { - Self::Set(fun()) - } else { - Self::Keep - } - } - } - - impl ToInputs for (&SetOrKeep, F) - where - T: ToInputs, - T: Clone, - F: Fn() -> T, - { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let (set_or_keep, default_fn) = self; - - match set_or_keep { - SetOrKeep::Set(this) => { - inputs.append_bool(true); - this.to_inputs(inputs); - } - SetOrKeep::Keep => { - inputs.append_bool(false); - let default = default_fn(); - default.to_inputs(inputs); - } - } - } - } - - impl ToFieldElements for (&SetOrKeep, F) - where - T: ToFieldElements, - T: Clone, - F: Fn() -> T, - { - fn to_field_elements(&self, fields: &mut Vec) { - let (set_or_keep, default_fn) = self; - - match set_or_keep { - SetOrKeep::Set(this) => { - Boolean::True.to_field_elements(fields); - this.to_field_elements(fields); - } - SetOrKeep::Keep => { - Boolean::False.to_field_elements(fields); - let default = default_fn(); - default.to_field_elements(fields); - } - } - } - } - - impl Check for (&SetOrKeep, F) - where - T: Check, - T: Clone, - F: Fn() -> T, - { - fn check(&self, w: &mut Witness) { - let (set_or_keep, default_fn) = self; - let value = match set_or_keep { - SetOrKeep::Set(this) => MyCow::Borrow(this), - SetOrKeep::Keep => MyCow::Own(default_fn()), - }; - value.check(w); - } - } - - #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] - pub struct WithHash { - pub data: T, - pub hash: H, - } - - impl Ord for WithHash { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.hash.cmp(&other.hash) - } - } - - impl PartialOrd for WithHash { - fn partial_cmp(&self, other: &Self) -> Option { - self.hash.partial_cmp(&other.hash) - } - } - - impl Eq for WithHash {} - - impl PartialEq for WithHash { - fn eq(&self, other: &Self) -> bool { - self.hash == other.hash - } - } - - impl std::hash::Hash for WithHash { - fn hash(&self, state: &mut H) { - let Self { data: _, hash } = self; - hash.hash(state); - } - } - - impl ToFieldElements for WithHash { - fn to_field_elements(&self, fields: &mut Vec) { - let Self { data: _, hash } = self; - hash.to_field_elements(fields); - } - } - - impl std::ops::Deref for WithHash { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.data - } - } - - impl WithHash { - pub fn of_data(data: T, hash_data: impl Fn(&T) -> Fp) -> Self { - let hash = hash_data(&data); - Self { data, hash } - } - } - - /// - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct Update { - pub app_state: [SetOrKeep; 8], - pub delegate: SetOrKeep, - pub verification_key: SetOrKeep, - pub permissions: SetOrKeep>, - pub zkapp_uri: SetOrKeep, - pub token_symbol: SetOrKeep, - pub timing: SetOrKeep, - pub voting_for: SetOrKeep, - } - - impl ToFieldElements for Update { - fn to_field_elements(&self, fields: &mut Vec) { - let Self { - app_state, - delegate, - verification_key, - permissions, - zkapp_uri, - token_symbol, - timing, - voting_for, - } = self; - - for s in app_state { - (s, Fp::zero).to_field_elements(fields); - } - (delegate, CompressedPubKey::empty).to_field_elements(fields); - (&verification_key.map(|w| w.hash()), Fp::zero).to_field_elements(fields); - (permissions, Permissions::empty).to_field_elements(fields); - (&zkapp_uri.map(Some), || Option::<&ZkAppUri>::None).to_field_elements(fields); - (token_symbol, TokenSymbol::default).to_field_elements(fields); - (timing, Timing::dummy).to_field_elements(fields); - (voting_for, VotingFor::dummy).to_field_elements(fields); - } - } - - impl Update { - /// - pub fn noop() -> Self { - Self { - app_state: std::array::from_fn(|_| SetOrKeep::Keep), - delegate: SetOrKeep::Keep, - verification_key: SetOrKeep::Keep, - permissions: SetOrKeep::Keep, - zkapp_uri: SetOrKeep::Keep, - token_symbol: SetOrKeep::Keep, - timing: SetOrKeep::Keep, - voting_for: SetOrKeep::Keep, - } - } - - /// - pub fn dummy() -> Self { - Self::noop() - } - - /// - pub fn gen( - token_account: Option, - zkapp_account: Option, - vk: Option<&VerificationKeyWire>, - permissions_auth: Option, - ) -> Self { - let mut rng = rand::thread_rng(); - - let token_account = token_account.unwrap_or(false); - let zkapp_account = zkapp_account.unwrap_or(false); - - let app_state: [_; 8] = std::array::from_fn(|_| SetOrKeep::gen(|| Fp::rand(&mut rng))); - - let delegate = if !token_account { - SetOrKeep::gen(|| gen_keypair().public.into_compressed()) - } else { - SetOrKeep::Keep - }; - - let verification_key = if zkapp_account { - SetOrKeep::gen(|| match vk { - None => VerificationKeyWire::dummy(), - Some(vk) => vk.clone(), - }) - } else { - SetOrKeep::Keep - }; - - let permissions = match permissions_auth { - None => SetOrKeep::Keep, - Some(auth_tag) => SetOrKeep::Set(Permissions::gen(auth_tag)), - }; - - let zkapp_uri = SetOrKeep::gen(|| { - ZkAppUri::from( - [ - "https://www.example.com", - "https://www.minaprotocol.com", - "https://www.gurgle.com", - "https://faceplant.com", - ] - .choose(&mut rng) - .unwrap() - .to_string() - .into_bytes(), - ) - }); - - let token_symbol = SetOrKeep::gen(|| { - TokenSymbol::from( - ["MINA", "TOKEN1", "TOKEN2", "TOKEN3", "TOKEN4", "TOKEN5"] - .choose(&mut rng) - .unwrap() - .to_string() - .into_bytes(), - ) - }); - - let voting_for = SetOrKeep::gen(|| VotingFor(Fp::rand(&mut rng))); - - let timing = SetOrKeep::Keep; - - Self { - app_state, - delegate, - verification_key, - permissions, - zkapp_uri, - token_symbol, - timing, - voting_for, - } - } - } - - // TODO: This could be std::ops::Range ? - /// - #[derive(Debug, Clone, PartialEq)] - pub struct ClosedInterval { - pub lower: T, - pub upper: T, - } - - impl ClosedInterval - where - T: MinMax, - { - pub fn min_max() -> Self { - Self { - lower: T::min(), - upper: T::max(), - } - } - } - - impl ToInputs for ClosedInterval - where - T: ToInputs, - { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let ClosedInterval { lower, upper } = self; - - lower.to_inputs(inputs); - upper.to_inputs(inputs); - } - } - - impl ToFieldElements for ClosedInterval - where - T: ToFieldElements, - { - fn to_field_elements(&self, fields: &mut Vec) { - let ClosedInterval { lower, upper } = self; - - lower.to_field_elements(fields); - upper.to_field_elements(fields); - } - } - - impl Check for ClosedInterval - where - T: Check, - { - fn check(&self, w: &mut Witness) { - let ClosedInterval { lower, upper } = self; - lower.check(w); - upper.check(w); - } - } - - impl ClosedInterval - where - T: PartialOrd, - { - pub fn is_constant(&self) -> bool { - self.lower == self.upper - } - - /// - pub fn gen(mut fun: F) -> Self - where - F: FnMut() -> T, - { - let a1 = fun(); - let a2 = fun(); - - if a1 <= a2 { - Self { - lower: a1, - upper: a2, - } - } else { - Self { - lower: a2, - upper: a1, - } - } - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub enum OrIgnore { - Check(T), - Ignore, - } - - impl ToInputs for (&OrIgnore, F) - where - T: ToInputs, - F: Fn() -> T, - { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let (or_ignore, default_fn) = self; - - match or_ignore { - OrIgnore::Check(this) => { - inputs.append_bool(true); - this.to_inputs(inputs); - } - OrIgnore::Ignore => { - inputs.append_bool(false); - let default = default_fn(); - default.to_inputs(inputs); - } - } - } - } - - impl ToFieldElements for (&OrIgnore, F) - where - T: ToFieldElements, - F: Fn() -> T, - { - fn to_field_elements(&self, fields: &mut Vec) { - let (or_ignore, default_fn) = self; - - match or_ignore { - OrIgnore::Check(this) => { - Boolean::True.to_field_elements(fields); - this.to_field_elements(fields); - } - OrIgnore::Ignore => { - Boolean::False.to_field_elements(fields); - let default = default_fn(); - default.to_field_elements(fields); - } - }; - } - } - - impl Check for (&OrIgnore, F) - where - T: Check, - F: Fn() -> T, - { - fn check(&self, w: &mut Witness) { - let (or_ignore, default_fn) = self; - let value = match or_ignore { - OrIgnore::Check(this) => MyCow::Borrow(this), - OrIgnore::Ignore => MyCow::Own(default_fn()), - }; - value.check(w); - } - } - - impl OrIgnore { - /// - pub fn gen(mut fun: F) -> Self - where - F: FnMut() -> T, - { - let mut rng = rand::thread_rng(); - - if rng.gen() { - Self::Check(fun()) - } else { - Self::Ignore - } - } - - pub fn map(&self, fun: F) -> OrIgnore - where - F: Fn(&T) -> V, - { - match self { - OrIgnore::Check(v) => OrIgnore::Check(fun(v)), - OrIgnore::Ignore => OrIgnore::Ignore, - } - } - } - - impl OrIgnore> - where - T: PartialOrd, - { - /// - pub fn is_constant(&self) -> bool { - match self { - OrIgnore::Check(interval) => interval.lower == interval.upper, - OrIgnore::Ignore => false, - } - } - } - - /// - pub type Hash = OrIgnore; - - /// - pub type EqData = OrIgnore; - - /// - pub type Numeric = OrIgnore>; - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct EpochLedger { - pub hash: Hash, - pub total_currency: Numeric, - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct EpochData { - pub(crate) ledger: EpochLedger, - pub seed: Hash, - pub start_checkpoint: Hash, - pub lock_checkpoint: Hash, - pub epoch_length: Numeric, - } - - #[cfg(feature = "fuzzing")] - impl EpochData { - pub fn new( - ledger: EpochLedger, - seed: Hash, - start_checkpoint: Hash, - lock_checkpoint: Hash, - epoch_length: Numeric, - ) -> Self { - EpochData { - ledger, - seed, - start_checkpoint, - lock_checkpoint, - epoch_length, - } - } - - pub fn ledger_mut(&mut self) -> &mut EpochLedger { - &mut self.ledger - } - } - - impl ToInputs for EpochData { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let EpochData { - ledger, - seed, - start_checkpoint, - lock_checkpoint, - epoch_length, - } = self; - - { - let EpochLedger { - hash, - total_currency, - } = ledger; - - inputs.append(&(hash, Fp::zero)); - inputs.append(&(total_currency, ClosedInterval::min_max)); - } - - inputs.append(&(seed, Fp::zero)); - inputs.append(&(start_checkpoint, Fp::zero)); - inputs.append(&(lock_checkpoint, Fp::zero)); - inputs.append(&(epoch_length, ClosedInterval::min_max)); - } - } - - impl ToFieldElements for EpochData { - fn to_field_elements(&self, fields: &mut Vec) { - let EpochData { - ledger, - seed, - start_checkpoint, - lock_checkpoint, - epoch_length, - } = self; - - { - let EpochLedger { - hash, - total_currency, - } = ledger; - - (hash, Fp::zero).to_field_elements(fields); - (total_currency, ClosedInterval::min_max).to_field_elements(fields); - } - - (seed, Fp::zero).to_field_elements(fields); - (start_checkpoint, Fp::zero).to_field_elements(fields); - (lock_checkpoint, Fp::zero).to_field_elements(fields); - (epoch_length, ClosedInterval::min_max).to_field_elements(fields); - } - } - - impl Check for EpochData { - fn check(&self, w: &mut Witness) { - let EpochData { - ledger, - seed, - start_checkpoint, - lock_checkpoint, - epoch_length, - } = self; - - { - let EpochLedger { - hash, - total_currency, - } = ledger; - - (hash, Fp::zero).check(w); - (total_currency, ClosedInterval::min_max).check(w); - } - - (seed, Fp::zero).check(w); - (start_checkpoint, Fp::zero).check(w); - (lock_checkpoint, Fp::zero).check(w); - (epoch_length, ClosedInterval::min_max).check(w); - } - } - - impl EpochData { - pub fn gen() -> Self { - let mut rng = rand::thread_rng(); - - EpochData { - ledger: EpochLedger { - hash: OrIgnore::gen(|| Fp::rand(&mut rng)), - total_currency: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), - }, - seed: OrIgnore::gen(|| Fp::rand(&mut rng)), - start_checkpoint: OrIgnore::gen(|| Fp::rand(&mut rng)), - lock_checkpoint: OrIgnore::gen(|| Fp::rand(&mut rng)), - epoch_length: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), - } - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct ZkAppPreconditions { - pub snarked_ledger_hash: Hash, - pub blockchain_length: Numeric, - pub min_window_density: Numeric, - pub total_currency: Numeric, - pub global_slot_since_genesis: Numeric, - pub staking_epoch_data: EpochData, - pub next_epoch_data: EpochData, - } - - impl ZkAppPreconditions { - pub fn zcheck( - &self, - s: &ProtocolStateView, - w: &mut Witness, - ) -> Boolean { - let Self { - snarked_ledger_hash, - blockchain_length, - min_window_density, - total_currency, - global_slot_since_genesis, - staking_epoch_data, - next_epoch_data, - } = self; - - // NOTE: Here the 2nd element in the tuples is the default value of `OrIgnore` - - let epoch_data = |epoch_data: &EpochData, - view: &protocol_state::EpochData, - w: &mut Witness| { - let EpochData { - ledger: - EpochLedger { - hash, - total_currency, - }, - seed: _, - start_checkpoint, - lock_checkpoint, - epoch_length, - } = epoch_data; - // Reverse to match OCaml order of the list, while still executing `zcheck` - // in correct order - [ - (epoch_length, ClosedInterval::min_max).zcheck::(&view.epoch_length, w), - (lock_checkpoint, Fp::zero).zcheck::(&view.lock_checkpoint, w), - (start_checkpoint, Fp::zero).zcheck::(&view.start_checkpoint, w), - (total_currency, ClosedInterval::min_max) - .zcheck::(&view.ledger.total_currency, w), - (hash, Fp::zero).zcheck::(&view.ledger.hash, w), - ] - }; - - let next_epoch_data = epoch_data(next_epoch_data, &s.next_epoch_data, w); - let staking_epoch_data = epoch_data(staking_epoch_data, &s.staking_epoch_data, w); - - // Reverse to match OCaml order of the list, while still executing `zcheck` - // in correct order - let bools = [ - (global_slot_since_genesis, ClosedInterval::min_max) - .zcheck::(&s.global_slot_since_genesis, w), - (total_currency, ClosedInterval::min_max).zcheck::(&s.total_currency, w), - (min_window_density, ClosedInterval::min_max) - .zcheck::(&s.min_window_density, w), - (blockchain_length, ClosedInterval::min_max).zcheck::(&s.blockchain_length, w), - (snarked_ledger_hash, Fp::zero).zcheck::(&s.snarked_ledger_hash, w), - ] - .into_iter() - .rev() - .chain(staking_epoch_data.into_iter().rev()) - .chain(next_epoch_data.into_iter().rev()); - - Ops::boolean_all(bools, w) - } - - /// - pub fn accept() -> Self { - let epoch_data = || EpochData { - ledger: EpochLedger { - hash: OrIgnore::Ignore, - total_currency: OrIgnore::Ignore, - }, - seed: OrIgnore::Ignore, - start_checkpoint: OrIgnore::Ignore, - lock_checkpoint: OrIgnore::Ignore, - epoch_length: OrIgnore::Ignore, - }; - - Self { - snarked_ledger_hash: OrIgnore::Ignore, - blockchain_length: OrIgnore::Ignore, - min_window_density: OrIgnore::Ignore, - total_currency: OrIgnore::Ignore, - global_slot_since_genesis: OrIgnore::Ignore, - staking_epoch_data: epoch_data(), - next_epoch_data: epoch_data(), - } - } - } - - impl ToInputs for ZkAppPreconditions { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let ZkAppPreconditions { - snarked_ledger_hash, - blockchain_length, - min_window_density, - total_currency, - global_slot_since_genesis, - staking_epoch_data, - next_epoch_data, - } = &self; - - inputs.append(&(snarked_ledger_hash, Fp::zero)); - inputs.append(&(blockchain_length, ClosedInterval::min_max)); - inputs.append(&(min_window_density, ClosedInterval::min_max)); - inputs.append(&(total_currency, ClosedInterval::min_max)); - inputs.append(&(global_slot_since_genesis, ClosedInterval::min_max)); - inputs.append(staking_epoch_data); - inputs.append(next_epoch_data); - } - } - - impl ToFieldElements for ZkAppPreconditions { - fn to_field_elements(&self, fields: &mut Vec) { - let Self { - snarked_ledger_hash, - blockchain_length, - min_window_density, - total_currency, - global_slot_since_genesis, - staking_epoch_data, - next_epoch_data, - } = self; - - (snarked_ledger_hash, Fp::zero).to_field_elements(fields); - (blockchain_length, ClosedInterval::min_max).to_field_elements(fields); - (min_window_density, ClosedInterval::min_max).to_field_elements(fields); - (total_currency, ClosedInterval::min_max).to_field_elements(fields); - (global_slot_since_genesis, ClosedInterval::min_max).to_field_elements(fields); - staking_epoch_data.to_field_elements(fields); - next_epoch_data.to_field_elements(fields); - } - } - - impl Check for ZkAppPreconditions { - fn check(&self, w: &mut Witness) { - let Self { - snarked_ledger_hash, - blockchain_length, - min_window_density, - total_currency, - global_slot_since_genesis, - staking_epoch_data, - next_epoch_data, - } = self; - - (snarked_ledger_hash, Fp::zero).check(w); - (blockchain_length, ClosedInterval::min_max).check(w); - (min_window_density, ClosedInterval::min_max).check(w); - (total_currency, ClosedInterval::min_max).check(w); - (global_slot_since_genesis, ClosedInterval::min_max).check(w); - staking_epoch_data.check(w); - next_epoch_data.check(w); - } - } - - /// - fn invalid_public_key() -> CompressedPubKey { - CompressedPubKey { - x: Fp::zero(), - is_odd: false, - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct Account { - pub balance: Numeric, - pub nonce: Numeric, - pub receipt_chain_hash: Hash, // TODO: Should be type `ReceiptChainHash` - pub delegate: EqData, - pub state: [EqData; 8], - pub action_state: EqData, - pub proved_state: EqData, - pub is_new: EqData, - } - - impl Account { - /// - pub fn accept() -> Self { - Self { - balance: Numeric::Ignore, - nonce: Numeric::Ignore, - receipt_chain_hash: Hash::Ignore, - delegate: EqData::Ignore, - state: std::array::from_fn(|_| EqData::Ignore), - action_state: EqData::Ignore, - proved_state: EqData::Ignore, - is_new: EqData::Ignore, - } - } - } - - impl Account { - fn zchecks( - &self, - account: &crate::Account, - new_account: Boolean, - w: &mut Witness, - ) -> Vec<(TransactionFailure, Boolean)> { - use TransactionFailure::*; - - let Self { - balance, - nonce, - receipt_chain_hash, - delegate, - state, - action_state, - proved_state, - is_new, - } = self; - - let zkapp_account = account.zkapp_or_empty(); - let is_new = is_new.map(ToBoolean::to_boolean); - let proved_state = proved_state.map(ToBoolean::to_boolean); - - // NOTE: Here we need to execute all `zcheck` in the exact same order than OCaml - // so we execute them in reverse order (compared to OCaml): OCaml evaluates from right - // to left. - // We then have to reverse the resulting vector, to match OCaml resulting list. - - // NOTE 2: Here the 2nd element in the tuples is the default value of `OrIgnore` - let mut checks: Vec<(TransactionFailure, _)> = [ - ( - AccountIsNewPreconditionUnsatisfied, - (&is_new, || Boolean::False).zcheck::(&new_account, w), - ), - ( - AccountProvedStatePreconditionUnsatisfied, - (&proved_state, || Boolean::False) - .zcheck::(&zkapp_account.proved_state.to_boolean(), w), - ), - ] - .into_iter() - .chain({ - let bools = state - .iter() - .zip(&zkapp_account.app_state) - .enumerate() - // Reversed to enforce right-to-left order application of `f` like in OCaml - .rev() - .map(|(i, (s, account_s))| { - let b = (s, Fp::zero).zcheck::(account_s, w); - (AccountAppStatePreconditionUnsatisfied(i as u64), b) - }) - .collect::>(); - // Not reversed again because we are constructing these results in - // reverse order to match the OCaml evaluation order. - bools.into_iter() - }) - .chain([ - { - let bools: Vec<_> = zkapp_account - .action_state - .iter() - // Reversed to enforce right-to-left order application of `f` like in OCaml - .rev() - .map(|account_s| { - (action_state, ZkAppAccount::empty_action_state) - .zcheck::(account_s, w) - }) - .collect(); - ( - AccountActionStatePreconditionUnsatisfied, - Ops::boolean_any(bools, w), - ) - }, - ( - AccountDelegatePreconditionUnsatisfied, - (delegate, CompressedPubKey::empty) - .zcheck::(&*account.delegate_or_empty(), w), - ), - ( - AccountReceiptChainHashPreconditionUnsatisfied, - (receipt_chain_hash, Fp::zero).zcheck::(&account.receipt_chain_hash.0, w), - ), - ( - AccountNoncePreconditionUnsatisfied, - (nonce, ClosedInterval::min_max).zcheck::(&account.nonce, w), - ), - ( - AccountBalancePreconditionUnsatisfied, - (balance, ClosedInterval::min_max).zcheck::(&account.balance, w), - ), - ]) - .collect::>(); - - checks.reverse(); - checks - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct AccountPreconditions(pub Account); - - impl ToInputs for AccountPreconditions { - /// - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let Account { - balance, - nonce, - receipt_chain_hash, - delegate, - state, - action_state, - proved_state, - is_new, - } = &self.0; - - inputs.append(&(balance, ClosedInterval::min_max)); - inputs.append(&(nonce, ClosedInterval::min_max)); - inputs.append(&(receipt_chain_hash, Fp::zero)); - inputs.append(&(delegate, CompressedPubKey::empty)); - for s in state.iter() { - inputs.append(&(s, Fp::zero)); - } - // - inputs.append(&(action_state, ZkAppAccount::empty_action_state)); - inputs.append(&(proved_state, || false)); - inputs.append(&(is_new, || false)); - } - } - - impl ToFieldElements for AccountPreconditions { - fn to_field_elements(&self, fields: &mut Vec) { - let Account { - balance, - nonce, - receipt_chain_hash, - delegate, - state, - action_state, - proved_state, - is_new, - } = &self.0; - - (balance, ClosedInterval::min_max).to_field_elements(fields); - (nonce, ClosedInterval::min_max).to_field_elements(fields); - (receipt_chain_hash, Fp::zero).to_field_elements(fields); - (delegate, CompressedPubKey::empty).to_field_elements(fields); - state.iter().for_each(|s| { - (s, Fp::zero).to_field_elements(fields); - }); - (action_state, ZkAppAccount::empty_action_state).to_field_elements(fields); - (proved_state, || false).to_field_elements(fields); - (is_new, || false).to_field_elements(fields); - } - } - - impl Check for AccountPreconditions { - fn check(&self, w: &mut Witness) { - let Account { - balance, - nonce, - receipt_chain_hash, - delegate, - state, - action_state, - proved_state, - is_new, - } = &self.0; - - (balance, ClosedInterval::min_max).check(w); - (nonce, ClosedInterval::min_max).check(w); - (receipt_chain_hash, Fp::zero).check(w); - (delegate, CompressedPubKey::empty).check(w); - state.iter().for_each(|s| { - (s, Fp::zero).check(w); - }); - (action_state, ZkAppAccount::empty_action_state).check(w); - (proved_state, || false).check(w); - (is_new, || false).check(w); - } - } - - impl AccountPreconditions { - pub fn with_nonce(nonce: Nonce) -> Self { - use OrIgnore::{Check, Ignore}; - AccountPreconditions(Account { - balance: Ignore, - nonce: Check(ClosedInterval { - lower: nonce, - upper: nonce, - }), - receipt_chain_hash: Ignore, - delegate: Ignore, - state: std::array::from_fn(|_| EqData::Ignore), - action_state: Ignore, - proved_state: Ignore, - is_new: Ignore, - }) - } - - pub fn nonce(&self) -> Numeric { - self.0.nonce.clone() - } - - /// - pub fn to_full(&self) -> MyCow<'_, Account> { - MyCow::Borrow(&self.0) - } - - pub fn zcheck( - &self, - new_account: Boolean, - account: &crate::Account, - mut check: Fun, - w: &mut Witness, - ) where - Ops: ZkappCheckOps, - Fun: FnMut(TransactionFailure, Boolean, &mut Witness), - { - let this = self.to_full(); - for (failure, passed) in this.zchecks::(account, new_account, w) { - check(failure, passed, w); - } - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct Preconditions { - pub network: ZkAppPreconditions, - pub account: AccountPreconditions, - pub valid_while: Numeric, - } - - #[cfg(feature = "fuzzing")] - impl Preconditions { - pub fn new( - network: ZkAppPreconditions, - account: AccountPreconditions, - valid_while: Numeric, - ) -> Self { - Self { - network, - account, - valid_while, - } - } - - pub fn network_mut(&mut self) -> &mut ZkAppPreconditions { - &mut self.network - } - } - - impl ToFieldElements for Preconditions { - fn to_field_elements(&self, fields: &mut Vec) { - let Self { - network, - account, - valid_while, - } = self; - - network.to_field_elements(fields); - account.to_field_elements(fields); - (valid_while, ClosedInterval::min_max).to_field_elements(fields); - } - } - - impl Check for Preconditions { - fn check(&self, w: &mut Witness) { - let Self { - network, - account, - valid_while, - } = self; - - network.check(w); - account.check(w); - (valid_while, ClosedInterval::min_max).check(w); - } - } - - impl ToInputs for Preconditions { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let Self { - network, - account, - valid_while, - } = self; - - inputs.append(network); - inputs.append(account); - inputs.append(&(valid_while, ClosedInterval::min_max)); - } - } - - /// - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum AuthorizationKind { - NoneGiven, - Signature, - Proof(Fp), // hash - } - - impl AuthorizationKind { - pub fn vk_hash(&self) -> Fp { - match self { - AuthorizationKind::NoneGiven | AuthorizationKind::Signature => { - VerificationKey::dummy().hash() - } - AuthorizationKind::Proof(hash) => *hash, - } - } - - pub fn is_proved(&self) -> bool { - match self { - AuthorizationKind::Proof(_) => true, - AuthorizationKind::NoneGiven => false, - AuthorizationKind::Signature => false, - } - } - - pub fn is_signed(&self) -> bool { - match self { - AuthorizationKind::Proof(_) => false, - AuthorizationKind::NoneGiven => false, - AuthorizationKind::Signature => true, - } - } - - fn to_structured(&self) -> ([bool; 2], Fp) { - // bits: [is_signed, is_proved] - let bits = match self { - AuthorizationKind::NoneGiven => [false, false], - AuthorizationKind::Signature => [true, false], - AuthorizationKind::Proof(_) => [false, true], - }; - let field = self.vk_hash(); - (bits, field) - } - } - - impl ToInputs for AuthorizationKind { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let (bits, field) = self.to_structured(); - - for bit in bits { - inputs.append_bool(bit); - } - inputs.append_field(field); - } - } - - impl ToFieldElements for AuthorizationKind { - fn to_field_elements(&self, fields: &mut Vec) { - self.to_structured().to_field_elements(fields); - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct Body { - pub public_key: CompressedPubKey, - pub token_id: TokenId, - pub update: Update, - pub balance_change: Signed, - pub increment_nonce: bool, - pub events: Events, - pub actions: Actions, - pub call_data: Fp, - pub preconditions: Preconditions, - pub use_full_commitment: bool, - pub implicit_account_creation_fee: bool, - pub may_use_token: MayUseToken, - pub authorization_kind: AuthorizationKind, - } - - impl ToInputs for Body { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let Self { - public_key, - token_id, - update, - balance_change, - increment_nonce, - events, - actions, - call_data, - preconditions, - use_full_commitment, - implicit_account_creation_fee, - may_use_token, - authorization_kind, - } = self; - - inputs.append(public_key); - inputs.append(token_id); - - // `Body::update` - { - let Update { - app_state, - delegate, - verification_key, - permissions, - zkapp_uri, - token_symbol, - timing, - voting_for, - } = update; - - for state in app_state { - inputs.append(&(state, Fp::zero)); - } - - inputs.append(&(delegate, CompressedPubKey::empty)); - inputs.append(&(&verification_key.map(|w| w.hash()), Fp::zero)); - inputs.append(&(permissions, Permissions::empty)); - inputs.append(&(&zkapp_uri.map(Some), || Option::<&ZkAppUri>::None)); - inputs.append(&(token_symbol, TokenSymbol::default)); - inputs.append(&(timing, Timing::dummy)); - inputs.append(&(voting_for, VotingFor::dummy)); - } - - inputs.append(balance_change); - inputs.append(increment_nonce); - inputs.append(events); - inputs.append(actions); - inputs.append(call_data); - inputs.append(preconditions); - inputs.append(use_full_commitment); - inputs.append(implicit_account_creation_fee); - inputs.append(may_use_token); - inputs.append(authorization_kind); - } - } - - impl ToFieldElements for Body { - fn to_field_elements(&self, fields: &mut Vec) { - let Self { - public_key, - token_id, - update, - balance_change, - increment_nonce, - events, - actions, - call_data, - preconditions, - use_full_commitment, - implicit_account_creation_fee, - may_use_token, - authorization_kind, - } = self; - - public_key.to_field_elements(fields); - token_id.to_field_elements(fields); - update.to_field_elements(fields); - balance_change.to_field_elements(fields); - increment_nonce.to_field_elements(fields); - events.to_field_elements(fields); - actions.to_field_elements(fields); - call_data.to_field_elements(fields); - preconditions.to_field_elements(fields); - use_full_commitment.to_field_elements(fields); - implicit_account_creation_fee.to_field_elements(fields); - may_use_token.to_field_elements(fields); - authorization_kind.to_field_elements(fields); - } - } - - impl Check for Body { - fn check(&self, w: &mut Witness) { - let Self { - public_key: _, - token_id: _, - update: - Update { - app_state: _, - delegate: _, - verification_key: _, - permissions, - zkapp_uri: _, - token_symbol, - timing, - voting_for: _, - }, - balance_change, - increment_nonce: _, - events: _, - actions: _, - call_data: _, - preconditions, - use_full_commitment: _, - implicit_account_creation_fee: _, - may_use_token, - authorization_kind: _, - } = self; - - (permissions, Permissions::empty).check(w); - (token_symbol, TokenSymbol::default).check(w); - (timing, Timing::dummy).check(w); - balance_change.check(w); - - preconditions.check(w); - may_use_token.check(w); - } - } - - impl Body { - pub fn account_id(&self) -> AccountId { - let Self { - public_key, - token_id, - .. - } = self; - AccountId::create(public_key.clone(), token_id.clone()) - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct BodySimple { - pub public_key: CompressedPubKey, - pub token_id: TokenId, - pub update: Update, - pub balance_change: Signed, - pub increment_nonce: bool, - pub events: Events, - pub actions: Actions, - pub call_data: Fp, - pub call_depth: usize, - pub preconditions: Preconditions, - pub use_full_commitment: bool, - pub implicit_account_creation_fee: bool, - pub may_use_token: MayUseToken, - pub authorization_kind: AuthorizationKind, - } - - /// Notes: - /// The type in OCaml is this one: - /// - /// - /// For now we use the type from `mina_p2p_messages`, but we need to use our own. - /// Lots of inner types are (BigInt, Bigint) which should be replaced with `Pallas<_>` etc. - /// Also, in OCaml it has custom `{to/from}_binable` implementation. - /// - /// - pub type SideLoadedProof = Arc; - - /// Authorization methods for zkApp account updates. - /// - /// Defines how an account update is authorized to modify an account's state. - /// - /// - #[derive(Clone, PartialEq)] - pub enum Control { - /// Verified by a zero-knowledge proof against the account's verification - /// key. - Proof(SideLoadedProof), - /// Signed by the account's private key. - Signature(Signature), - /// No authorization (only valid for certain operations). - NoneGiven, - } - - impl std::fmt::Debug for Control { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Proof(_) => f.debug_tuple("Proof").field(&"_").finish(), - Self::Signature(arg0) => f.debug_tuple("Signature").field(arg0).finish(), - Self::NoneGiven => write!(f, "NoneGiven"), - } - } - } - - impl Control { - /// - pub fn tag(&self) -> crate::ControlTag { - match self { - Control::Proof(_) => crate::ControlTag::Proof, - Control::Signature(_) => crate::ControlTag::Signature, - Control::NoneGiven => crate::ControlTag::NoneGiven, - } - } - - pub fn dummy_of_tag(tag: ControlTag) -> Self { - match tag { - ControlTag::Proof => Self::Proof(dummy::sideloaded_proof()), - ControlTag::Signature => Self::Signature(Signature::dummy()), - ControlTag::NoneGiven => Self::NoneGiven, - } - } - - pub fn dummy(&self) -> Self { - Self::dummy_of_tag(self.tag()) - } - } - - #[derive(Clone, Debug, PartialEq)] - pub enum MayUseToken { - /// No permission to use any token other than the default Mina - /// token - No, - /// Has permission to use the token owned by the direct parent of - /// this account update, which may be inherited by child account - /// updates. - ParentsOwnToken, - /// Inherit the token permission available to the parent. - InheritFromParent, - } - - impl MayUseToken { - pub fn parents_own_token(&self) -> bool { - matches!(self, Self::ParentsOwnToken) - } - - pub fn inherit_from_parent(&self) -> bool { - matches!(self, Self::InheritFromParent) - } - - fn to_bits(&self) -> [bool; 2] { - // [ parents_own_token; inherit_from_parent ] - match self { - MayUseToken::No => [false, false], - MayUseToken::ParentsOwnToken => [true, false], - MayUseToken::InheritFromParent => [false, true], - } - } - } - - impl ToInputs for MayUseToken { - fn to_inputs(&self, inputs: &mut Inputs) { - for bit in self.to_bits() { - inputs.append_bool(bit); - } - } - } - - impl ToFieldElements for MayUseToken { - fn to_field_elements(&self, fields: &mut Vec) { - for bit in self.to_bits() { - bit.to_field_elements(fields); - } - } - } - - impl Check for MayUseToken { - fn check(&self, w: &mut Witness) { - use crate::proofs::field::field; - - let [parents_own_token, inherit_from_parent] = self.to_bits(); - let [parents_own_token, inherit_from_parent] = [ - parents_own_token.to_boolean(), - inherit_from_parent.to_boolean(), - ]; - - let sum = parents_own_token.to_field::() + inherit_from_parent.to_field::(); - let _sum_squared = field::mul(sum, sum, w); - } - } - - pub struct CheckAuthorizationResult { - pub proof_verifies: Bool, - pub signature_verifies: Bool, - } - - /// - pub type AccountUpdate = AccountUpdateSkeleton; - - #[derive(Debug, Clone, PartialEq)] - pub struct AccountUpdateSkeleton { - pub body: Body, - pub authorization: Control, - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct AccountUpdateSimple { - pub body: BodySimple, - pub authorization: Control, - } - - impl ToInputs for AccountUpdate { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - // Only the body is used - let Self { - body, - authorization: _, - } = self; - - inputs.append(body); - } - } - - impl AccountUpdate { - /// - /// - pub fn of_fee_payer(fee_payer: FeePayer) -> Self { - let FeePayer { - body: - FeePayerBody { - public_key, - fee, - valid_until, - nonce, - }, - authorization, - } = fee_payer; - - Self { - body: Body { - public_key, - token_id: TokenId::default(), - update: Update::noop(), - balance_change: Signed { - magnitude: Amount::of_fee(&fee), - sgn: Sgn::Neg, - }, - increment_nonce: true, - events: Events::empty(), - actions: Actions::empty(), - call_data: Fp::zero(), - preconditions: Preconditions { - network: { - let mut network = ZkAppPreconditions::accept(); - - let valid_util = valid_until.unwrap_or_else(Slot::max); - network.global_slot_since_genesis = OrIgnore::Check(ClosedInterval { - lower: Slot::zero(), - upper: valid_util, - }); - - network - }, - account: AccountPreconditions::with_nonce(nonce), - valid_while: Numeric::Ignore, - }, - use_full_commitment: true, - authorization_kind: AuthorizationKind::Signature, - implicit_account_creation_fee: true, - may_use_token: MayUseToken::No, - }, - authorization: Control::Signature(authorization), - } - } - - /// - pub fn account_id(&self) -> AccountId { - AccountId::new(self.body.public_key.clone(), self.body.token_id.clone()) - } - - /// - pub fn digest(&self) -> Fp { - self.hash_with_param(mina_core::NetworkConfig::global().account_update_hash_param) - } - - pub fn timing(&self) -> SetOrKeep { - self.body.update.timing.clone() - } - - pub fn may_use_parents_own_token(&self) -> bool { - self.body.may_use_token.parents_own_token() - } - - pub fn may_use_token_inherited_from_parent(&self) -> bool { - self.body.may_use_token.inherit_from_parent() - } - - pub fn public_key(&self) -> CompressedPubKey { - self.body.public_key.clone() - } - - pub fn token_id(&self) -> TokenId { - self.body.token_id.clone() - } - - pub fn increment_nonce(&self) -> bool { - self.body.increment_nonce - } - - pub fn implicit_account_creation_fee(&self) -> bool { - self.body.implicit_account_creation_fee - } - - // commitment and calls argument are ignored here, only used in the transaction snark - pub fn check_authorization( - &self, - _will_succeed: bool, - _commitment: Fp, - _calls: CallForest, - ) -> CheckAuthorizationResult { - match self.authorization { - Control::Signature(_) => CheckAuthorizationResult { - proof_verifies: false, - signature_verifies: true, - }, - Control::Proof(_) => CheckAuthorizationResult { - proof_verifies: true, - signature_verifies: false, - }, - Control::NoneGiven => CheckAuthorizationResult { - proof_verifies: false, - signature_verifies: false, - }, - } - } - - pub fn permissions(&self) -> SetOrKeep> { - self.body.update.permissions.clone() - } - - pub fn app_state(&self) -> [SetOrKeep; 8] { - self.body.update.app_state.clone() - } - - pub fn zkapp_uri(&self) -> SetOrKeep { - self.body.update.zkapp_uri.clone() - } - - /* - pub fn token_symbol(&self) -> SetOrKeep<[u8; 6]> { - self.body.update.token_symbol.clone() - } - */ - - pub fn token_symbol(&self) -> SetOrKeep { - self.body.update.token_symbol.clone() - } - - pub fn delegate(&self) -> SetOrKeep { - self.body.update.delegate.clone() - } - - pub fn voting_for(&self) -> SetOrKeep { - self.body.update.voting_for.clone() - } - - pub fn verification_key(&self) -> SetOrKeep { - self.body.update.verification_key.clone() - } - - pub fn valid_while_precondition(&self) -> OrIgnore> { - self.body.preconditions.valid_while.clone() - } - - pub fn actions(&self) -> Actions { - self.body.actions.clone() - } - - pub fn balance_change(&self) -> Signed { - self.body.balance_change - } - pub fn use_full_commitment(&self) -> bool { - self.body.use_full_commitment - } - - pub fn protocol_state_precondition(&self) -> ZkAppPreconditions { - self.body.preconditions.network.clone() - } - - pub fn account_precondition(&self) -> AccountPreconditions { - self.body.preconditions.account.clone() - } - - pub fn is_proved(&self) -> bool { - match &self.body.authorization_kind { - AuthorizationKind::Proof(_) => true, - AuthorizationKind::Signature | AuthorizationKind::NoneGiven => false, - } - } - - pub fn is_signed(&self) -> bool { - match &self.body.authorization_kind { - AuthorizationKind::Signature => true, - AuthorizationKind::Proof(_) | AuthorizationKind::NoneGiven => false, - } - } - - /// - pub fn verification_key_hash(&self) -> Option { - match &self.body.authorization_kind { - AuthorizationKind::Proof(vk_hash) => Some(*vk_hash), - _ => None, - } - } - - /// - pub fn of_simple(simple: &AccountUpdateSimple) -> Self { - let AccountUpdateSimple { - body: - BodySimple { - public_key, - token_id, - update, - balance_change, - increment_nonce, - events, - actions, - call_data, - call_depth: _, - preconditions, - use_full_commitment, - implicit_account_creation_fee, - may_use_token, - authorization_kind, - }, - authorization, - } = simple.clone(); - - Self { - body: Body { - public_key, - token_id, - update, - balance_change, - increment_nonce, - events, - actions, - call_data, - preconditions, - use_full_commitment, - implicit_account_creation_fee, - may_use_token, - authorization_kind, - }, - authorization, - } - } - - /// Usage: Random `AccountUpdate` to compare hashes with OCaml - pub fn rand() -> Self { - let mut rng = rand::thread_rng(); - let rng = &mut rng; - - Self { - body: Body { - public_key: gen_compressed(), - token_id: TokenId(Fp::rand(rng)), - update: Update { - app_state: std::array::from_fn(|_| SetOrKeep::gen(|| Fp::rand(rng))), - delegate: SetOrKeep::gen(gen_compressed), - verification_key: SetOrKeep::gen(VerificationKeyWire::gen), - permissions: SetOrKeep::gen(|| { - let auth_tag = [ - ControlTag::NoneGiven, - ControlTag::Proof, - ControlTag::Signature, - ] - .choose(rng) - .unwrap(); - - Permissions::gen(*auth_tag) - }), - zkapp_uri: SetOrKeep::gen(ZkAppUri::gen), - token_symbol: SetOrKeep::gen(TokenSymbol::gen), - timing: SetOrKeep::gen(|| Timing { - initial_minimum_balance: rng.gen(), - cliff_time: rng.gen(), - cliff_amount: rng.gen(), - vesting_period: rng.gen(), - vesting_increment: rng.gen(), - }), - voting_for: SetOrKeep::gen(|| VotingFor(Fp::rand(rng))), - }, - balance_change: Signed::gen(), - increment_nonce: rng.gen(), - events: Events(gen_events()), - actions: Actions(gen_events()), - call_data: Fp::rand(rng), - preconditions: Preconditions { - network: ZkAppPreconditions { - snarked_ledger_hash: OrIgnore::gen(|| Fp::rand(rng)), - blockchain_length: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), - min_window_density: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), - total_currency: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), - global_slot_since_genesis: OrIgnore::gen(|| { - ClosedInterval::gen(|| rng.gen()) - }), - staking_epoch_data: EpochData::gen(), - next_epoch_data: EpochData::gen(), - }, - account: AccountPreconditions(Account { - balance: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), - nonce: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), - receipt_chain_hash: OrIgnore::gen(|| Fp::rand(rng)), - delegate: OrIgnore::gen(gen_compressed), - state: std::array::from_fn(|_| OrIgnore::gen(|| Fp::rand(rng))), - action_state: OrIgnore::gen(|| Fp::rand(rng)), - proved_state: OrIgnore::gen(|| rng.gen()), - is_new: OrIgnore::gen(|| rng.gen()), - }), - valid_while: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), - }, - use_full_commitment: rng.gen(), - implicit_account_creation_fee: rng.gen(), - may_use_token: { - match MayUseToken::No { - MayUseToken::No => (), - MayUseToken::ParentsOwnToken => (), - MayUseToken::InheritFromParent => (), - }; - - [ - MayUseToken::No, - MayUseToken::InheritFromParent, - MayUseToken::ParentsOwnToken, - ] - .choose(rng) - .cloned() - .unwrap() - }, - authorization_kind: { - match AuthorizationKind::NoneGiven { - AuthorizationKind::NoneGiven => (), - AuthorizationKind::Signature => (), - AuthorizationKind::Proof(_) => (), - }; - - [ - AuthorizationKind::NoneGiven, - AuthorizationKind::Signature, - AuthorizationKind::Proof(Fp::rand(rng)), - ] - .choose(rng) - .cloned() - .unwrap() - }, - }, - authorization: { - match Control::NoneGiven { - Control::Proof(_) => (), - Control::Signature(_) => (), - Control::NoneGiven => (), - }; - - match rng.gen_range(0..3) { - 0 => Control::NoneGiven, - 1 => Control::Signature(Signature::dummy()), - _ => Control::Proof(dummy::sideloaded_proof()), - } - }, - } - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct Tree { - pub account_update: AccUpdate, - pub account_update_digest: MutableFp, - pub calls: CallForest, - } - - impl Tree { - // TODO: Cache this result somewhere ? - pub fn digest(&self) -> Fp { - let stack_hash = match self.calls.0.first() { - Some(e) => e.stack_hash.get().expect("Must call `ensure_hashed`"), - None => Fp::zero(), - }; - let account_update_digest = self.account_update_digest.get().unwrap(); - hash_with_kimchi( - &MINA_ACCOUNT_UPDATE_NODE, - &[account_update_digest, stack_hash], - ) - } - - fn fold(&self, init: Vec, f: &mut F) -> Vec - where - F: FnMut(Vec, &AccUpdate) -> Vec, - { - self.calls.fold(f(init, &self.account_update), f) - } - } - - /// - #[derive(Debug, Clone)] - pub struct WithStackHash { - pub elt: Tree, - pub stack_hash: MutableFp, - } - - impl PartialEq for WithStackHash { - fn eq(&self, other: &Self) -> bool { - self.elt == other.elt && self.stack_hash == other.stack_hash - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct CallForest(pub Vec>); - - impl Default for CallForest { - fn default() -> Self { - Self::new() - } - } - - #[derive(Clone)] - struct CallForestContext { - caller: TokenId, - this: TokenId, - } - - pub trait AccountUpdateRef { - fn account_update_ref(&self) -> &AccountUpdate; - } - impl AccountUpdateRef for AccountUpdate { - fn account_update_ref(&self) -> &AccountUpdate { - self - } - } - impl AccountUpdateRef for (AccountUpdate, T) { - fn account_update_ref(&self) -> &AccountUpdate { - let (this, _) = self; - this - } - } - impl AccountUpdateRef for AccountUpdateSimple { - fn account_update_ref(&self) -> &AccountUpdate { - // AccountUpdateSimple are first converted into `AccountUpdate` - unreachable!() - } - } - - impl CallForest { - pub fn new() -> Self { - Self(Vec::new()) - } - - pub fn empty() -> Self { - Self::new() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - // In OCaml push/pop to the head is cheap because they work with lists. - // In Rust we use vectors so we will push/pop to the tail. - // To work with the elements as if they were in the original order we need to iterate backwards - pub fn iter(&self) -> impl Iterator> { - self.0.iter() //.rev() - } - // Warning: Update this if we ever change the order - pub fn first(&self) -> Option<&WithStackHash> { - self.0.first() - } - // Warning: Update this if we ever change the order - pub fn tail(&self) -> Option<&[WithStackHash]> { - self.0.get(1..) - } - - pub fn hash(&self) -> Fp { - self.ensure_hashed(); - /* - for x in self.0.iter() { - println!("hash: {:?}", x.stack_hash); - } - */ - - if let Some(x) = self.first() { - x.stack_hash.get().unwrap() // Never fail, we called `ensure_hashed` - } else { - Fp::zero() - } - } - - fn cons_tree(&self, tree: Tree) -> Self { - self.ensure_hashed(); - - let hash = tree.digest(); - let h_tl = self.hash(); - - let stack_hash = hash_with_kimchi(&MINA_ACCOUNT_UPDATE_CONS, &[hash, h_tl]); - let node = WithStackHash:: { - elt: tree, - stack_hash: MutableFp::new(stack_hash), - }; - let mut forest = Vec::with_capacity(self.0.len() + 1); - forest.push(node); - forest.extend(self.0.iter().cloned()); - - Self(forest) - } - - pub fn pop_exn(&self) -> ((AccUpdate, CallForest), CallForest) { - if self.0.is_empty() { - panic!() - } - - let Tree:: { - account_update, - calls, - .. - } = self.0[0].elt.clone(); - ( - (account_update, calls), - CallForest(Vec::from_iter(self.0[1..].iter().cloned())), - ) - } - - /// - fn fold_impl<'a, A, F>(&'a self, init: A, fun: &mut F) -> A - where - F: FnMut(A, &'a AccUpdate) -> A, - { - let mut accum = init; - for elem in self.iter() { - accum = fun(accum, &elem.elt.account_update); - accum = elem.elt.calls.fold_impl(accum, fun); - } - accum - } - - pub fn fold<'a, A, F>(&'a self, init: A, mut fun: F) -> A - where - F: FnMut(A, &'a AccUpdate) -> A, - { - self.fold_impl(init, &mut fun) - } - - pub fn exists<'a, F>(&'a self, mut fun: F) -> bool - where - F: FnMut(&'a AccUpdate) -> bool, - { - self.fold(false, |acc, x| acc || fun(x)) - } - - fn map_to_impl( - &self, - fun: &F, - ) -> CallForest - where - F: Fn(&AccUpdate) -> AnotherAccUpdate, - { - CallForest::( - self.iter() - .map(|item| WithStackHash:: { - elt: Tree:: { - account_update: fun(&item.elt.account_update), - account_update_digest: item.elt.account_update_digest.clone(), - calls: item.elt.calls.map_to_impl(fun), - }, - stack_hash: item.stack_hash.clone(), - }) - .collect(), - ) - } - - #[must_use] - pub fn map_to( - &self, - fun: F, - ) -> CallForest - where - F: Fn(&AccUpdate) -> AnotherAccUpdate, - { - self.map_to_impl(&fun) - } - - fn map_with_trees_to_impl( - &self, - fun: &F, - ) -> CallForest - where - F: Fn(&AccUpdate, &Tree) -> AnotherAccUpdate, - { - CallForest::( - self.iter() - .map(|item| { - let account_update = fun(&item.elt.account_update, &item.elt); - - WithStackHash:: { - elt: Tree:: { - account_update, - account_update_digest: item.elt.account_update_digest.clone(), - calls: item.elt.calls.map_with_trees_to_impl(fun), - }, - stack_hash: item.stack_hash.clone(), - } - }) - .collect(), - ) - } - - #[must_use] - pub fn map_with_trees_to( - &self, - fun: F, - ) -> CallForest - where - F: Fn(&AccUpdate, &Tree) -> AnotherAccUpdate, - { - self.map_with_trees_to_impl(&fun) - } - - fn try_map_to_impl( - &self, - fun: &mut F, - ) -> Result, E> - where - F: FnMut(&AccUpdate) -> Result, - { - Ok(CallForest::( - self.iter() - .map(|item| { - Ok(WithStackHash:: { - elt: Tree:: { - account_update: fun(&item.elt.account_update)?, - account_update_digest: item.elt.account_update_digest.clone(), - calls: item.elt.calls.try_map_to_impl(fun)?, - }, - stack_hash: item.stack_hash.clone(), - }) - }) - .collect::>()?, - )) - } - - pub fn try_map_to( - &self, - mut fun: F, - ) -> Result, E> - where - F: FnMut(&AccUpdate) -> Result, - { - self.try_map_to_impl(&mut fun) - } - - fn to_account_updates_impl(&self, accounts: &mut Vec) { - // TODO: Check iteration order in OCaml - for elem in self.iter() { - accounts.push(elem.elt.account_update.clone()); - elem.elt.calls.to_account_updates_impl(accounts); - } - } - - /// - pub fn to_account_updates(&self) -> Vec { - let mut accounts = Vec::with_capacity(128); - self.to_account_updates_impl(&mut accounts); - accounts - } - - fn to_zkapp_command_with_hashes_list_impl(&self, output: &mut Vec<(AccUpdate, Fp)>) { - self.iter().for_each(|item| { - let WithStackHash { elt, stack_hash } = item; - let Tree { - account_update, - account_update_digest: _, - calls, - } = elt; - output.push((account_update.clone(), stack_hash.get().unwrap())); // Never fail, we called `ensure_hashed` - calls.to_zkapp_command_with_hashes_list_impl(output); - }); - } - - pub fn to_zkapp_command_with_hashes_list(&self) -> Vec<(AccUpdate, Fp)> { - self.ensure_hashed(); - - let mut output = Vec::with_capacity(128); - self.to_zkapp_command_with_hashes_list_impl(&mut output); - output - } - - pub fn ensure_hashed(&self) { - let Some(first) = self.first() else { - return; - }; - if first.stack_hash.get().is_none() { - self.accumulate_hashes(); - } - } - } - - impl CallForest { - /// - pub fn accumulate_hashes(&self) { - /// - fn cons(hash: Fp, h_tl: Fp) -> Fp { - hash_with_kimchi(&MINA_ACCOUNT_UPDATE_CONS, &[hash, h_tl]) - } - - /// - fn hash( - elem: Option<&WithStackHash>, - ) -> Fp { - match elem { - Some(next) => next.stack_hash.get().unwrap(), // Never fail, we hash them from reverse below - None => Fp::zero(), - } - } - - // We traverse the list in reverse here (to get same behavior as OCaml recursivity) - // Note that reverse here means 0 to last, see `CallForest::iter` for explaination - // - // We use indexes to make the borrow checker happy - - for index in (0..self.0.len()).rev() { - let elem = &self.0[index]; - let WithStackHash { - elt: - Tree:: { - account_update, - account_update_digest, - calls, - .. - }, - .. - } = elem; - - calls.accumulate_hashes(); - account_update_digest.set(account_update.account_update_ref().digest()); - - let node_hash = elem.elt.digest(); - let hash = hash(self.0.get(index + 1)); - - self.0[index].stack_hash.set(cons(node_hash, hash)); - } - } - } - - impl CallForest { - pub fn cons( - &self, - calls: Option>, - account_update: AccountUpdate, - ) -> Self { - let account_update_digest = account_update.digest(); - - let tree = Tree:: { - account_update, - account_update_digest: MutableFp::new(account_update_digest), - calls: calls.unwrap_or_else(|| CallForest(Vec::new())), - }; - self.cons_tree(tree) - } - - pub fn accumulate_hashes_predicated(&mut self) { - // Note: There seems to be no difference with `accumulate_hashes` - self.accumulate_hashes(); - } - - /// - pub fn of_wire( - &mut self, - _wired: &[MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA], - ) { - self.accumulate_hashes(); - } - - /// - pub fn to_wire( - &self, - _wired: &mut [MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA], - ) { - // self.remove_callers(wired); - } - } - - impl CallForest<(AccountUpdate, Option>)> { - // Don't implement `{from,to}_wire` because the binprot types contain the hashes - - // /// - // pub fn of_wire( - // &mut self, - // _wired: &[v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesA], - // ) { - // self.accumulate_hashes(&|(account_update, _vk_opt)| account_update.digest()); - // } - - // /// - // pub fn to_wire( - // &self, - // _wired: &mut [MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA], - // ) { - // // self.remove_callers(wired); - // } - } - - /// - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct FeePayerBody { - pub public_key: CompressedPubKey, - pub fee: Fee, - pub valid_until: Option, - pub nonce: Nonce, - } - - /// - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct FeePayer { - pub body: FeePayerBody, - pub authorization: Signature, - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct ZkAppCommand { - pub fee_payer: FeePayer, - pub account_updates: CallForest, - pub memo: Memo, - } - - #[derive(Debug, Clone, PartialEq, Hash, Eq, Ord, PartialOrd)] - pub enum AccessedOrNot { - Accessed, - NotAccessed, - } - - impl ZkAppCommand { - pub fn fee_payer(&self) -> AccountId { - let public_key = self.fee_payer.body.public_key.clone(); - AccountId::new(public_key, self.fee_token()) - } - - pub fn fee_token(&self) -> TokenId { - TokenId::default() - } - - pub fn fee(&self) -> Fee { - self.fee_payer.body.fee - } - - pub fn fee_excess(&self) -> FeeExcess { - FeeExcess::of_single((self.fee_token(), Signed::::of_unsigned(self.fee()))) - } - - fn fee_payer_account_update(&self) -> &FeePayer { - let Self { fee_payer, .. } = self; - fee_payer - } - - pub fn applicable_at_nonce(&self) -> Nonce { - self.fee_payer_account_update().body.nonce - } - - pub fn weight(&self) -> u64 { - let Self { - fee_payer, - account_updates, - memo, - } = self; - [ - zkapp_weight::fee_payer(fee_payer), - zkapp_weight::account_updates(account_updates), - zkapp_weight::memo(memo), - ] - .iter() - .sum() - } - - pub fn has_zero_vesting_period(&self) -> bool { - self.account_updates - .exists(|account_update| match &account_update.body.update.timing { - SetOrKeep::Keep => false, - SetOrKeep::Set(Timing { vesting_period, .. }) => vesting_period.is_zero(), - }) - } - - pub fn is_incompatible_version(&self) -> bool { - self.account_updates.exists(|account_update| { - match &account_update.body.update.permissions { - SetOrKeep::Keep => false, - SetOrKeep::Set(Permissions { - set_verification_key, - .. - }) => { - let SetVerificationKey { - auth: _, - txn_version, - } = set_verification_key; - *txn_version != crate::TXN_VERSION_CURRENT - } - } - }) - } - - fn zkapp_cost( - proof_segments: usize, - signed_single_segments: usize, - signed_pair_segments: usize, - ) -> f64 { - // (*10.26*np + 10.08*n2 + 9.14*n1 < 69.45*) - let GenesisConstant { - zkapp_proof_update_cost: proof_cost, - zkapp_signed_pair_update_cost: signed_pair_cost, - zkapp_signed_single_update_cost: signed_single_cost, - .. - } = GENESIS_CONSTANT; - - (proof_cost * (proof_segments as f64)) - + (signed_pair_cost * (signed_pair_segments as f64)) - + (signed_single_cost * (signed_single_segments as f64)) - } - - /// Zkapp_command transactions are filtered using this predicate - /// - when adding to the transaction pool - /// - in incoming blocks - pub fn valid_size(&self) -> Result<(), String> { - use crate::proofs::zkapp::group::{SegmentBasic, ZkappCommandIntermediateState}; - - let Self { - account_updates, - fee_payer: _, - memo: _, - } = self; - - let events_elements = - |events: &[Event]| -> usize { events.iter().map(Event::len).sum() }; - - let mut n_account_updates = 0; - let (mut num_event_elements, mut num_action_elements) = (0, 0); - - account_updates.fold((), |_, account_update| { - num_event_elements += events_elements(account_update.body.events.events()); - num_action_elements += events_elements(account_update.body.actions.events()); - n_account_updates += 1; - }); - - let group = std::iter::repeat(((), (), ())) - .take(n_account_updates + 2) // + 2 to prepend two. See OCaml - .collect::>(); - - let groups = crate::proofs::zkapp::group::group_by_zkapp_command_rev::<_, (), (), ()>( - [self], - vec![vec![((), (), ())], group], - ); - - let (mut proof_segments, mut signed_single_segments, mut signed_pair_segments) = - (0, 0, 0); - - for ZkappCommandIntermediateState { spec, .. } in &groups { - match spec { - SegmentBasic::Proved => proof_segments += 1, - SegmentBasic::OptSigned => signed_single_segments += 1, - SegmentBasic::OptSignedOptSigned => signed_pair_segments += 1, - } - } - - let GenesisConstant { - zkapp_transaction_cost_limit: cost_limit, - max_event_elements, - max_action_elements, - .. - } = GENESIS_CONSTANT; - - let zkapp_cost_within_limit = - Self::zkapp_cost(proof_segments, signed_single_segments, signed_pair_segments) - < cost_limit; - let valid_event_elements = num_event_elements <= max_event_elements; - let valid_action_elements = num_action_elements <= max_action_elements; - - if zkapp_cost_within_limit && valid_event_elements && valid_action_elements { - return Ok(()); - } - - let err = [ - (zkapp_cost_within_limit, "zkapp transaction too expensive"), - (valid_event_elements, "too many event elements"), - (valid_action_elements, "too many action elements"), - ] - .iter() - .filter(|(b, _s)| !b) - .map(|(_b, s)| s) - .join(";"); - - Err(err) - } - - /// - pub fn account_access_statuses( - &self, - status: &TransactionStatus, - ) -> Vec<(AccountId, AccessedOrNot)> { - use AccessedOrNot::*; - use TransactionStatus::*; - - // always `Accessed` for fee payer - let init = vec![(self.fee_payer(), Accessed)]; - - let status_sym = match status { - Applied => Accessed, - Failed(_) => NotAccessed, - }; - - let ids = self - .account_updates - .fold(init, |mut accum, account_update| { - accum.push((account_update.account_id(), status_sym.clone())); - accum - }); - // WARNING: the code previous to merging latest changes wasn't doing the "rev()" call. Check this in case of errors. - ids.iter() - .unique() /*.rev()*/ - .cloned() - .collect() - } - - /// - pub fn accounts_referenced(&self) -> Vec { - self.account_access_statuses(&TransactionStatus::Applied) - .into_iter() - .map(|(id, _status)| id) - .collect() - } - - /// - pub fn of_verifiable(verifiable: verifiable::ZkAppCommand) -> Self { - Self { - fee_payer: verifiable.fee_payer, - account_updates: verifiable.account_updates.map_to(|(acc, _)| acc.clone()), - memo: verifiable.memo, - } - } - - /// - pub fn account_updates_hash(&self) -> Fp { - self.account_updates.hash() - } - - /// - pub fn extract_vks(&self) -> Vec<(AccountId, VerificationKeyWire)> { - self.account_updates - .fold(Vec::with_capacity(256), |mut acc, p| { - if let SetOrKeep::Set(vk) = &p.body.update.verification_key { - acc.push((p.account_id(), vk.clone())); - }; - acc - }) - } - - pub fn all_account_updates(&self) -> CallForest { - let p = &self.fee_payer; - - let mut fee_payer = AccountUpdate::of_fee_payer(p.clone()); - fee_payer.authorization = Control::Signature(p.authorization.clone()); - - self.account_updates.cons(None, fee_payer) - } - - pub fn all_account_updates_list(&self) -> Vec { - let mut account_updates = Vec::with_capacity(16); - account_updates.push(AccountUpdate::of_fee_payer(self.fee_payer.clone())); - - self.account_updates.fold(account_updates, |mut acc, u| { - acc.push(u.clone()); - acc - }) - } - - pub fn commitment(&self) -> TransactionCommitment { - let account_updates_hash = self.account_updates_hash(); - TransactionCommitment::create(account_updates_hash) - } - } - - pub mod verifiable { - use mina_p2p_messages::v2::MinaBaseZkappCommandVerifiableStableV1; - - use super::*; - - #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] - #[serde(try_from = "MinaBaseZkappCommandVerifiableStableV1")] - #[serde(into = "MinaBaseZkappCommandVerifiableStableV1")] - pub struct ZkAppCommand { - pub fee_payer: FeePayer, - pub account_updates: CallForest<(AccountUpdate, Option)>, - pub memo: Memo, - } - - fn ok_if_vk_hash_expected( - got: VerificationKeyWire, - expected: Fp, - ) -> Result { - if got.hash() == expected { - return Ok(got.clone()); - } - Err(format!( - "Expected vk hash doesn't match hash in vk we received\ - expected: {:?}\ - got: {:?}", - expected, got - )) - } - - pub fn find_vk_via_ledger( - ledger: L, - expected_vk_hash: Fp, - account_id: &AccountId, - ) -> Result - where - L: LedgerIntf + Clone, - { - let vk = ledger - .location_of_account(account_id) - .and_then(|location| ledger.get(&location)) - .and_then(|account| { - account - .zkapp - .as_ref() - .and_then(|zkapp| zkapp.verification_key.clone()) - }); - - match vk { - Some(vk) => ok_if_vk_hash_expected(vk, expected_vk_hash), - None => Err(format!( - "No verification key found for proved account update\ - account_id: {:?}", - account_id - )), - } - } - - fn check_authorization(p: &AccountUpdate) -> Result<(), String> { - use AuthorizationKind as AK; - use Control as C; - - match (&p.authorization, &p.body.authorization_kind) { - (C::NoneGiven, AK::NoneGiven) - | (C::Proof(_), AK::Proof(_)) - | (C::Signature(_), AK::Signature) => Ok(()), - _ => Err(format!( - "Authorization kind does not match the authorization\ - expected={:#?}\ - got={:#?}", - p.body.authorization_kind, p.authorization - )), - } - } - - /// Ensures that there's a verification_key available for all account_updates - /// and creates a valid command associating the correct keys with each - /// account_id. - /// - /// If an account_update replaces the verification_key (or deletes it), - /// subsequent account_updates use the replaced key instead of looking in the - /// ledger for the key (ie set by a previous transaction). - pub fn create( - zkapp: &super::ZkAppCommand, - is_failed: bool, - find_vk: impl Fn(Fp, &AccountId) -> Result, - ) -> Result { - let super::ZkAppCommand { - fee_payer, - account_updates, - memo, - } = zkapp; - - let mut tbl = HashMap::with_capacity(128); - // Keep track of the verification keys that have been set so far - // during this transaction. - let mut vks_overridden: HashMap> = - HashMap::with_capacity(128); - - let account_updates = account_updates.try_map_to(|p| { - let account_id = p.account_id(); - - check_authorization(p)?; - - let result = match (&p.body.authorization_kind, is_failed) { - (AuthorizationKind::Proof(vk_hash), false) => { - let prioritized_vk = { - // only lookup _past_ vk setting, ie exclude the new one we - // potentially set in this account_update (use the non-' - // vks_overrided) . - - match vks_overridden.get(&account_id) { - Some(Some(vk)) => { - ok_if_vk_hash_expected(vk.clone(), *vk_hash)? - }, - Some(None) => { - // we explicitly have erased the key - return Err(format!("No verification key found for proved account \ - update: the verification key was removed by a \ - previous account update\ - account_id={:?}", account_id)); - } - None => { - // we haven't set anything; lookup the vk in the fallback - find_vk(*vk_hash, &account_id)? - }, - } - }; - - tbl.insert(account_id, prioritized_vk.hash()); - - Ok((p.clone(), Some(prioritized_vk))) - }, - - _ => { - Ok((p.clone(), None)) - } - }; - - // NOTE: we only update the overriden map AFTER verifying the update to make sure - // that the verification for the VK update itself is done against the previous VK. - if let SetOrKeep::Set(vk_next) = &p.body.update.verification_key { - vks_overridden.insert(p.account_id().clone(), Some(vk_next.clone())); - } - - result - })?; - - Ok(ZkAppCommand { - fee_payer: fee_payer.clone(), - account_updates, - memo: memo.clone(), - }) - } - } - - pub mod valid { - use crate::scan_state::transaction_logic::zkapp_command::verifiable::create; - - use super::*; - - #[derive(Clone, Debug, PartialEq)] - pub struct ZkAppCommand { - pub zkapp_command: super::ZkAppCommand, - } - - impl ZkAppCommand { - pub fn forget(self) -> super::ZkAppCommand { - self.zkapp_command - } - pub fn forget_ref(&self) -> &super::ZkAppCommand { - &self.zkapp_command - } - } - - /// - pub fn of_verifiable(cmd: verifiable::ZkAppCommand) -> ZkAppCommand { - ZkAppCommand { - zkapp_command: super::ZkAppCommand::of_verifiable(cmd), - } - } - - /// - pub fn to_valid( - zkapp_command: super::ZkAppCommand, - status: &TransactionStatus, - find_vk: impl Fn(Fp, &AccountId) -> Result, - ) -> Result { - create(&zkapp_command, status.is_failed(), find_vk).map(of_verifiable) - } - } - - pub struct MaybeWithStatus { - pub cmd: T, - pub status: Option, - } - - impl From> for MaybeWithStatus { - fn from(value: WithStatus) -> Self { - let WithStatus { data, status } = value; - Self { - cmd: data, - status: Some(status), - } - } - } - - impl From> for WithStatus { - fn from(value: MaybeWithStatus) -> Self { - let MaybeWithStatus { cmd, status } = value; - Self { - data: cmd, - status: status.unwrap(), - } - } - } - - impl MaybeWithStatus { - pub fn cmd(&self) -> &T { - &self.cmd - } - pub fn is_failed(&self) -> bool { - self.status - .as_ref() - .map(TransactionStatus::is_failed) - .unwrap_or(false) - } - pub fn map(self, fun: F) -> MaybeWithStatus - where - F: FnOnce(T) -> V, - { - MaybeWithStatus { - cmd: fun(self.cmd), - status: self.status, - } - } - } - - pub trait ToVerifiableCache { - fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire>; - fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire); - } - - pub trait ToVerifiableStrategy { - type Cache: ToVerifiableCache; - - fn create_all( - cmd: &ZkAppCommand, - is_failed: bool, - cache: &mut Self::Cache, - ) -> Result { - let verified_cmd = verifiable::create(cmd, is_failed, |vk_hash, account_id| { - cache - .find(account_id, &vk_hash) - .cloned() - .or_else(|| { - cmd.extract_vks() - .iter() - .find(|(id, _)| account_id == id) - .map(|(_, key)| key.clone()) - }) - .ok_or_else(|| format!("verification key not found in cache: {:?}", vk_hash)) - })?; - if !is_failed { - for (account_id, vk) in cmd.extract_vks() { - cache.add(account_id, vk); - } - } - Ok(verified_cmd) - } - } - - pub mod from_unapplied_sequence { - use super::*; - - pub struct Cache { - cache: HashMap>, - } - - impl Cache { - pub fn new(cache: HashMap>) -> Self { - Self { cache } - } - } - - impl ToVerifiableCache for Cache { - fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { - let vks = self.cache.get(account_id)?; - vks.get(vk_hash) - } - fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { - let vks = self.cache.entry(account_id).or_default(); - vks.insert(vk.hash(), vk); - } - } - - pub struct FromUnappliedSequence; - - impl ToVerifiableStrategy for FromUnappliedSequence { - type Cache = Cache; - } - } - - pub mod from_applied_sequence { - use super::*; - - pub struct Cache { - cache: HashMap, - } - - impl Cache { - pub fn new(cache: HashMap) -> Self { - Self { cache } - } - } - - impl ToVerifiableCache for Cache { - fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { - self.cache - .get(account_id) - .filter(|vk| &vk.hash() == vk_hash) - } - fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { - self.cache.insert(account_id, vk); - } - } - - pub struct FromAppliedSequence; - - impl ToVerifiableStrategy for FromAppliedSequence { - type Cache = Cache; - } - } - - /// - pub mod zkapp_weight { - use crate::scan_state::transaction_logic::zkapp_command::{ - AccountUpdate, CallForest, FeePayer, - }; - - pub fn account_update(_: &AccountUpdate) -> u64 { - 1 - } - pub fn fee_payer(_: &FeePayer) -> u64 { - 1 - } - pub fn account_updates(list: &CallForest) -> u64 { - list.fold(0, |acc, p| acc + account_update(p)) - } - pub fn memo(_: &super::Memo) -> u64 { - 0 - } - } -} - +pub mod zkapp_command; pub mod zkapp_statement { use poseidon::hash::params::MINA_ACCOUNT_UPDATE_CONS; diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command.rs b/ledger/src/scan_state/transaction_logic/zkapp_command.rs new file mode 100644 index 000000000..f18b6d7ad --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command.rs @@ -0,0 +1,3300 @@ +use std::{collections::HashMap, sync::Arc}; + +use ark_ff::{UniformRand, Zero}; +use itertools::Itertools; +use mina_hasher::Fp; +use mina_p2p_messages::v2::MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA; +use mina_signer::{CompressedPubKey, Signature}; +use poseidon::hash::{ + hash_noinputs, hash_with_kimchi, + params::{ + MINA_ACCOUNT_UPDATE_CONS, MINA_ACCOUNT_UPDATE_NODE, MINA_ZKAPP_EVENT, MINA_ZKAPP_EVENTS, + MINA_ZKAPP_SEQ_EVENTS, NO_INPUT_MINA_ZKAPP_ACTIONS_EMPTY, NO_INPUT_MINA_ZKAPP_EVENTS_EMPTY, + }, + Inputs, +}; +use rand::{seq::SliceRandom, Rng}; + +use crate::{ + dummy, gen_compressed, gen_keypair, + hash::AppendToInputs, + proofs::{ + field::{Boolean, ToBoolean}, + to_field_elements::ToFieldElements, + transaction::Check, + witness::Witness, + }, + scan_state::{ + currency::{ + Amount, Balance, Fee, Length, Magnitude, MinMax, Nonce, Sgn, Signed, Slot, SlotSpan, + }, + fee_excess::FeeExcess, + GenesisConstant, GENESIS_CONSTANT, + }, + sparse_ledger::LedgerIntf, + zkapps::checks::{ZkappCheck, ZkappCheckOps}, + AccountId, AuthRequired, ControlTag, MutableFp, MyCow, Permissions, SetVerificationKey, + ToInputs, TokenId, TokenSymbol, VerificationKey, VerificationKeyWire, VotingFor, ZkAppAccount, + ZkAppUri, +}; + +use super::{ + protocol_state::{self, ProtocolStateView}, + zkapp_statement::TransactionCommitment, + Memo, TransactionFailure, TransactionStatus, WithStatus, +}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Event(pub Vec); + +impl Event { + pub fn empty() -> Self { + Self(Vec::new()) + } + pub fn hash(&self) -> Fp { + hash_with_kimchi(&MINA_ZKAPP_EVENT, &self.0[..]) + } + pub fn len(&self) -> usize { + let Self(list) = self; + list.len() + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct Events(pub Vec); + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct Actions(pub Vec); + +pub fn gen_events() -> Vec { + let mut rng = rand::thread_rng(); + + let n = rng.gen_range(0..=5); + + (0..=n) + .map(|_| { + let n = rng.gen_range(0..=3); + let event = (0..=n).map(|_| Fp::rand(&mut rng)).collect(); + Event(event) + }) + .collect() +} + +use poseidon::hash::LazyParam; + +/// +pub trait MakeEvents { + const DERIVER_NAME: (); // Unused here for now + + fn get_salt_phrase() -> &'static LazyParam; + fn get_hash_prefix() -> &'static LazyParam; + fn events(&self) -> &[Event]; + fn empty_hash() -> Fp; +} + +/// +impl MakeEvents for Events { + const DERIVER_NAME: () = (); + fn get_salt_phrase() -> &'static LazyParam { + &NO_INPUT_MINA_ZKAPP_EVENTS_EMPTY + } + fn get_hash_prefix() -> &'static poseidon::hash::LazyParam { + &MINA_ZKAPP_EVENTS + } + fn events(&self) -> &[Event] { + self.0.as_slice() + } + fn empty_hash() -> Fp { + cache_one!(Fp, events_to_field(&Events::empty())) + } +} + +/// +impl MakeEvents for Actions { + const DERIVER_NAME: () = (); + fn get_salt_phrase() -> &'static LazyParam { + &NO_INPUT_MINA_ZKAPP_ACTIONS_EMPTY + } + fn get_hash_prefix() -> &'static poseidon::hash::LazyParam { + &MINA_ZKAPP_SEQ_EVENTS + } + fn events(&self) -> &[Event] { + self.0.as_slice() + } + fn empty_hash() -> Fp { + cache_one!(Fp, events_to_field(&Actions::empty())) + } +} + +/// +pub fn events_to_field(e: &E) -> Fp +where + E: MakeEvents, +{ + let init = hash_noinputs(E::get_salt_phrase()); + + e.events().iter().rfold(init, |accum, elem| { + hash_with_kimchi(E::get_hash_prefix(), &[accum, elem.hash()]) + }) +} + +impl ToInputs for Events { + fn to_inputs(&self, inputs: &mut Inputs) { + inputs.append(&events_to_field(self)); + } +} + +impl ToInputs for Actions { + fn to_inputs(&self, inputs: &mut Inputs) { + inputs.append(&events_to_field(self)); + } +} + +impl ToFieldElements for Events { + fn to_field_elements(&self, fields: &mut Vec) { + events_to_field(self).to_field_elements(fields); + } +} + +impl ToFieldElements for Actions { + fn to_field_elements(&self, fields: &mut Vec) { + events_to_field(self).to_field_elements(fields); + } +} + +/// Note: It's a different one than in the normal `Account` +/// +/// +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Timing { + pub initial_minimum_balance: Balance, + pub cliff_time: Slot, + pub cliff_amount: Amount, + pub vesting_period: SlotSpan, + pub vesting_increment: Amount, +} + +impl Timing { + /// + fn dummy() -> Self { + Self { + initial_minimum_balance: Balance::zero(), + cliff_time: Slot::zero(), + cliff_amount: Amount::zero(), + vesting_period: SlotSpan::zero(), + vesting_increment: Amount::zero(), + } + } + + /// + /// + pub fn of_account_timing(timing: crate::account::Timing) -> Option { + match timing { + crate::Timing::Untimed => None, + crate::Timing::Timed { + initial_minimum_balance, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + } => Some(Self { + initial_minimum_balance, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + }), + } + } + + /// + pub fn to_account_timing(self) -> crate::account::Timing { + let Self { + initial_minimum_balance, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + } = self; + + crate::account::Timing::Timed { + initial_minimum_balance, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + } + } +} + +impl ToFieldElements for Timing { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + initial_minimum_balance, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + } = self; + + initial_minimum_balance.to_field_elements(fields); + cliff_time.to_field_elements(fields); + cliff_amount.to_field_elements(fields); + vesting_period.to_field_elements(fields); + vesting_increment.to_field_elements(fields); + } +} + +impl Check for Timing { + fn check(&self, w: &mut Witness) { + let Self { + initial_minimum_balance, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + } = self; + + initial_minimum_balance.check(w); + cliff_time.check(w); + cliff_amount.check(w); + vesting_period.check(w); + vesting_increment.check(w); + } +} + +impl ToInputs for Timing { + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let Timing { + initial_minimum_balance, + cliff_time, + cliff_amount, + vesting_period, + vesting_increment, + } = self; + + inputs.append_u64(initial_minimum_balance.as_u64()); + inputs.append_u32(cliff_time.as_u32()); + inputs.append_u64(cliff_amount.as_u64()); + inputs.append_u32(vesting_period.as_u32()); + inputs.append_u64(vesting_increment.as_u64()); + } +} + +impl Events { + pub fn empty() -> Self { + Self(Vec::new()) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn push_event(acc: Fp, event: Event) -> Fp { + hash_with_kimchi(Self::get_hash_prefix(), &[acc, event.hash()]) + } + + pub fn push_events(&self, acc: Fp) -> Fp { + let hash = self + .0 + .iter() + .rfold(hash_noinputs(Self::get_salt_phrase()), |acc, e| { + Self::push_event(acc, e.clone()) + }); + hash_with_kimchi(Self::get_hash_prefix(), &[acc, hash]) + } +} + +impl Actions { + pub fn empty() -> Self { + Self(Vec::new()) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn push_event(acc: Fp, event: Event) -> Fp { + hash_with_kimchi(Self::get_hash_prefix(), &[acc, event.hash()]) + } + + pub fn push_events(&self, acc: Fp) -> Fp { + let hash = self + .0 + .iter() + .rfold(hash_noinputs(Self::get_salt_phrase()), |acc, e| { + Self::push_event(acc, e.clone()) + }); + hash_with_kimchi(Self::get_hash_prefix(), &[acc, hash]) + } +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SetOrKeep { + Set(T), + Keep, +} + +impl SetOrKeep { + fn map<'a, F, U>(&'a self, fun: F) -> SetOrKeep + where + F: FnOnce(&'a T) -> U, + U: Clone, + { + match self { + SetOrKeep::Set(v) => SetOrKeep::Set(fun(v)), + SetOrKeep::Keep => SetOrKeep::Keep, + } + } + + pub fn into_map(self, fun: F) -> SetOrKeep + where + F: FnOnce(T) -> U, + U: Clone, + { + match self { + SetOrKeep::Set(v) => SetOrKeep::Set(fun(v)), + SetOrKeep::Keep => SetOrKeep::Keep, + } + } + + pub fn set_or_keep(&self, x: T) -> T { + match self { + Self::Set(data) => data.clone(), + Self::Keep => x, + } + } + + pub fn is_keep(&self) -> bool { + match self { + Self::Keep => true, + Self::Set(_) => false, + } + } + + pub fn is_set(&self) -> bool { + !self.is_keep() + } + + pub fn gen(mut fun: F) -> Self + where + F: FnMut() -> T, + { + let mut rng = rand::thread_rng(); + + if rng.gen() { + Self::Set(fun()) + } else { + Self::Keep + } + } +} + +impl ToInputs for (&SetOrKeep, F) +where + T: ToInputs, + T: Clone, + F: Fn() -> T, +{ + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let (set_or_keep, default_fn) = self; + + match set_or_keep { + SetOrKeep::Set(this) => { + inputs.append_bool(true); + this.to_inputs(inputs); + } + SetOrKeep::Keep => { + inputs.append_bool(false); + let default = default_fn(); + default.to_inputs(inputs); + } + } + } +} + +impl ToFieldElements for (&SetOrKeep, F) +where + T: ToFieldElements, + T: Clone, + F: Fn() -> T, +{ + fn to_field_elements(&self, fields: &mut Vec) { + let (set_or_keep, default_fn) = self; + + match set_or_keep { + SetOrKeep::Set(this) => { + Boolean::True.to_field_elements(fields); + this.to_field_elements(fields); + } + SetOrKeep::Keep => { + Boolean::False.to_field_elements(fields); + let default = default_fn(); + default.to_field_elements(fields); + } + } + } +} + +impl Check for (&SetOrKeep, F) +where + T: Check, + T: Clone, + F: Fn() -> T, +{ + fn check(&self, w: &mut Witness) { + let (set_or_keep, default_fn) = self; + let value = match set_or_keep { + SetOrKeep::Set(this) => MyCow::Borrow(this), + SetOrKeep::Keep => MyCow::Own(default_fn()), + }; + value.check(w); + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct WithHash { + pub data: T, + pub hash: H, +} + +impl Ord for WithHash { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.hash.cmp(&other.hash) + } +} + +impl PartialOrd for WithHash { + fn partial_cmp(&self, other: &Self) -> Option { + self.hash.partial_cmp(&other.hash) + } +} + +impl Eq for WithHash {} + +impl PartialEq for WithHash { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl std::hash::Hash for WithHash { + fn hash(&self, state: &mut H) { + let Self { data: _, hash } = self; + hash.hash(state); + } +} + +impl ToFieldElements for WithHash { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { data: _, hash } = self; + hash.to_field_elements(fields); + } +} + +impl std::ops::Deref for WithHash { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl WithHash { + pub fn of_data(data: T, hash_data: impl Fn(&T) -> Fp) -> Self { + let hash = hash_data(&data); + Self { data, hash } + } +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Update { + pub app_state: [SetOrKeep; 8], + pub delegate: SetOrKeep, + pub verification_key: SetOrKeep, + pub permissions: SetOrKeep>, + pub zkapp_uri: SetOrKeep, + pub token_symbol: SetOrKeep, + pub timing: SetOrKeep, + pub voting_for: SetOrKeep, +} + +impl ToFieldElements for Update { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + app_state, + delegate, + verification_key, + permissions, + zkapp_uri, + token_symbol, + timing, + voting_for, + } = self; + + for s in app_state { + (s, Fp::zero).to_field_elements(fields); + } + (delegate, CompressedPubKey::empty).to_field_elements(fields); + (&verification_key.map(|w| w.hash()), Fp::zero).to_field_elements(fields); + (permissions, Permissions::empty).to_field_elements(fields); + (&zkapp_uri.map(Some), || Option::<&ZkAppUri>::None).to_field_elements(fields); + (token_symbol, TokenSymbol::default).to_field_elements(fields); + (timing, Timing::dummy).to_field_elements(fields); + (voting_for, VotingFor::dummy).to_field_elements(fields); + } +} + +impl Update { + /// + pub fn noop() -> Self { + Self { + app_state: std::array::from_fn(|_| SetOrKeep::Keep), + delegate: SetOrKeep::Keep, + verification_key: SetOrKeep::Keep, + permissions: SetOrKeep::Keep, + zkapp_uri: SetOrKeep::Keep, + token_symbol: SetOrKeep::Keep, + timing: SetOrKeep::Keep, + voting_for: SetOrKeep::Keep, + } + } + + /// + pub fn dummy() -> Self { + Self::noop() + } + + /// + pub fn gen( + token_account: Option, + zkapp_account: Option, + vk: Option<&VerificationKeyWire>, + permissions_auth: Option, + ) -> Self { + let mut rng = rand::thread_rng(); + + let token_account = token_account.unwrap_or(false); + let zkapp_account = zkapp_account.unwrap_or(false); + + let app_state: [_; 8] = std::array::from_fn(|_| SetOrKeep::gen(|| Fp::rand(&mut rng))); + + let delegate = if !token_account { + SetOrKeep::gen(|| gen_keypair().public.into_compressed()) + } else { + SetOrKeep::Keep + }; + + let verification_key = if zkapp_account { + SetOrKeep::gen(|| match vk { + None => VerificationKeyWire::dummy(), + Some(vk) => vk.clone(), + }) + } else { + SetOrKeep::Keep + }; + + let permissions = match permissions_auth { + None => SetOrKeep::Keep, + Some(auth_tag) => SetOrKeep::Set(Permissions::gen(auth_tag)), + }; + + let zkapp_uri = SetOrKeep::gen(|| { + ZkAppUri::from( + [ + "https://www.example.com", + "https://www.minaprotocol.com", + "https://www.gurgle.com", + "https://faceplant.com", + ] + .choose(&mut rng) + .unwrap() + .to_string() + .into_bytes(), + ) + }); + + let token_symbol = SetOrKeep::gen(|| { + TokenSymbol::from( + ["MINA", "TOKEN1", "TOKEN2", "TOKEN3", "TOKEN4", "TOKEN5"] + .choose(&mut rng) + .unwrap() + .to_string() + .into_bytes(), + ) + }); + + let voting_for = SetOrKeep::gen(|| VotingFor(Fp::rand(&mut rng))); + + let timing = SetOrKeep::Keep; + + Self { + app_state, + delegate, + verification_key, + permissions, + zkapp_uri, + token_symbol, + timing, + voting_for, + } + } +} + +// TODO: This could be std::ops::Range ? +/// +#[derive(Debug, Clone, PartialEq)] +pub struct ClosedInterval { + pub lower: T, + pub upper: T, +} + +impl ClosedInterval +where + T: MinMax, +{ + pub fn min_max() -> Self { + Self { + lower: T::min(), + upper: T::max(), + } + } +} + +impl ToInputs for ClosedInterval +where + T: ToInputs, +{ + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let ClosedInterval { lower, upper } = self; + + lower.to_inputs(inputs); + upper.to_inputs(inputs); + } +} + +impl ToFieldElements for ClosedInterval +where + T: ToFieldElements, +{ + fn to_field_elements(&self, fields: &mut Vec) { + let ClosedInterval { lower, upper } = self; + + lower.to_field_elements(fields); + upper.to_field_elements(fields); + } +} + +impl Check for ClosedInterval +where + T: Check, +{ + fn check(&self, w: &mut Witness) { + let ClosedInterval { lower, upper } = self; + lower.check(w); + upper.check(w); + } +} + +impl ClosedInterval +where + T: PartialOrd, +{ + pub fn is_constant(&self) -> bool { + self.lower == self.upper + } + + /// + pub fn gen(mut fun: F) -> Self + where + F: FnMut() -> T, + { + let a1 = fun(); + let a2 = fun(); + + if a1 <= a2 { + Self { + lower: a1, + upper: a2, + } + } else { + Self { + lower: a2, + upper: a1, + } + } + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub enum OrIgnore { + Check(T), + Ignore, +} + +impl ToInputs for (&OrIgnore, F) +where + T: ToInputs, + F: Fn() -> T, +{ + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let (or_ignore, default_fn) = self; + + match or_ignore { + OrIgnore::Check(this) => { + inputs.append_bool(true); + this.to_inputs(inputs); + } + OrIgnore::Ignore => { + inputs.append_bool(false); + let default = default_fn(); + default.to_inputs(inputs); + } + } + } +} + +impl ToFieldElements for (&OrIgnore, F) +where + T: ToFieldElements, + F: Fn() -> T, +{ + fn to_field_elements(&self, fields: &mut Vec) { + let (or_ignore, default_fn) = self; + + match or_ignore { + OrIgnore::Check(this) => { + Boolean::True.to_field_elements(fields); + this.to_field_elements(fields); + } + OrIgnore::Ignore => { + Boolean::False.to_field_elements(fields); + let default = default_fn(); + default.to_field_elements(fields); + } + }; + } +} + +impl Check for (&OrIgnore, F) +where + T: Check, + F: Fn() -> T, +{ + fn check(&self, w: &mut Witness) { + let (or_ignore, default_fn) = self; + let value = match or_ignore { + OrIgnore::Check(this) => MyCow::Borrow(this), + OrIgnore::Ignore => MyCow::Own(default_fn()), + }; + value.check(w); + } +} + +impl OrIgnore { + /// + pub fn gen(mut fun: F) -> Self + where + F: FnMut() -> T, + { + let mut rng = rand::thread_rng(); + + if rng.gen() { + Self::Check(fun()) + } else { + Self::Ignore + } + } + + pub fn map(&self, fun: F) -> OrIgnore + where + F: Fn(&T) -> V, + { + match self { + OrIgnore::Check(v) => OrIgnore::Check(fun(v)), + OrIgnore::Ignore => OrIgnore::Ignore, + } + } +} + +impl OrIgnore> +where + T: PartialOrd, +{ + /// + pub fn is_constant(&self) -> bool { + match self { + OrIgnore::Check(interval) => interval.lower == interval.upper, + OrIgnore::Ignore => false, + } + } +} + +/// +pub type Hash = OrIgnore; + +/// +pub type EqData = OrIgnore; + +/// +pub type Numeric = OrIgnore>; + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct EpochLedger { + pub hash: Hash, + pub total_currency: Numeric, +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct EpochData { + pub(crate) ledger: EpochLedger, + pub seed: Hash, + pub start_checkpoint: Hash, + pub lock_checkpoint: Hash, + pub epoch_length: Numeric, +} + +#[cfg(feature = "fuzzing")] +impl EpochData { + pub fn new( + ledger: EpochLedger, + seed: Hash, + start_checkpoint: Hash, + lock_checkpoint: Hash, + epoch_length: Numeric, + ) -> Self { + EpochData { + ledger, + seed, + start_checkpoint, + lock_checkpoint, + epoch_length, + } + } + + pub fn ledger_mut(&mut self) -> &mut EpochLedger { + &mut self.ledger + } +} + +impl ToInputs for EpochData { + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let EpochData { + ledger, + seed, + start_checkpoint, + lock_checkpoint, + epoch_length, + } = self; + + { + let EpochLedger { + hash, + total_currency, + } = ledger; + + inputs.append(&(hash, Fp::zero)); + inputs.append(&(total_currency, ClosedInterval::min_max)); + } + + inputs.append(&(seed, Fp::zero)); + inputs.append(&(start_checkpoint, Fp::zero)); + inputs.append(&(lock_checkpoint, Fp::zero)); + inputs.append(&(epoch_length, ClosedInterval::min_max)); + } +} + +impl ToFieldElements for EpochData { + fn to_field_elements(&self, fields: &mut Vec) { + let EpochData { + ledger, + seed, + start_checkpoint, + lock_checkpoint, + epoch_length, + } = self; + + { + let EpochLedger { + hash, + total_currency, + } = ledger; + + (hash, Fp::zero).to_field_elements(fields); + (total_currency, ClosedInterval::min_max).to_field_elements(fields); + } + + (seed, Fp::zero).to_field_elements(fields); + (start_checkpoint, Fp::zero).to_field_elements(fields); + (lock_checkpoint, Fp::zero).to_field_elements(fields); + (epoch_length, ClosedInterval::min_max).to_field_elements(fields); + } +} + +impl Check for EpochData { + fn check(&self, w: &mut Witness) { + let EpochData { + ledger, + seed, + start_checkpoint, + lock_checkpoint, + epoch_length, + } = self; + + { + let EpochLedger { + hash, + total_currency, + } = ledger; + + (hash, Fp::zero).check(w); + (total_currency, ClosedInterval::min_max).check(w); + } + + (seed, Fp::zero).check(w); + (start_checkpoint, Fp::zero).check(w); + (lock_checkpoint, Fp::zero).check(w); + (epoch_length, ClosedInterval::min_max).check(w); + } +} + +impl EpochData { + pub fn gen() -> Self { + let mut rng = rand::thread_rng(); + + EpochData { + ledger: EpochLedger { + hash: OrIgnore::gen(|| Fp::rand(&mut rng)), + total_currency: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), + }, + seed: OrIgnore::gen(|| Fp::rand(&mut rng)), + start_checkpoint: OrIgnore::gen(|| Fp::rand(&mut rng)), + lock_checkpoint: OrIgnore::gen(|| Fp::rand(&mut rng)), + epoch_length: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), + } + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct ZkAppPreconditions { + pub snarked_ledger_hash: Hash, + pub blockchain_length: Numeric, + pub min_window_density: Numeric, + pub total_currency: Numeric, + pub global_slot_since_genesis: Numeric, + pub staking_epoch_data: EpochData, + pub next_epoch_data: EpochData, +} + +impl ZkAppPreconditions { + pub fn zcheck( + &self, + s: &ProtocolStateView, + w: &mut Witness, + ) -> Boolean { + let Self { + snarked_ledger_hash, + blockchain_length, + min_window_density, + total_currency, + global_slot_since_genesis, + staking_epoch_data, + next_epoch_data, + } = self; + + // NOTE: Here the 2nd element in the tuples is the default value of `OrIgnore` + + let epoch_data = + |epoch_data: &EpochData, view: &protocol_state::EpochData, w: &mut Witness| { + let EpochData { + ledger: + EpochLedger { + hash, + total_currency, + }, + seed: _, + start_checkpoint, + lock_checkpoint, + epoch_length, + } = epoch_data; + // Reverse to match OCaml order of the list, while still executing `zcheck` + // in correct order + [ + (epoch_length, ClosedInterval::min_max).zcheck::(&view.epoch_length, w), + (lock_checkpoint, Fp::zero).zcheck::(&view.lock_checkpoint, w), + (start_checkpoint, Fp::zero).zcheck::(&view.start_checkpoint, w), + (total_currency, ClosedInterval::min_max) + .zcheck::(&view.ledger.total_currency, w), + (hash, Fp::zero).zcheck::(&view.ledger.hash, w), + ] + }; + + let next_epoch_data = epoch_data(next_epoch_data, &s.next_epoch_data, w); + let staking_epoch_data = epoch_data(staking_epoch_data, &s.staking_epoch_data, w); + + // Reverse to match OCaml order of the list, while still executing `zcheck` + // in correct order + let bools = [ + (global_slot_since_genesis, ClosedInterval::min_max) + .zcheck::(&s.global_slot_since_genesis, w), + (total_currency, ClosedInterval::min_max).zcheck::(&s.total_currency, w), + (min_window_density, ClosedInterval::min_max).zcheck::(&s.min_window_density, w), + (blockchain_length, ClosedInterval::min_max).zcheck::(&s.blockchain_length, w), + (snarked_ledger_hash, Fp::zero).zcheck::(&s.snarked_ledger_hash, w), + ] + .into_iter() + .rev() + .chain(staking_epoch_data.into_iter().rev()) + .chain(next_epoch_data.into_iter().rev()); + + Ops::boolean_all(bools, w) + } + + /// + pub fn accept() -> Self { + let epoch_data = || EpochData { + ledger: EpochLedger { + hash: OrIgnore::Ignore, + total_currency: OrIgnore::Ignore, + }, + seed: OrIgnore::Ignore, + start_checkpoint: OrIgnore::Ignore, + lock_checkpoint: OrIgnore::Ignore, + epoch_length: OrIgnore::Ignore, + }; + + Self { + snarked_ledger_hash: OrIgnore::Ignore, + blockchain_length: OrIgnore::Ignore, + min_window_density: OrIgnore::Ignore, + total_currency: OrIgnore::Ignore, + global_slot_since_genesis: OrIgnore::Ignore, + staking_epoch_data: epoch_data(), + next_epoch_data: epoch_data(), + } + } +} + +impl ToInputs for ZkAppPreconditions { + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let ZkAppPreconditions { + snarked_ledger_hash, + blockchain_length, + min_window_density, + total_currency, + global_slot_since_genesis, + staking_epoch_data, + next_epoch_data, + } = &self; + + inputs.append(&(snarked_ledger_hash, Fp::zero)); + inputs.append(&(blockchain_length, ClosedInterval::min_max)); + inputs.append(&(min_window_density, ClosedInterval::min_max)); + inputs.append(&(total_currency, ClosedInterval::min_max)); + inputs.append(&(global_slot_since_genesis, ClosedInterval::min_max)); + inputs.append(staking_epoch_data); + inputs.append(next_epoch_data); + } +} + +impl ToFieldElements for ZkAppPreconditions { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + snarked_ledger_hash, + blockchain_length, + min_window_density, + total_currency, + global_slot_since_genesis, + staking_epoch_data, + next_epoch_data, + } = self; + + (snarked_ledger_hash, Fp::zero).to_field_elements(fields); + (blockchain_length, ClosedInterval::min_max).to_field_elements(fields); + (min_window_density, ClosedInterval::min_max).to_field_elements(fields); + (total_currency, ClosedInterval::min_max).to_field_elements(fields); + (global_slot_since_genesis, ClosedInterval::min_max).to_field_elements(fields); + staking_epoch_data.to_field_elements(fields); + next_epoch_data.to_field_elements(fields); + } +} + +impl Check for ZkAppPreconditions { + fn check(&self, w: &mut Witness) { + let Self { + snarked_ledger_hash, + blockchain_length, + min_window_density, + total_currency, + global_slot_since_genesis, + staking_epoch_data, + next_epoch_data, + } = self; + + (snarked_ledger_hash, Fp::zero).check(w); + (blockchain_length, ClosedInterval::min_max).check(w); + (min_window_density, ClosedInterval::min_max).check(w); + (total_currency, ClosedInterval::min_max).check(w); + (global_slot_since_genesis, ClosedInterval::min_max).check(w); + staking_epoch_data.check(w); + next_epoch_data.check(w); + } +} + +/// +fn invalid_public_key() -> CompressedPubKey { + CompressedPubKey { + x: Fp::zero(), + is_odd: false, + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct Account { + pub balance: Numeric, + pub nonce: Numeric, + pub receipt_chain_hash: Hash, // TODO: Should be type `ReceiptChainHash` + pub delegate: EqData, + pub state: [EqData; 8], + pub action_state: EqData, + pub proved_state: EqData, + pub is_new: EqData, +} + +impl Account { + /// + pub fn accept() -> Self { + Self { + balance: Numeric::Ignore, + nonce: Numeric::Ignore, + receipt_chain_hash: Hash::Ignore, + delegate: EqData::Ignore, + state: std::array::from_fn(|_| EqData::Ignore), + action_state: EqData::Ignore, + proved_state: EqData::Ignore, + is_new: EqData::Ignore, + } + } +} + +impl Account { + fn zchecks( + &self, + account: &crate::Account, + new_account: Boolean, + w: &mut Witness, + ) -> Vec<(TransactionFailure, Boolean)> { + use TransactionFailure::*; + + let Self { + balance, + nonce, + receipt_chain_hash, + delegate, + state, + action_state, + proved_state, + is_new, + } = self; + + let zkapp_account = account.zkapp_or_empty(); + let is_new = is_new.map(ToBoolean::to_boolean); + let proved_state = proved_state.map(ToBoolean::to_boolean); + + // NOTE: Here we need to execute all `zcheck` in the exact same order than OCaml + // so we execute them in reverse order (compared to OCaml): OCaml evaluates from right + // to left. + // We then have to reverse the resulting vector, to match OCaml resulting list. + + // NOTE 2: Here the 2nd element in the tuples is the default value of `OrIgnore` + let mut checks: Vec<(TransactionFailure, _)> = [ + ( + AccountIsNewPreconditionUnsatisfied, + (&is_new, || Boolean::False).zcheck::(&new_account, w), + ), + ( + AccountProvedStatePreconditionUnsatisfied, + (&proved_state, || Boolean::False) + .zcheck::(&zkapp_account.proved_state.to_boolean(), w), + ), + ] + .into_iter() + .chain({ + let bools = state + .iter() + .zip(&zkapp_account.app_state) + .enumerate() + // Reversed to enforce right-to-left order application of `f` like in OCaml + .rev() + .map(|(i, (s, account_s))| { + let b = (s, Fp::zero).zcheck::(account_s, w); + (AccountAppStatePreconditionUnsatisfied(i as u64), b) + }) + .collect::>(); + // Not reversed again because we are constructing these results in + // reverse order to match the OCaml evaluation order. + bools.into_iter() + }) + .chain([ + { + let bools: Vec<_> = zkapp_account + .action_state + .iter() + // Reversed to enforce right-to-left order application of `f` like in OCaml + .rev() + .map(|account_s| { + (action_state, ZkAppAccount::empty_action_state).zcheck::(account_s, w) + }) + .collect(); + ( + AccountActionStatePreconditionUnsatisfied, + Ops::boolean_any(bools, w), + ) + }, + ( + AccountDelegatePreconditionUnsatisfied, + (delegate, CompressedPubKey::empty).zcheck::(&*account.delegate_or_empty(), w), + ), + ( + AccountReceiptChainHashPreconditionUnsatisfied, + (receipt_chain_hash, Fp::zero).zcheck::(&account.receipt_chain_hash.0, w), + ), + ( + AccountNoncePreconditionUnsatisfied, + (nonce, ClosedInterval::min_max).zcheck::(&account.nonce, w), + ), + ( + AccountBalancePreconditionUnsatisfied, + (balance, ClosedInterval::min_max).zcheck::(&account.balance, w), + ), + ]) + .collect::>(); + + checks.reverse(); + checks + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct AccountPreconditions(pub Account); + +impl ToInputs for AccountPreconditions { + /// + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let Account { + balance, + nonce, + receipt_chain_hash, + delegate, + state, + action_state, + proved_state, + is_new, + } = &self.0; + + inputs.append(&(balance, ClosedInterval::min_max)); + inputs.append(&(nonce, ClosedInterval::min_max)); + inputs.append(&(receipt_chain_hash, Fp::zero)); + inputs.append(&(delegate, CompressedPubKey::empty)); + for s in state.iter() { + inputs.append(&(s, Fp::zero)); + } + // + inputs.append(&(action_state, ZkAppAccount::empty_action_state)); + inputs.append(&(proved_state, || false)); + inputs.append(&(is_new, || false)); + } +} + +impl ToFieldElements for AccountPreconditions { + fn to_field_elements(&self, fields: &mut Vec) { + let Account { + balance, + nonce, + receipt_chain_hash, + delegate, + state, + action_state, + proved_state, + is_new, + } = &self.0; + + (balance, ClosedInterval::min_max).to_field_elements(fields); + (nonce, ClosedInterval::min_max).to_field_elements(fields); + (receipt_chain_hash, Fp::zero).to_field_elements(fields); + (delegate, CompressedPubKey::empty).to_field_elements(fields); + state.iter().for_each(|s| { + (s, Fp::zero).to_field_elements(fields); + }); + (action_state, ZkAppAccount::empty_action_state).to_field_elements(fields); + (proved_state, || false).to_field_elements(fields); + (is_new, || false).to_field_elements(fields); + } +} + +impl Check for AccountPreconditions { + fn check(&self, w: &mut Witness) { + let Account { + balance, + nonce, + receipt_chain_hash, + delegate, + state, + action_state, + proved_state, + is_new, + } = &self.0; + + (balance, ClosedInterval::min_max).check(w); + (nonce, ClosedInterval::min_max).check(w); + (receipt_chain_hash, Fp::zero).check(w); + (delegate, CompressedPubKey::empty).check(w); + state.iter().for_each(|s| { + (s, Fp::zero).check(w); + }); + (action_state, ZkAppAccount::empty_action_state).check(w); + (proved_state, || false).check(w); + (is_new, || false).check(w); + } +} + +impl AccountPreconditions { + pub fn with_nonce(nonce: Nonce) -> Self { + use OrIgnore::{Check, Ignore}; + AccountPreconditions(Account { + balance: Ignore, + nonce: Check(ClosedInterval { + lower: nonce, + upper: nonce, + }), + receipt_chain_hash: Ignore, + delegate: Ignore, + state: std::array::from_fn(|_| EqData::Ignore), + action_state: Ignore, + proved_state: Ignore, + is_new: Ignore, + }) + } + + pub fn nonce(&self) -> Numeric { + self.0.nonce.clone() + } + + /// + pub fn to_full(&self) -> MyCow<'_, Account> { + MyCow::Borrow(&self.0) + } + + pub fn zcheck( + &self, + new_account: Boolean, + account: &crate::Account, + mut check: Fun, + w: &mut Witness, + ) where + Ops: ZkappCheckOps, + Fun: FnMut(TransactionFailure, Boolean, &mut Witness), + { + let this = self.to_full(); + for (failure, passed) in this.zchecks::(account, new_account, w) { + check(failure, passed, w); + } + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct Preconditions { + pub network: ZkAppPreconditions, + pub account: AccountPreconditions, + pub valid_while: Numeric, +} + +#[cfg(feature = "fuzzing")] +impl Preconditions { + pub fn new( + network: ZkAppPreconditions, + account: AccountPreconditions, + valid_while: Numeric, + ) -> Self { + Self { + network, + account, + valid_while, + } + } + + pub fn network_mut(&mut self) -> &mut ZkAppPreconditions { + &mut self.network + } +} + +impl ToFieldElements for Preconditions { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + network, + account, + valid_while, + } = self; + + network.to_field_elements(fields); + account.to_field_elements(fields); + (valid_while, ClosedInterval::min_max).to_field_elements(fields); + } +} + +impl Check for Preconditions { + fn check(&self, w: &mut Witness) { + let Self { + network, + account, + valid_while, + } = self; + + network.check(w); + account.check(w); + (valid_while, ClosedInterval::min_max).check(w); + } +} + +impl ToInputs for Preconditions { + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let Self { + network, + account, + valid_while, + } = self; + + inputs.append(network); + inputs.append(account); + inputs.append(&(valid_while, ClosedInterval::min_max)); + } +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AuthorizationKind { + NoneGiven, + Signature, + Proof(Fp), // hash +} + +impl AuthorizationKind { + pub fn vk_hash(&self) -> Fp { + match self { + AuthorizationKind::NoneGiven | AuthorizationKind::Signature => { + VerificationKey::dummy().hash() + } + AuthorizationKind::Proof(hash) => *hash, + } + } + + pub fn is_proved(&self) -> bool { + match self { + AuthorizationKind::Proof(_) => true, + AuthorizationKind::NoneGiven => false, + AuthorizationKind::Signature => false, + } + } + + pub fn is_signed(&self) -> bool { + match self { + AuthorizationKind::Proof(_) => false, + AuthorizationKind::NoneGiven => false, + AuthorizationKind::Signature => true, + } + } + + fn to_structured(&self) -> ([bool; 2], Fp) { + // bits: [is_signed, is_proved] + let bits = match self { + AuthorizationKind::NoneGiven => [false, false], + AuthorizationKind::Signature => [true, false], + AuthorizationKind::Proof(_) => [false, true], + }; + let field = self.vk_hash(); + (bits, field) + } +} + +impl ToInputs for AuthorizationKind { + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let (bits, field) = self.to_structured(); + + for bit in bits { + inputs.append_bool(bit); + } + inputs.append_field(field); + } +} + +impl ToFieldElements for AuthorizationKind { + fn to_field_elements(&self, fields: &mut Vec) { + self.to_structured().to_field_elements(fields); + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct Body { + pub public_key: CompressedPubKey, + pub token_id: TokenId, + pub update: Update, + pub balance_change: Signed, + pub increment_nonce: bool, + pub events: Events, + pub actions: Actions, + pub call_data: Fp, + pub preconditions: Preconditions, + pub use_full_commitment: bool, + pub implicit_account_creation_fee: bool, + pub may_use_token: MayUseToken, + pub authorization_kind: AuthorizationKind, +} + +impl ToInputs for Body { + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let Self { + public_key, + token_id, + update, + balance_change, + increment_nonce, + events, + actions, + call_data, + preconditions, + use_full_commitment, + implicit_account_creation_fee, + may_use_token, + authorization_kind, + } = self; + + inputs.append(public_key); + inputs.append(token_id); + + // `Body::update` + { + let Update { + app_state, + delegate, + verification_key, + permissions, + zkapp_uri, + token_symbol, + timing, + voting_for, + } = update; + + for state in app_state { + inputs.append(&(state, Fp::zero)); + } + + inputs.append(&(delegate, CompressedPubKey::empty)); + inputs.append(&(&verification_key.map(|w| w.hash()), Fp::zero)); + inputs.append(&(permissions, Permissions::empty)); + inputs.append(&(&zkapp_uri.map(Some), || Option::<&ZkAppUri>::None)); + inputs.append(&(token_symbol, TokenSymbol::default)); + inputs.append(&(timing, Timing::dummy)); + inputs.append(&(voting_for, VotingFor::dummy)); + } + + inputs.append(balance_change); + inputs.append(increment_nonce); + inputs.append(events); + inputs.append(actions); + inputs.append(call_data); + inputs.append(preconditions); + inputs.append(use_full_commitment); + inputs.append(implicit_account_creation_fee); + inputs.append(may_use_token); + inputs.append(authorization_kind); + } +} + +impl ToFieldElements for Body { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + public_key, + token_id, + update, + balance_change, + increment_nonce, + events, + actions, + call_data, + preconditions, + use_full_commitment, + implicit_account_creation_fee, + may_use_token, + authorization_kind, + } = self; + + public_key.to_field_elements(fields); + token_id.to_field_elements(fields); + update.to_field_elements(fields); + balance_change.to_field_elements(fields); + increment_nonce.to_field_elements(fields); + events.to_field_elements(fields); + actions.to_field_elements(fields); + call_data.to_field_elements(fields); + preconditions.to_field_elements(fields); + use_full_commitment.to_field_elements(fields); + implicit_account_creation_fee.to_field_elements(fields); + may_use_token.to_field_elements(fields); + authorization_kind.to_field_elements(fields); + } +} + +impl Check for Body { + fn check(&self, w: &mut Witness) { + let Self { + public_key: _, + token_id: _, + update: + Update { + app_state: _, + delegate: _, + verification_key: _, + permissions, + zkapp_uri: _, + token_symbol, + timing, + voting_for: _, + }, + balance_change, + increment_nonce: _, + events: _, + actions: _, + call_data: _, + preconditions, + use_full_commitment: _, + implicit_account_creation_fee: _, + may_use_token, + authorization_kind: _, + } = self; + + (permissions, Permissions::empty).check(w); + (token_symbol, TokenSymbol::default).check(w); + (timing, Timing::dummy).check(w); + balance_change.check(w); + + preconditions.check(w); + may_use_token.check(w); + } +} + +impl Body { + pub fn account_id(&self) -> AccountId { + let Self { + public_key, + token_id, + .. + } = self; + AccountId::create(public_key.clone(), token_id.clone()) + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct BodySimple { + pub public_key: CompressedPubKey, + pub token_id: TokenId, + pub update: Update, + pub balance_change: Signed, + pub increment_nonce: bool, + pub events: Events, + pub actions: Actions, + pub call_data: Fp, + pub call_depth: usize, + pub preconditions: Preconditions, + pub use_full_commitment: bool, + pub implicit_account_creation_fee: bool, + pub may_use_token: MayUseToken, + pub authorization_kind: AuthorizationKind, +} + +/// Notes: +/// The type in OCaml is this one: +/// +/// +/// For now we use the type from `mina_p2p_messages`, but we need to use our own. +/// Lots of inner types are (BigInt, Bigint) which should be replaced with `Pallas<_>` etc. +/// Also, in OCaml it has custom `{to/from}_binable` implementation. +/// +/// +pub type SideLoadedProof = Arc; + +/// Authorization methods for zkApp account updates. +/// +/// Defines how an account update is authorized to modify an account's state. +/// +/// +#[derive(Clone, PartialEq)] +pub enum Control { + /// Verified by a zero-knowledge proof against the account's verification + /// key. + Proof(SideLoadedProof), + /// Signed by the account's private key. + Signature(Signature), + /// No authorization (only valid for certain operations). + NoneGiven, +} + +impl std::fmt::Debug for Control { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Proof(_) => f.debug_tuple("Proof").field(&"_").finish(), + Self::Signature(arg0) => f.debug_tuple("Signature").field(arg0).finish(), + Self::NoneGiven => write!(f, "NoneGiven"), + } + } +} + +impl Control { + /// + pub fn tag(&self) -> crate::ControlTag { + match self { + Control::Proof(_) => crate::ControlTag::Proof, + Control::Signature(_) => crate::ControlTag::Signature, + Control::NoneGiven => crate::ControlTag::NoneGiven, + } + } + + pub fn dummy_of_tag(tag: ControlTag) -> Self { + match tag { + ControlTag::Proof => Self::Proof(dummy::sideloaded_proof()), + ControlTag::Signature => Self::Signature(Signature::dummy()), + ControlTag::NoneGiven => Self::NoneGiven, + } + } + + pub fn dummy(&self) -> Self { + Self::dummy_of_tag(self.tag()) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum MayUseToken { + /// No permission to use any token other than the default Mina + /// token + No, + /// Has permission to use the token owned by the direct parent of + /// this account update, which may be inherited by child account + /// updates. + ParentsOwnToken, + /// Inherit the token permission available to the parent. + InheritFromParent, +} + +impl MayUseToken { + pub fn parents_own_token(&self) -> bool { + matches!(self, Self::ParentsOwnToken) + } + + pub fn inherit_from_parent(&self) -> bool { + matches!(self, Self::InheritFromParent) + } + + fn to_bits(&self) -> [bool; 2] { + // [ parents_own_token; inherit_from_parent ] + match self { + MayUseToken::No => [false, false], + MayUseToken::ParentsOwnToken => [true, false], + MayUseToken::InheritFromParent => [false, true], + } + } +} + +impl ToInputs for MayUseToken { + fn to_inputs(&self, inputs: &mut Inputs) { + for bit in self.to_bits() { + inputs.append_bool(bit); + } + } +} + +impl ToFieldElements for MayUseToken { + fn to_field_elements(&self, fields: &mut Vec) { + for bit in self.to_bits() { + bit.to_field_elements(fields); + } + } +} + +impl Check for MayUseToken { + fn check(&self, w: &mut Witness) { + use crate::proofs::field::field; + + let [parents_own_token, inherit_from_parent] = self.to_bits(); + let [parents_own_token, inherit_from_parent] = [ + parents_own_token.to_boolean(), + inherit_from_parent.to_boolean(), + ]; + + let sum = parents_own_token.to_field::() + inherit_from_parent.to_field::(); + let _sum_squared = field::mul(sum, sum, w); + } +} + +pub struct CheckAuthorizationResult { + pub proof_verifies: Bool, + pub signature_verifies: Bool, +} + +/// +pub type AccountUpdate = AccountUpdateSkeleton; + +#[derive(Debug, Clone, PartialEq)] +pub struct AccountUpdateSkeleton { + pub body: Body, + pub authorization: Control, +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct AccountUpdateSimple { + pub body: BodySimple, + pub authorization: Control, +} + +impl ToInputs for AccountUpdate { + /// + fn to_inputs(&self, inputs: &mut Inputs) { + // Only the body is used + let Self { + body, + authorization: _, + } = self; + + inputs.append(body); + } +} + +impl AccountUpdate { + /// + /// + pub fn of_fee_payer(fee_payer: FeePayer) -> Self { + let FeePayer { + body: + FeePayerBody { + public_key, + fee, + valid_until, + nonce, + }, + authorization, + } = fee_payer; + + Self { + body: Body { + public_key, + token_id: TokenId::default(), + update: Update::noop(), + balance_change: Signed { + magnitude: Amount::of_fee(&fee), + sgn: Sgn::Neg, + }, + increment_nonce: true, + events: Events::empty(), + actions: Actions::empty(), + call_data: Fp::zero(), + preconditions: Preconditions { + network: { + let mut network = ZkAppPreconditions::accept(); + + let valid_util = valid_until.unwrap_or_else(Slot::max); + network.global_slot_since_genesis = OrIgnore::Check(ClosedInterval { + lower: Slot::zero(), + upper: valid_util, + }); + + network + }, + account: AccountPreconditions::with_nonce(nonce), + valid_while: Numeric::Ignore, + }, + use_full_commitment: true, + authorization_kind: AuthorizationKind::Signature, + implicit_account_creation_fee: true, + may_use_token: MayUseToken::No, + }, + authorization: Control::Signature(authorization), + } + } + + /// + pub fn account_id(&self) -> AccountId { + AccountId::new(self.body.public_key.clone(), self.body.token_id.clone()) + } + + /// + pub fn digest(&self) -> Fp { + self.hash_with_param(mina_core::NetworkConfig::global().account_update_hash_param) + } + + pub fn timing(&self) -> SetOrKeep { + self.body.update.timing.clone() + } + + pub fn may_use_parents_own_token(&self) -> bool { + self.body.may_use_token.parents_own_token() + } + + pub fn may_use_token_inherited_from_parent(&self) -> bool { + self.body.may_use_token.inherit_from_parent() + } + + pub fn public_key(&self) -> CompressedPubKey { + self.body.public_key.clone() + } + + pub fn token_id(&self) -> TokenId { + self.body.token_id.clone() + } + + pub fn increment_nonce(&self) -> bool { + self.body.increment_nonce + } + + pub fn implicit_account_creation_fee(&self) -> bool { + self.body.implicit_account_creation_fee + } + + // commitment and calls argument are ignored here, only used in the transaction snark + pub fn check_authorization( + &self, + _will_succeed: bool, + _commitment: Fp, + _calls: CallForest, + ) -> CheckAuthorizationResult { + match self.authorization { + Control::Signature(_) => CheckAuthorizationResult { + proof_verifies: false, + signature_verifies: true, + }, + Control::Proof(_) => CheckAuthorizationResult { + proof_verifies: true, + signature_verifies: false, + }, + Control::NoneGiven => CheckAuthorizationResult { + proof_verifies: false, + signature_verifies: false, + }, + } + } + + pub fn permissions(&self) -> SetOrKeep> { + self.body.update.permissions.clone() + } + + pub fn app_state(&self) -> [SetOrKeep; 8] { + self.body.update.app_state.clone() + } + + pub fn zkapp_uri(&self) -> SetOrKeep { + self.body.update.zkapp_uri.clone() + } + + /* + pub fn token_symbol(&self) -> SetOrKeep<[u8; 6]> { + self.body.update.token_symbol.clone() + } + */ + + pub fn token_symbol(&self) -> SetOrKeep { + self.body.update.token_symbol.clone() + } + + pub fn delegate(&self) -> SetOrKeep { + self.body.update.delegate.clone() + } + + pub fn voting_for(&self) -> SetOrKeep { + self.body.update.voting_for.clone() + } + + pub fn verification_key(&self) -> SetOrKeep { + self.body.update.verification_key.clone() + } + + pub fn valid_while_precondition(&self) -> OrIgnore> { + self.body.preconditions.valid_while.clone() + } + + pub fn actions(&self) -> Actions { + self.body.actions.clone() + } + + pub fn balance_change(&self) -> Signed { + self.body.balance_change + } + pub fn use_full_commitment(&self) -> bool { + self.body.use_full_commitment + } + + pub fn protocol_state_precondition(&self) -> ZkAppPreconditions { + self.body.preconditions.network.clone() + } + + pub fn account_precondition(&self) -> AccountPreconditions { + self.body.preconditions.account.clone() + } + + pub fn is_proved(&self) -> bool { + match &self.body.authorization_kind { + AuthorizationKind::Proof(_) => true, + AuthorizationKind::Signature | AuthorizationKind::NoneGiven => false, + } + } + + pub fn is_signed(&self) -> bool { + match &self.body.authorization_kind { + AuthorizationKind::Signature => true, + AuthorizationKind::Proof(_) | AuthorizationKind::NoneGiven => false, + } + } + + /// + pub fn verification_key_hash(&self) -> Option { + match &self.body.authorization_kind { + AuthorizationKind::Proof(vk_hash) => Some(*vk_hash), + _ => None, + } + } + + /// + pub fn of_simple(simple: &AccountUpdateSimple) -> Self { + let AccountUpdateSimple { + body: + BodySimple { + public_key, + token_id, + update, + balance_change, + increment_nonce, + events, + actions, + call_data, + call_depth: _, + preconditions, + use_full_commitment, + implicit_account_creation_fee, + may_use_token, + authorization_kind, + }, + authorization, + } = simple.clone(); + + Self { + body: Body { + public_key, + token_id, + update, + balance_change, + increment_nonce, + events, + actions, + call_data, + preconditions, + use_full_commitment, + implicit_account_creation_fee, + may_use_token, + authorization_kind, + }, + authorization, + } + } + + /// Usage: Random `AccountUpdate` to compare hashes with OCaml + pub fn rand() -> Self { + let mut rng = rand::thread_rng(); + let rng = &mut rng; + + Self { + body: Body { + public_key: gen_compressed(), + token_id: TokenId(Fp::rand(rng)), + update: Update { + app_state: std::array::from_fn(|_| SetOrKeep::gen(|| Fp::rand(rng))), + delegate: SetOrKeep::gen(gen_compressed), + verification_key: SetOrKeep::gen(VerificationKeyWire::gen), + permissions: SetOrKeep::gen(|| { + let auth_tag = [ + ControlTag::NoneGiven, + ControlTag::Proof, + ControlTag::Signature, + ] + .choose(rng) + .unwrap(); + + Permissions::gen(*auth_tag) + }), + zkapp_uri: SetOrKeep::gen(ZkAppUri::gen), + token_symbol: SetOrKeep::gen(TokenSymbol::gen), + timing: SetOrKeep::gen(|| Timing { + initial_minimum_balance: rng.gen(), + cliff_time: rng.gen(), + cliff_amount: rng.gen(), + vesting_period: rng.gen(), + vesting_increment: rng.gen(), + }), + voting_for: SetOrKeep::gen(|| VotingFor(Fp::rand(rng))), + }, + balance_change: Signed::gen(), + increment_nonce: rng.gen(), + events: Events(gen_events()), + actions: Actions(gen_events()), + call_data: Fp::rand(rng), + preconditions: Preconditions { + network: ZkAppPreconditions { + snarked_ledger_hash: OrIgnore::gen(|| Fp::rand(rng)), + blockchain_length: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), + min_window_density: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), + total_currency: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), + global_slot_since_genesis: OrIgnore::gen(|| { + ClosedInterval::gen(|| rng.gen()) + }), + staking_epoch_data: EpochData::gen(), + next_epoch_data: EpochData::gen(), + }, + account: AccountPreconditions(Account { + balance: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), + nonce: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), + receipt_chain_hash: OrIgnore::gen(|| Fp::rand(rng)), + delegate: OrIgnore::gen(gen_compressed), + state: std::array::from_fn(|_| OrIgnore::gen(|| Fp::rand(rng))), + action_state: OrIgnore::gen(|| Fp::rand(rng)), + proved_state: OrIgnore::gen(|| rng.gen()), + is_new: OrIgnore::gen(|| rng.gen()), + }), + valid_while: OrIgnore::gen(|| ClosedInterval::gen(|| rng.gen())), + }, + use_full_commitment: rng.gen(), + implicit_account_creation_fee: rng.gen(), + may_use_token: { + match MayUseToken::No { + MayUseToken::No => (), + MayUseToken::ParentsOwnToken => (), + MayUseToken::InheritFromParent => (), + }; + + [ + MayUseToken::No, + MayUseToken::InheritFromParent, + MayUseToken::ParentsOwnToken, + ] + .choose(rng) + .cloned() + .unwrap() + }, + authorization_kind: { + match AuthorizationKind::NoneGiven { + AuthorizationKind::NoneGiven => (), + AuthorizationKind::Signature => (), + AuthorizationKind::Proof(_) => (), + }; + + [ + AuthorizationKind::NoneGiven, + AuthorizationKind::Signature, + AuthorizationKind::Proof(Fp::rand(rng)), + ] + .choose(rng) + .cloned() + .unwrap() + }, + }, + authorization: { + match Control::NoneGiven { + Control::Proof(_) => (), + Control::Signature(_) => (), + Control::NoneGiven => (), + }; + + match rng.gen_range(0..3) { + 0 => Control::NoneGiven, + 1 => Control::Signature(Signature::dummy()), + _ => Control::Proof(dummy::sideloaded_proof()), + } + }, + } + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct Tree { + pub account_update: AccUpdate, + pub account_update_digest: MutableFp, + pub calls: CallForest, +} + +impl Tree { + // TODO: Cache this result somewhere ? + pub fn digest(&self) -> Fp { + let stack_hash = match self.calls.0.first() { + Some(e) => e.stack_hash.get().expect("Must call `ensure_hashed`"), + None => Fp::zero(), + }; + let account_update_digest = self.account_update_digest.get().unwrap(); + hash_with_kimchi( + &MINA_ACCOUNT_UPDATE_NODE, + &[account_update_digest, stack_hash], + ) + } + + fn fold(&self, init: Vec, f: &mut F) -> Vec + where + F: FnMut(Vec, &AccUpdate) -> Vec, + { + self.calls.fold(f(init, &self.account_update), f) + } +} + +/// +#[derive(Debug, Clone)] +pub struct WithStackHash { + pub elt: Tree, + pub stack_hash: MutableFp, +} + +impl PartialEq for WithStackHash { + fn eq(&self, other: &Self) -> bool { + self.elt == other.elt && self.stack_hash == other.stack_hash + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct CallForest(pub Vec>); + +impl Default for CallForest { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone)] +struct CallForestContext { + caller: TokenId, + this: TokenId, +} + +pub trait AccountUpdateRef { + fn account_update_ref(&self) -> &AccountUpdate; +} +impl AccountUpdateRef for AccountUpdate { + fn account_update_ref(&self) -> &AccountUpdate { + self + } +} +impl AccountUpdateRef for (AccountUpdate, T) { + fn account_update_ref(&self) -> &AccountUpdate { + let (this, _) = self; + this + } +} +impl AccountUpdateRef for AccountUpdateSimple { + fn account_update_ref(&self) -> &AccountUpdate { + // AccountUpdateSimple are first converted into `AccountUpdate` + unreachable!() + } +} + +impl CallForest { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn empty() -> Self { + Self::new() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + // In OCaml push/pop to the head is cheap because they work with lists. + // In Rust we use vectors so we will push/pop to the tail. + // To work with the elements as if they were in the original order we need to iterate backwards + pub fn iter(&self) -> impl Iterator> { + self.0.iter() //.rev() + } + // Warning: Update this if we ever change the order + pub fn first(&self) -> Option<&WithStackHash> { + self.0.first() + } + // Warning: Update this if we ever change the order + pub fn tail(&self) -> Option<&[WithStackHash]> { + self.0.get(1..) + } + + pub fn hash(&self) -> Fp { + self.ensure_hashed(); + /* + for x in self.0.iter() { + println!("hash: {:?}", x.stack_hash); + } + */ + + if let Some(x) = self.first() { + x.stack_hash.get().unwrap() // Never fail, we called `ensure_hashed` + } else { + Fp::zero() + } + } + + fn cons_tree(&self, tree: Tree) -> Self { + self.ensure_hashed(); + + let hash = tree.digest(); + let h_tl = self.hash(); + + let stack_hash = hash_with_kimchi(&MINA_ACCOUNT_UPDATE_CONS, &[hash, h_tl]); + let node = WithStackHash:: { + elt: tree, + stack_hash: MutableFp::new(stack_hash), + }; + let mut forest = Vec::with_capacity(self.0.len() + 1); + forest.push(node); + forest.extend(self.0.iter().cloned()); + + Self(forest) + } + + pub fn pop_exn(&self) -> ((AccUpdate, CallForest), CallForest) { + if self.0.is_empty() { + panic!() + } + + let Tree:: { + account_update, + calls, + .. + } = self.0[0].elt.clone(); + ( + (account_update, calls), + CallForest(Vec::from_iter(self.0[1..].iter().cloned())), + ) + } + + /// + fn fold_impl<'a, A, F>(&'a self, init: A, fun: &mut F) -> A + where + F: FnMut(A, &'a AccUpdate) -> A, + { + let mut accum = init; + for elem in self.iter() { + accum = fun(accum, &elem.elt.account_update); + accum = elem.elt.calls.fold_impl(accum, fun); + } + accum + } + + pub fn fold<'a, A, F>(&'a self, init: A, mut fun: F) -> A + where + F: FnMut(A, &'a AccUpdate) -> A, + { + self.fold_impl(init, &mut fun) + } + + pub fn exists<'a, F>(&'a self, mut fun: F) -> bool + where + F: FnMut(&'a AccUpdate) -> bool, + { + self.fold(false, |acc, x| acc || fun(x)) + } + + fn map_to_impl( + &self, + fun: &F, + ) -> CallForest + where + F: Fn(&AccUpdate) -> AnotherAccUpdate, + { + CallForest::( + self.iter() + .map(|item| WithStackHash:: { + elt: Tree:: { + account_update: fun(&item.elt.account_update), + account_update_digest: item.elt.account_update_digest.clone(), + calls: item.elt.calls.map_to_impl(fun), + }, + stack_hash: item.stack_hash.clone(), + }) + .collect(), + ) + } + + #[must_use] + pub fn map_to( + &self, + fun: F, + ) -> CallForest + where + F: Fn(&AccUpdate) -> AnotherAccUpdate, + { + self.map_to_impl(&fun) + } + + fn map_with_trees_to_impl( + &self, + fun: &F, + ) -> CallForest + where + F: Fn(&AccUpdate, &Tree) -> AnotherAccUpdate, + { + CallForest::( + self.iter() + .map(|item| { + let account_update = fun(&item.elt.account_update, &item.elt); + + WithStackHash:: { + elt: Tree:: { + account_update, + account_update_digest: item.elt.account_update_digest.clone(), + calls: item.elt.calls.map_with_trees_to_impl(fun), + }, + stack_hash: item.stack_hash.clone(), + } + }) + .collect(), + ) + } + + #[must_use] + pub fn map_with_trees_to( + &self, + fun: F, + ) -> CallForest + where + F: Fn(&AccUpdate, &Tree) -> AnotherAccUpdate, + { + self.map_with_trees_to_impl(&fun) + } + + fn try_map_to_impl( + &self, + fun: &mut F, + ) -> Result, E> + where + F: FnMut(&AccUpdate) -> Result, + { + Ok(CallForest::( + self.iter() + .map(|item| { + Ok(WithStackHash:: { + elt: Tree:: { + account_update: fun(&item.elt.account_update)?, + account_update_digest: item.elt.account_update_digest.clone(), + calls: item.elt.calls.try_map_to_impl(fun)?, + }, + stack_hash: item.stack_hash.clone(), + }) + }) + .collect::>()?, + )) + } + + pub fn try_map_to( + &self, + mut fun: F, + ) -> Result, E> + where + F: FnMut(&AccUpdate) -> Result, + { + self.try_map_to_impl(&mut fun) + } + + fn to_account_updates_impl(&self, accounts: &mut Vec) { + // TODO: Check iteration order in OCaml + for elem in self.iter() { + accounts.push(elem.elt.account_update.clone()); + elem.elt.calls.to_account_updates_impl(accounts); + } + } + + /// + pub fn to_account_updates(&self) -> Vec { + let mut accounts = Vec::with_capacity(128); + self.to_account_updates_impl(&mut accounts); + accounts + } + + fn to_zkapp_command_with_hashes_list_impl(&self, output: &mut Vec<(AccUpdate, Fp)>) { + self.iter().for_each(|item| { + let WithStackHash { elt, stack_hash } = item; + let Tree { + account_update, + account_update_digest: _, + calls, + } = elt; + output.push((account_update.clone(), stack_hash.get().unwrap())); // Never fail, we called `ensure_hashed` + calls.to_zkapp_command_with_hashes_list_impl(output); + }); + } + + pub fn to_zkapp_command_with_hashes_list(&self) -> Vec<(AccUpdate, Fp)> { + self.ensure_hashed(); + + let mut output = Vec::with_capacity(128); + self.to_zkapp_command_with_hashes_list_impl(&mut output); + output + } + + pub fn ensure_hashed(&self) { + let Some(first) = self.first() else { + return; + }; + if first.stack_hash.get().is_none() { + self.accumulate_hashes(); + } + } +} + +impl CallForest { + /// + pub fn accumulate_hashes(&self) { + /// + fn cons(hash: Fp, h_tl: Fp) -> Fp { + hash_with_kimchi(&MINA_ACCOUNT_UPDATE_CONS, &[hash, h_tl]) + } + + /// + fn hash( + elem: Option<&WithStackHash>, + ) -> Fp { + match elem { + Some(next) => next.stack_hash.get().unwrap(), // Never fail, we hash them from reverse below + None => Fp::zero(), + } + } + + // We traverse the list in reverse here (to get same behavior as OCaml recursivity) + // Note that reverse here means 0 to last, see `CallForest::iter` for explaination + // + // We use indexes to make the borrow checker happy + + for index in (0..self.0.len()).rev() { + let elem = &self.0[index]; + let WithStackHash { + elt: + Tree:: { + account_update, + account_update_digest, + calls, + .. + }, + .. + } = elem; + + calls.accumulate_hashes(); + account_update_digest.set(account_update.account_update_ref().digest()); + + let node_hash = elem.elt.digest(); + let hash = hash(self.0.get(index + 1)); + + self.0[index].stack_hash.set(cons(node_hash, hash)); + } + } +} + +impl CallForest { + pub fn cons( + &self, + calls: Option>, + account_update: AccountUpdate, + ) -> Self { + let account_update_digest = account_update.digest(); + + let tree = Tree:: { + account_update, + account_update_digest: MutableFp::new(account_update_digest), + calls: calls.unwrap_or_else(|| CallForest(Vec::new())), + }; + self.cons_tree(tree) + } + + pub fn accumulate_hashes_predicated(&mut self) { + // Note: There seems to be no difference with `accumulate_hashes` + self.accumulate_hashes(); + } + + /// + pub fn of_wire(&mut self, _wired: &[MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA]) { + self.accumulate_hashes(); + } + + /// + pub fn to_wire(&self, _wired: &mut [MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA]) { + // self.remove_callers(wired); + } +} + +impl CallForest<(AccountUpdate, Option>)> { + // Don't implement `{from,to}_wire` because the binprot types contain the hashes + + // /// + // pub fn of_wire( + // &mut self, + // _wired: &[v2::MinaBaseZkappCommandVerifiableStableV1AccountUpdatesA], + // ) { + // self.accumulate_hashes(&|(account_update, _vk_opt)| account_update.digest()); + // } + + // /// + // pub fn to_wire( + // &self, + // _wired: &mut [MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA], + // ) { + // // self.remove_callers(wired); + // } +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FeePayerBody { + pub public_key: CompressedPubKey, + pub fee: Fee, + pub valid_until: Option, + pub nonce: Nonce, +} + +/// +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FeePayer { + pub body: FeePayerBody, + pub authorization: Signature, +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct ZkAppCommand { + pub fee_payer: FeePayer, + pub account_updates: CallForest, + pub memo: Memo, +} + +#[derive(Debug, Clone, PartialEq, Hash, Eq, Ord, PartialOrd)] +pub enum AccessedOrNot { + Accessed, + NotAccessed, +} + +impl ZkAppCommand { + pub fn fee_payer(&self) -> AccountId { + let public_key = self.fee_payer.body.public_key.clone(); + AccountId::new(public_key, self.fee_token()) + } + + pub fn fee_token(&self) -> TokenId { + TokenId::default() + } + + pub fn fee(&self) -> Fee { + self.fee_payer.body.fee + } + + pub fn fee_excess(&self) -> FeeExcess { + FeeExcess::of_single((self.fee_token(), Signed::::of_unsigned(self.fee()))) + } + + fn fee_payer_account_update(&self) -> &FeePayer { + let Self { fee_payer, .. } = self; + fee_payer + } + + pub fn applicable_at_nonce(&self) -> Nonce { + self.fee_payer_account_update().body.nonce + } + + pub fn weight(&self) -> u64 { + let Self { + fee_payer, + account_updates, + memo, + } = self; + [ + zkapp_weight::fee_payer(fee_payer), + zkapp_weight::account_updates(account_updates), + zkapp_weight::memo(memo), + ] + .iter() + .sum() + } + + pub fn has_zero_vesting_period(&self) -> bool { + self.account_updates + .exists(|account_update| match &account_update.body.update.timing { + SetOrKeep::Keep => false, + SetOrKeep::Set(Timing { vesting_period, .. }) => vesting_period.is_zero(), + }) + } + + pub fn is_incompatible_version(&self) -> bool { + self.account_updates.exists(|account_update| { + match &account_update.body.update.permissions { + SetOrKeep::Keep => false, + SetOrKeep::Set(Permissions { + set_verification_key, + .. + }) => { + let SetVerificationKey { + auth: _, + txn_version, + } = set_verification_key; + *txn_version != crate::TXN_VERSION_CURRENT + } + } + }) + } + + fn zkapp_cost( + proof_segments: usize, + signed_single_segments: usize, + signed_pair_segments: usize, + ) -> f64 { + // (*10.26*np + 10.08*n2 + 9.14*n1 < 69.45*) + let GenesisConstant { + zkapp_proof_update_cost: proof_cost, + zkapp_signed_pair_update_cost: signed_pair_cost, + zkapp_signed_single_update_cost: signed_single_cost, + .. + } = GENESIS_CONSTANT; + + (proof_cost * (proof_segments as f64)) + + (signed_pair_cost * (signed_pair_segments as f64)) + + (signed_single_cost * (signed_single_segments as f64)) + } + + /// Zkapp_command transactions are filtered using this predicate + /// - when adding to the transaction pool + /// - in incoming blocks + pub fn valid_size(&self) -> Result<(), String> { + use crate::proofs::zkapp::group::{SegmentBasic, ZkappCommandIntermediateState}; + + let Self { + account_updates, + fee_payer: _, + memo: _, + } = self; + + let events_elements = |events: &[Event]| -> usize { events.iter().map(Event::len).sum() }; + + let mut n_account_updates = 0; + let (mut num_event_elements, mut num_action_elements) = (0, 0); + + account_updates.fold((), |_, account_update| { + num_event_elements += events_elements(account_update.body.events.events()); + num_action_elements += events_elements(account_update.body.actions.events()); + n_account_updates += 1; + }); + + let group = std::iter::repeat(((), (), ())) + .take(n_account_updates + 2) // + 2 to prepend two. See OCaml + .collect::>(); + + let groups = crate::proofs::zkapp::group::group_by_zkapp_command_rev::<_, (), (), ()>( + [self], + vec![vec![((), (), ())], group], + ); + + let (mut proof_segments, mut signed_single_segments, mut signed_pair_segments) = (0, 0, 0); + + for ZkappCommandIntermediateState { spec, .. } in &groups { + match spec { + SegmentBasic::Proved => proof_segments += 1, + SegmentBasic::OptSigned => signed_single_segments += 1, + SegmentBasic::OptSignedOptSigned => signed_pair_segments += 1, + } + } + + let GenesisConstant { + zkapp_transaction_cost_limit: cost_limit, + max_event_elements, + max_action_elements, + .. + } = GENESIS_CONSTANT; + + let zkapp_cost_within_limit = + Self::zkapp_cost(proof_segments, signed_single_segments, signed_pair_segments) + < cost_limit; + let valid_event_elements = num_event_elements <= max_event_elements; + let valid_action_elements = num_action_elements <= max_action_elements; + + if zkapp_cost_within_limit && valid_event_elements && valid_action_elements { + return Ok(()); + } + + let err = [ + (zkapp_cost_within_limit, "zkapp transaction too expensive"), + (valid_event_elements, "too many event elements"), + (valid_action_elements, "too many action elements"), + ] + .iter() + .filter(|(b, _s)| !b) + .map(|(_b, s)| s) + .join(";"); + + Err(err) + } + + /// + pub fn account_access_statuses( + &self, + status: &TransactionStatus, + ) -> Vec<(AccountId, AccessedOrNot)> { + use AccessedOrNot::*; + use TransactionStatus::*; + + // always `Accessed` for fee payer + let init = vec![(self.fee_payer(), Accessed)]; + + let status_sym = match status { + Applied => Accessed, + Failed(_) => NotAccessed, + }; + + let ids = self + .account_updates + .fold(init, |mut accum, account_update| { + accum.push((account_update.account_id(), status_sym.clone())); + accum + }); + // WARNING: the code previous to merging latest changes wasn't doing the "rev()" call. Check this in case of errors. + ids.iter() + .unique() /*.rev()*/ + .cloned() + .collect() + } + + /// + pub fn accounts_referenced(&self) -> Vec { + self.account_access_statuses(&TransactionStatus::Applied) + .into_iter() + .map(|(id, _status)| id) + .collect() + } + + /// + pub fn of_verifiable(verifiable: verifiable::ZkAppCommand) -> Self { + Self { + fee_payer: verifiable.fee_payer, + account_updates: verifiable.account_updates.map_to(|(acc, _)| acc.clone()), + memo: verifiable.memo, + } + } + + /// + pub fn account_updates_hash(&self) -> Fp { + self.account_updates.hash() + } + + /// + pub fn extract_vks(&self) -> Vec<(AccountId, VerificationKeyWire)> { + self.account_updates + .fold(Vec::with_capacity(256), |mut acc, p| { + if let SetOrKeep::Set(vk) = &p.body.update.verification_key { + acc.push((p.account_id(), vk.clone())); + }; + acc + }) + } + + pub fn all_account_updates(&self) -> CallForest { + let p = &self.fee_payer; + + let mut fee_payer = AccountUpdate::of_fee_payer(p.clone()); + fee_payer.authorization = Control::Signature(p.authorization.clone()); + + self.account_updates.cons(None, fee_payer) + } + + pub fn all_account_updates_list(&self) -> Vec { + let mut account_updates = Vec::with_capacity(16); + account_updates.push(AccountUpdate::of_fee_payer(self.fee_payer.clone())); + + self.account_updates.fold(account_updates, |mut acc, u| { + acc.push(u.clone()); + acc + }) + } + + pub fn commitment(&self) -> TransactionCommitment { + let account_updates_hash = self.account_updates_hash(); + TransactionCommitment::create(account_updates_hash) + } +} + +pub mod verifiable { + use mina_p2p_messages::v2::MinaBaseZkappCommandVerifiableStableV1; + + use super::*; + + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] + #[serde(try_from = "MinaBaseZkappCommandVerifiableStableV1")] + #[serde(into = "MinaBaseZkappCommandVerifiableStableV1")] + pub struct ZkAppCommand { + pub fee_payer: FeePayer, + pub account_updates: CallForest<(AccountUpdate, Option)>, + pub memo: Memo, + } + + fn ok_if_vk_hash_expected( + got: VerificationKeyWire, + expected: Fp, + ) -> Result { + if got.hash() == expected { + return Ok(got.clone()); + } + Err(format!( + "Expected vk hash doesn't match hash in vk we received\ + expected: {:?}\ + got: {:?}", + expected, got + )) + } + + pub fn find_vk_via_ledger( + ledger: L, + expected_vk_hash: Fp, + account_id: &AccountId, + ) -> Result + where + L: LedgerIntf + Clone, + { + let vk = ledger + .location_of_account(account_id) + .and_then(|location| ledger.get(&location)) + .and_then(|account| { + account + .zkapp + .as_ref() + .and_then(|zkapp| zkapp.verification_key.clone()) + }); + + match vk { + Some(vk) => ok_if_vk_hash_expected(vk, expected_vk_hash), + None => Err(format!( + "No verification key found for proved account update\ + account_id: {:?}", + account_id + )), + } + } + + fn check_authorization(p: &AccountUpdate) -> Result<(), String> { + use AuthorizationKind as AK; + use Control as C; + + match (&p.authorization, &p.body.authorization_kind) { + (C::NoneGiven, AK::NoneGiven) + | (C::Proof(_), AK::Proof(_)) + | (C::Signature(_), AK::Signature) => Ok(()), + _ => Err(format!( + "Authorization kind does not match the authorization\ + expected={:#?}\ + got={:#?}", + p.body.authorization_kind, p.authorization + )), + } + } + + /// Ensures that there's a verification_key available for all account_updates + /// and creates a valid command associating the correct keys with each + /// account_id. + /// + /// If an account_update replaces the verification_key (or deletes it), + /// subsequent account_updates use the replaced key instead of looking in the + /// ledger for the key (ie set by a previous transaction). + pub fn create( + zkapp: &super::ZkAppCommand, + is_failed: bool, + find_vk: impl Fn(Fp, &AccountId) -> Result, + ) -> Result { + let super::ZkAppCommand { + fee_payer, + account_updates, + memo, + } = zkapp; + + let mut tbl = HashMap::with_capacity(128); + // Keep track of the verification keys that have been set so far + // during this transaction. + let mut vks_overridden: HashMap> = + HashMap::with_capacity(128); + + let account_updates = account_updates.try_map_to(|p| { + let account_id = p.account_id(); + + check_authorization(p)?; + + let result = match (&p.body.authorization_kind, is_failed) { + (AuthorizationKind::Proof(vk_hash), false) => { + let prioritized_vk = { + // only lookup _past_ vk setting, ie exclude the new one we + // potentially set in this account_update (use the non-' + // vks_overrided) . + + match vks_overridden.get(&account_id) { + Some(Some(vk)) => ok_if_vk_hash_expected(vk.clone(), *vk_hash)?, + Some(None) => { + // we explicitly have erased the key + return Err(format!( + "No verification key found for proved account \ + update: the verification key was removed by a \ + previous account update\ + account_id={:?}", + account_id + )); + } + None => { + // we haven't set anything; lookup the vk in the fallback + find_vk(*vk_hash, &account_id)? + } + } + }; + + tbl.insert(account_id, prioritized_vk.hash()); + + Ok((p.clone(), Some(prioritized_vk))) + } + + _ => Ok((p.clone(), None)), + }; + + // NOTE: we only update the overriden map AFTER verifying the update to make sure + // that the verification for the VK update itself is done against the previous VK. + if let SetOrKeep::Set(vk_next) = &p.body.update.verification_key { + vks_overridden.insert(p.account_id().clone(), Some(vk_next.clone())); + } + + result + })?; + + Ok(ZkAppCommand { + fee_payer: fee_payer.clone(), + account_updates, + memo: memo.clone(), + }) + } +} + +pub mod valid { + use crate::scan_state::transaction_logic::zkapp_command::verifiable::create; + + use super::*; + + #[derive(Clone, Debug, PartialEq)] + pub struct ZkAppCommand { + pub zkapp_command: super::ZkAppCommand, + } + + impl ZkAppCommand { + pub fn forget(self) -> super::ZkAppCommand { + self.zkapp_command + } + pub fn forget_ref(&self) -> &super::ZkAppCommand { + &self.zkapp_command + } + } + + /// + pub fn of_verifiable(cmd: verifiable::ZkAppCommand) -> ZkAppCommand { + ZkAppCommand { + zkapp_command: super::ZkAppCommand::of_verifiable(cmd), + } + } + + /// + pub fn to_valid( + zkapp_command: super::ZkAppCommand, + status: &TransactionStatus, + find_vk: impl Fn(Fp, &AccountId) -> Result, + ) -> Result { + create(&zkapp_command, status.is_failed(), find_vk).map(of_verifiable) + } +} + +pub struct MaybeWithStatus { + pub cmd: T, + pub status: Option, +} + +impl From> for MaybeWithStatus { + fn from(value: WithStatus) -> Self { + let WithStatus { data, status } = value; + Self { + cmd: data, + status: Some(status), + } + } +} + +impl From> for WithStatus { + fn from(value: MaybeWithStatus) -> Self { + let MaybeWithStatus { cmd, status } = value; + Self { + data: cmd, + status: status.unwrap(), + } + } +} + +impl MaybeWithStatus { + pub fn cmd(&self) -> &T { + &self.cmd + } + pub fn is_failed(&self) -> bool { + self.status + .as_ref() + .map(TransactionStatus::is_failed) + .unwrap_or(false) + } + pub fn map(self, fun: F) -> MaybeWithStatus + where + F: FnOnce(T) -> V, + { + MaybeWithStatus { + cmd: fun(self.cmd), + status: self.status, + } + } +} + +pub trait ToVerifiableCache { + fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire>; + fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire); +} + +pub trait ToVerifiableStrategy { + type Cache: ToVerifiableCache; + + fn create_all( + cmd: &ZkAppCommand, + is_failed: bool, + cache: &mut Self::Cache, + ) -> Result { + let verified_cmd = verifiable::create(cmd, is_failed, |vk_hash, account_id| { + cache + .find(account_id, &vk_hash) + .cloned() + .or_else(|| { + cmd.extract_vks() + .iter() + .find(|(id, _)| account_id == id) + .map(|(_, key)| key.clone()) + }) + .ok_or_else(|| format!("verification key not found in cache: {:?}", vk_hash)) + })?; + if !is_failed { + for (account_id, vk) in cmd.extract_vks() { + cache.add(account_id, vk); + } + } + Ok(verified_cmd) + } +} + +pub mod from_unapplied_sequence { + use super::*; + + pub struct Cache { + cache: HashMap>, + } + + impl Cache { + pub fn new(cache: HashMap>) -> Self { + Self { cache } + } + } + + impl ToVerifiableCache for Cache { + fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { + let vks = self.cache.get(account_id)?; + vks.get(vk_hash) + } + fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { + let vks = self.cache.entry(account_id).or_default(); + vks.insert(vk.hash(), vk); + } + } + + pub struct FromUnappliedSequence; + + impl ToVerifiableStrategy for FromUnappliedSequence { + type Cache = Cache; + } +} + +pub mod from_applied_sequence { + use super::*; + + pub struct Cache { + cache: HashMap, + } + + impl Cache { + pub fn new(cache: HashMap) -> Self { + Self { cache } + } + } + + impl ToVerifiableCache for Cache { + fn find(&self, account_id: &AccountId, vk_hash: &Fp) -> Option<&VerificationKeyWire> { + self.cache + .get(account_id) + .filter(|vk| &vk.hash() == vk_hash) + } + fn add(&mut self, account_id: AccountId, vk: VerificationKeyWire) { + self.cache.insert(account_id, vk); + } + } + + pub struct FromAppliedSequence; + + impl ToVerifiableStrategy for FromAppliedSequence { + type Cache = Cache; + } +} + +/// +pub mod zkapp_weight { + use crate::scan_state::transaction_logic::zkapp_command::{ + AccountUpdate, CallForest, FeePayer, + }; + + pub fn account_update(_: &AccountUpdate) -> u64 { + 1 + } + pub fn fee_payer(_: &FeePayer) -> u64 { + 1 + } + pub fn account_updates(list: &CallForest) -> u64 { + list.fold(0, |acc, p| acc + account_update(p)) + } + pub fn memo(_: &super::Memo) -> u64 { + 0 + } +} From 3e8b2b300e7e3a7dd97fa51f334be38131f9ccfc Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 18:24:09 +0200 Subject: [PATCH 04/16] Ledger/transaction_logic: extract zkapp_statement module to separate file Extract the zkapp_statement module from transaction_logic/mod.rs into its own file. This module contains the TransactionCommitment and ZkappStatement types used for zkApp transaction commitments. Changes: - Extract zkapp_statement module to transaction_logic/zkapp_statement.rs - Use explicit imports instead of 'use super::*' - Import zkapp_command module as 'self' to access AccountUpdateRef trait - Update mod.rs to reference the new module file --- .../src/scan_state/transaction_logic/mod.rs | 102 +----------------- .../transaction_logic/zkapp_statement.rs | 94 ++++++++++++++++ 2 files changed, 97 insertions(+), 99 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_statement.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index bac589fe2..d5893de60 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -6,14 +6,14 @@ use std::{ use ark_ff::Zero; use itertools::{FoldWhile, Itertools}; use mina_core::constants::ConstraintConstants; -use mina_hasher::{Fp, Hashable, ROInput}; +use mina_hasher::Fp; use mina_macros::SerdeYojsonEnum; use mina_p2p_messages::{ bigint::InvalidBigInt, binprot, v2::{MinaBaseUserCommandStableV2, MinaTransactionTransactionStableV2}, }; -use mina_signer::{CompressedPubKey, NetworkId}; +use mina_signer::CompressedPubKey; use poseidon::hash::{ hash_with_kimchi, params::{CODA_RECEIPT_UC, MINA_ZKAPP_MEMO}, @@ -608,103 +608,7 @@ impl Memo { pub mod signed_command; pub mod zkapp_command; -pub mod zkapp_statement { - use poseidon::hash::params::MINA_ACCOUNT_UPDATE_CONS; - - use super::{ - zkapp_command::{CallForest, Tree}, - *, - }; - - #[derive(Copy, Clone, Debug, derive_more::Deref, derive_more::From)] - pub struct TransactionCommitment(pub Fp); - - impl TransactionCommitment { - /// - pub fn create(account_updates_hash: Fp) -> Self { - Self(account_updates_hash) - } - - /// - pub fn create_complete(&self, memo_hash: Fp, fee_payer_hash: Fp) -> Self { - Self(hash_with_kimchi( - &MINA_ACCOUNT_UPDATE_CONS, - &[memo_hash, fee_payer_hash, self.0], - )) - } - - pub fn empty() -> Self { - Self(Fp::zero()) - } - } - - impl Hashable for TransactionCommitment { - type D = NetworkId; - - fn to_roinput(&self) -> ROInput { - let mut roi = ROInput::new(); - roi = roi.append_field(self.0); - roi - } - - fn domain_string(network_id: NetworkId) -> Option { - match network_id { - NetworkId::MAINNET => mina_core::network::mainnet::SIGNATURE_PREFIX, - NetworkId::TESTNET => mina_core::network::devnet::SIGNATURE_PREFIX, - } - .to_string() - .into() - } - } - - #[derive(Clone, Debug)] - pub struct ZkappStatement { - pub account_update: TransactionCommitment, - pub calls: TransactionCommitment, - } - - impl ZkappStatement { - pub fn to_field_elements(&self) -> Vec { - let Self { - account_update, - calls, - } = self; - - vec![**account_update, **calls] - } - - pub fn of_tree( - tree: &Tree, - ) -> Self { - let Tree { - account_update: _, - account_update_digest, - calls, - } = tree; - - Self { - account_update: TransactionCommitment(account_update_digest.get().unwrap()), - calls: TransactionCommitment(calls.hash()), - } - } - - pub fn zkapp_statements_of_forest_prime( - forest: CallForest<(AccountUpdate, Data)>, - ) -> CallForest<(AccountUpdate, (Data, Self))> { - forest.map_with_trees_to(|(account_update, data), tree| { - (account_update.clone(), (data.clone(), Self::of_tree(tree))) - }) - } - - fn zkapp_statements_of_forest( - forest: CallForest, - ) -> CallForest<(AccountUpdate, Self)> { - forest.map_with_trees_to(|account_update, tree| { - (account_update.clone(), Self::of_tree(tree)) - }) - } - } -} +pub mod zkapp_statement; pub mod verifiable { use std::ops::Neg; diff --git a/ledger/src/scan_state/transaction_logic/zkapp_statement.rs b/ledger/src/scan_state/transaction_logic/zkapp_statement.rs new file mode 100644 index 000000000..3343e4e24 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_statement.rs @@ -0,0 +1,94 @@ +use ark_ff::Zero; +use mina_hasher::{Fp, Hashable, ROInput}; +use mina_signer::NetworkId; +use poseidon::hash::{hash_with_kimchi, params::MINA_ACCOUNT_UPDATE_CONS}; + +use super::zkapp_command::{self, AccountUpdate, CallForest, Tree}; + +#[derive(Copy, Clone, Debug, derive_more::Deref, derive_more::From)] +pub struct TransactionCommitment(pub Fp); + +impl TransactionCommitment { + /// + pub fn create(account_updates_hash: Fp) -> Self { + Self(account_updates_hash) + } + + /// + pub fn create_complete(&self, memo_hash: Fp, fee_payer_hash: Fp) -> Self { + Self(hash_with_kimchi( + &MINA_ACCOUNT_UPDATE_CONS, + &[memo_hash, fee_payer_hash, self.0], + )) + } + + pub fn empty() -> Self { + Self(Fp::zero()) + } +} + +impl Hashable for TransactionCommitment { + type D = NetworkId; + + fn to_roinput(&self) -> ROInput { + let mut roi = ROInput::new(); + roi = roi.append_field(self.0); + roi + } + + fn domain_string(network_id: NetworkId) -> Option { + match network_id { + NetworkId::MAINNET => mina_core::network::mainnet::SIGNATURE_PREFIX, + NetworkId::TESTNET => mina_core::network::devnet::SIGNATURE_PREFIX, + } + .to_string() + .into() + } +} + +#[derive(Clone, Debug)] +pub struct ZkappStatement { + pub account_update: TransactionCommitment, + pub calls: TransactionCommitment, +} + +impl ZkappStatement { + pub fn to_field_elements(&self) -> Vec { + let Self { + account_update, + calls, + } = self; + + vec![**account_update, **calls] + } + + pub fn of_tree( + tree: &Tree, + ) -> Self { + let Tree { + account_update: _, + account_update_digest, + calls, + } = tree; + + Self { + account_update: TransactionCommitment(account_update_digest.get().unwrap()), + calls: TransactionCommitment(calls.hash()), + } + } + + pub fn zkapp_statements_of_forest_prime( + forest: CallForest<(AccountUpdate, Data)>, + ) -> CallForest<(AccountUpdate, (Data, Self))> { + forest.map_with_trees_to(|(account_update, data), tree| { + (account_update.clone(), (data.clone(), Self::of_tree(tree))) + }) + } + + fn zkapp_statements_of_forest( + forest: CallForest, + ) -> CallForest<(AccountUpdate, Self)> { + forest + .map_with_trees_to(|account_update, tree| (account_update.clone(), Self::of_tree(tree))) + } +} From 0da5eb94252907f874d99dc1cab2819feabdd9b9 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 18:43:14 +0200 Subject: [PATCH 05/16] Ledger/transaction_logic: extract verifiable module to separate file Extract the verifiable module from transaction_logic/mod.rs into its own file. This module contains the verifiable UserCommand enum (with serde traits) and signature verification functions. Changes: - Extract verifiable module to transaction_logic/verifiable.rs - Use explicit imports instead of 'use super::*' - Update mod.rs to reference the new module file --- .../src/scan_state/transaction_logic/mod.rs | 51 +----------------- .../transaction_logic/verifiable.rs | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 50 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/verifiable.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index d5893de60..ea4ba4bea 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -610,56 +610,7 @@ pub mod signed_command; pub mod zkapp_command; pub mod zkapp_statement; -pub mod verifiable { - use std::ops::Neg; - - use ark_ff::{BigInteger, PrimeField}; - - use super::*; - - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] - pub enum UserCommand { - SignedCommand(Box), - ZkAppCommand(Box), - } - - pub fn compressed_to_pubkey(pubkey: &CompressedPubKey) -> mina_signer::PubKey { - // Taken from https://github.com/o1-labs/proof-systems/blob/e3fc04ce87f8695288de167115dea80050ab33f4/signer/src/pubkey.rs#L95-L106 - let mut pt = - mina_signer::CurvePoint::get_point_from_x_unchecked(pubkey.x, pubkey.is_odd).unwrap(); - - if pt.y.into_bigint().is_even() == pubkey.is_odd { - pt.y = pt.y.neg(); - } - - assert!(pt.is_on_curve()); - - // Safe now because we checked point pt is on curve - mina_signer::PubKey::from_point_unsafe(pt) - } - - /// - pub fn check_only_for_signature( - cmd: Box, - ) -> Result> { - // - - let signed_command::SignedCommand { - payload, - signer: pubkey, - signature, - } = &*cmd; - - let payload = TransactionUnionPayload::of_user_command_payload(payload); - let pubkey = compressed_to_pubkey(pubkey); - - if crate::verifier::common::legacy_verify_signature(signature, &pubkey, &payload) { - Ok(valid::UserCommand::SignedCommand(cmd)) - } else { - Err(cmd) - } - } -} +pub mod verifiable; #[derive(Clone, Debug, PartialEq)] pub enum UserCommand { diff --git a/ledger/src/scan_state/transaction_logic/verifiable.rs b/ledger/src/scan_state/transaction_logic/verifiable.rs new file mode 100644 index 000000000..e77195c23 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/verifiable.rs @@ -0,0 +1,52 @@ +use std::ops::Neg; + +use ark_ff::{BigInteger, PrimeField}; +use mina_signer::CompressedPubKey; + +use super::{ + signed_command, transaction_union_payload::TransactionUnionPayload, valid, + zkapp_command, +}; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum UserCommand { + SignedCommand(Box), + ZkAppCommand(Box), +} + +pub fn compressed_to_pubkey(pubkey: &CompressedPubKey) -> mina_signer::PubKey { + // Taken from https://github.com/o1-labs/proof-systems/blob/e3fc04ce87f8695288de167115dea80050ab33f4/signer/src/pubkey.rs#L95-L106 + let mut pt = + mina_signer::CurvePoint::get_point_from_x_unchecked(pubkey.x, pubkey.is_odd).unwrap(); + + if pt.y.into_bigint().is_even() == pubkey.is_odd { + pt.y = pt.y.neg(); + } + + assert!(pt.is_on_curve()); + + // Safe now because we checked point pt is on curve + mina_signer::PubKey::from_point_unsafe(pt) +} + +/// +pub fn check_only_for_signature( + cmd: Box, +) -> Result> { + // + + let signed_command::SignedCommand { + payload, + signer: pubkey, + signature, + } = &*cmd; + + let payload = TransactionUnionPayload::of_user_command_payload(payload); + let pubkey = compressed_to_pubkey(pubkey); + + if crate::verifier::common::legacy_verify_signature(signature, &pubkey, &payload) { + Ok(valid::UserCommand::SignedCommand(cmd)) + } else { + Err(cmd) + } +} From 137204bf93a800030b564392b784194d0f17b64d Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 19:40:33 +0200 Subject: [PATCH 06/16] Ledger/transaction_logic: extract transaction_applied module to separate file Extract the transaction_applied module from transaction_logic/mod.rs into its own file. This module contains types for applied transactions including SignedCommandApplied, ZkappCommandApplied, FeeTransferApplied, and CoinbaseApplied. Changes: - Extract transaction_applied module to transaction_logic/transaction_applied.rs - Use explicit imports instead of 'use super::*' - Add Magnitude trait import for Amount::zero() method - Update mod.rs to reference the new module file --- .../src/scan_state/transaction_logic/mod.rs | 179 +---------------- .../transaction_logic/transaction_applied.rs | 186 ++++++++++++++++++ 2 files changed, 187 insertions(+), 178 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/transaction_applied.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index ea4ba4bea..d83a09554 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1033,184 +1033,7 @@ impl From<&Transaction> for MinaTransactionTransactionStableV2 { } } -pub mod transaction_applied { - use crate::AccountId; - - use super::*; - - pub mod signed_command_applied { - use super::*; - - #[derive(Debug, Clone, PartialEq)] - pub struct Common { - pub user_command: WithStatus, - } - - #[derive(Debug, Clone, PartialEq)] - pub enum Body { - Payments { - new_accounts: Vec, - }, - StakeDelegation { - previous_delegate: Option, - }, - Failed, - } - - #[derive(Debug, Clone, PartialEq)] - pub struct SignedCommandApplied { - pub common: Common, - pub body: Body, - } - } - - pub use signed_command_applied::SignedCommandApplied; - - impl SignedCommandApplied { - pub fn new_accounts(&self) -> &[AccountId] { - use signed_command_applied::Body::*; - - match &self.body { - Payments { new_accounts } => new_accounts.as_slice(), - StakeDelegation { .. } | Failed => &[], - } - } - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct ZkappCommandApplied { - pub accounts: Vec<(AccountId, Option>)>, - pub command: WithStatus, - pub new_accounts: Vec, - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub enum CommandApplied { - SignedCommand(Box), - ZkappCommand(Box), - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct FeeTransferApplied { - pub fee_transfer: WithStatus, - pub new_accounts: Vec, - pub burned_tokens: Amount, - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct CoinbaseApplied { - pub coinbase: WithStatus, - pub new_accounts: Vec, - pub burned_tokens: Amount, - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub enum Varying { - Command(CommandApplied), - FeeTransfer(FeeTransferApplied), - Coinbase(CoinbaseApplied), - } - - /// - #[derive(Debug, Clone, PartialEq)] - pub struct TransactionApplied { - pub previous_hash: Fp, - pub varying: Varying, - } - - impl TransactionApplied { - /// - pub fn transaction(&self) -> WithStatus { - use CommandApplied::*; - use Varying::*; - - match &self.varying { - Command(SignedCommand(cmd)) => cmd - .common - .user_command - .map(|c| Transaction::Command(UserCommand::SignedCommand(Box::new(c.clone())))), - Command(ZkappCommand(cmd)) => cmd - .command - .map(|c| Transaction::Command(UserCommand::ZkAppCommand(Box::new(c.clone())))), - FeeTransfer(f) => f.fee_transfer.map(|f| Transaction::FeeTransfer(f.clone())), - Coinbase(c) => c.coinbase.map(|c| Transaction::Coinbase(c.clone())), - } - } - - /// - pub fn transaction_status(&self) -> &TransactionStatus { - use CommandApplied::*; - use Varying::*; - - match &self.varying { - Command(SignedCommand(cmd)) => &cmd.common.user_command.status, - Command(ZkappCommand(cmd)) => &cmd.command.status, - FeeTransfer(f) => &f.fee_transfer.status, - Coinbase(c) => &c.coinbase.status, - } - } - - pub fn burned_tokens(&self) -> Amount { - match &self.varying { - Varying::Command(_) => Amount::zero(), - Varying::FeeTransfer(f) => f.burned_tokens, - Varying::Coinbase(c) => c.burned_tokens, - } - } - - pub fn new_accounts(&self) -> &[AccountId] { - use CommandApplied::*; - use Varying::*; - - match &self.varying { - Command(SignedCommand(cmd)) => cmd.new_accounts(), - Command(ZkappCommand(cmd)) => cmd.new_accounts.as_slice(), - FeeTransfer(f) => f.new_accounts.as_slice(), - Coinbase(cb) => cb.new_accounts.as_slice(), - } - } - - /// - pub fn supply_increase( - &self, - constraint_constants: &ConstraintConstants, - ) -> Result, String> { - let burned_tokens = Signed::::of_unsigned(self.burned_tokens()); - - let account_creation_fees = { - let account_creation_fee_int = constraint_constants.account_creation_fee; - let num_accounts_created = self.new_accounts().len() as u64; - - // int type is OK, no danger of overflow - let amount = account_creation_fee_int - .checked_mul(num_accounts_created) - .unwrap(); - Signed::::of_unsigned(Amount::from_u64(amount)) - }; - - let expected_supply_increase = match &self.varying { - Varying::Coinbase(cb) => cb.coinbase.data.expected_supply_increase()?, - _ => Amount::zero(), - }; - let expected_supply_increase = Signed::::of_unsigned(expected_supply_increase); - - // TODO: Make sure it's correct - let total = [burned_tokens, account_creation_fees] - .into_iter() - .try_fold(expected_supply_increase, |total, amt| { - total.add(&amt.negate()) - }); - - total.ok_or_else(|| "overflow".to_string()) - } - } -} - +pub mod transaction_applied; pub mod transaction_witness { use mina_p2p_messages::v2::MinaStateProtocolStateBodyValueStableV2; diff --git a/ledger/src/scan_state/transaction_logic/transaction_applied.rs b/ledger/src/scan_state/transaction_logic/transaction_applied.rs new file mode 100644 index 000000000..e83be4b9b --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/transaction_applied.rs @@ -0,0 +1,186 @@ +use mina_core::constants::ConstraintConstants; +use mina_hasher::Fp; + +use crate::{Account, AccountId}; + +use super::{ + signed_command, zkapp_command, Coinbase, FeeTransfer, Transaction, TransactionStatus, + UserCommand, WithStatus, +}; +use crate::scan_state::currency::{Amount, Magnitude, Signed}; + +pub mod signed_command_applied { + use mina_signer::CompressedPubKey; + + use crate::AccountId; + + use super::{signed_command, WithStatus}; + + #[derive(Debug, Clone, PartialEq)] + pub struct Common { + pub user_command: WithStatus, + } + + #[derive(Debug, Clone, PartialEq)] + pub enum Body { + Payments { + new_accounts: Vec, + }, + StakeDelegation { + previous_delegate: Option, + }, + Failed, + } + + #[derive(Debug, Clone, PartialEq)] + pub struct SignedCommandApplied { + pub common: Common, + pub body: Body, + } +} + +pub use signed_command_applied::SignedCommandApplied; + +impl SignedCommandApplied { + pub fn new_accounts(&self) -> &[AccountId] { + use signed_command_applied::Body::*; + + match &self.body { + Payments { new_accounts } => new_accounts.as_slice(), + StakeDelegation { .. } | Failed => &[], + } + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct ZkappCommandApplied { + pub accounts: Vec<(AccountId, Option>)>, + pub command: WithStatus, + pub new_accounts: Vec, +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub enum CommandApplied { + SignedCommand(Box), + ZkappCommand(Box), +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct FeeTransferApplied { + pub fee_transfer: WithStatus, + pub new_accounts: Vec, + pub burned_tokens: Amount, +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct CoinbaseApplied { + pub coinbase: WithStatus, + pub new_accounts: Vec, + pub burned_tokens: Amount, +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub enum Varying { + Command(CommandApplied), + FeeTransfer(FeeTransferApplied), + Coinbase(CoinbaseApplied), +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct TransactionApplied { + pub previous_hash: Fp, + pub varying: Varying, +} + +impl TransactionApplied { + /// + pub fn transaction(&self) -> WithStatus { + use CommandApplied::*; + use Varying::*; + + match &self.varying { + Command(SignedCommand(cmd)) => cmd + .common + .user_command + .map(|c| Transaction::Command(UserCommand::SignedCommand(Box::new(c.clone())))), + Command(ZkappCommand(cmd)) => cmd + .command + .map(|c| Transaction::Command(UserCommand::ZkAppCommand(Box::new(c.clone())))), + FeeTransfer(f) => f.fee_transfer.map(|f| Transaction::FeeTransfer(f.clone())), + Coinbase(c) => c.coinbase.map(|c| Transaction::Coinbase(c.clone())), + } + } + + /// + pub fn transaction_status(&self) -> &TransactionStatus { + use CommandApplied::*; + use Varying::*; + + match &self.varying { + Command(SignedCommand(cmd)) => &cmd.common.user_command.status, + Command(ZkappCommand(cmd)) => &cmd.command.status, + FeeTransfer(f) => &f.fee_transfer.status, + Coinbase(c) => &c.coinbase.status, + } + } + + pub fn burned_tokens(&self) -> Amount { + match &self.varying { + Varying::Command(_) => Amount::zero(), + Varying::FeeTransfer(f) => f.burned_tokens, + Varying::Coinbase(c) => c.burned_tokens, + } + } + + pub fn new_accounts(&self) -> &[AccountId] { + use CommandApplied::*; + use Varying::*; + + match &self.varying { + Command(SignedCommand(cmd)) => cmd.new_accounts(), + Command(ZkappCommand(cmd)) => cmd.new_accounts.as_slice(), + FeeTransfer(f) => f.new_accounts.as_slice(), + Coinbase(cb) => cb.new_accounts.as_slice(), + } + } + + /// + pub fn supply_increase( + &self, + constraint_constants: &ConstraintConstants, + ) -> Result, String> { + let burned_tokens = Signed::::of_unsigned(self.burned_tokens()); + + let account_creation_fees = { + let account_creation_fee_int = constraint_constants.account_creation_fee; + let num_accounts_created = self.new_accounts().len() as u64; + + // int type is OK, no danger of overflow + let amount = account_creation_fee_int + .checked_mul(num_accounts_created) + .unwrap(); + Signed::::of_unsigned(Amount::from_u64(amount)) + }; + + let expected_supply_increase = match &self.varying { + Varying::Coinbase(cb) => cb.coinbase.data.expected_supply_increase()?, + _ => Amount::zero(), + }; + let expected_supply_increase = Signed::::of_unsigned(expected_supply_increase); + + // TODO: Make sure it's correct + let total = [burned_tokens, account_creation_fees] + .into_iter() + .try_fold(expected_supply_increase, |total, amt| { + total.add(&amt.negate()) + }); + + total.ok_or_else(|| "overflow".to_string()) + } +} From aff5f24b2bbf2e313bbaa109b460775a8741b686 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 20:34:29 +0200 Subject: [PATCH 07/16] Ledger/transaction_logic: extract transaction_witness module to separate file Extract the transaction_witness module from transaction_logic/mod.rs into its own file. This is a small module containing only the TransactionWitness struct used for transaction proofs. Changes: - Extract transaction_witness module to transaction_logic/transaction_witness.rs - Use explicit imports instead of 'use super::*' - Update mod.rs to reference the new module file --- .../src/scan_state/transaction_logic/mod.rs | 21 +------------------ .../transaction_logic/transaction_witness.rs | 18 ++++++++++++++++ 2 files changed, 19 insertions(+), 20 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/transaction_witness.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index d83a09554..7d4aceec6 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1034,26 +1034,7 @@ impl From<&Transaction> for MinaTransactionTransactionStableV2 { } pub mod transaction_applied; -pub mod transaction_witness { - use mina_p2p_messages::v2::MinaStateProtocolStateBodyValueStableV2; - - use crate::scan_state::pending_coinbase::Stack; - - use super::*; - - /// - #[derive(Debug)] - pub struct TransactionWitness { - pub transaction: Transaction, - pub first_pass_ledger: SparseLedger, - pub second_pass_ledger: SparseLedger, - pub protocol_state_body: MinaStateProtocolStateBodyValueStableV2, - pub init_stack: Stack, - pub status: TransactionStatus, - pub block_global_slot: Slot, - } -} - +pub mod transaction_witness; pub mod protocol_state { use mina_p2p_messages::v2::{self, MinaStateProtocolStateValueStableV2}; diff --git a/ledger/src/scan_state/transaction_logic/transaction_witness.rs b/ledger/src/scan_state/transaction_logic/transaction_witness.rs new file mode 100644 index 000000000..23dfb76b2 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/transaction_witness.rs @@ -0,0 +1,18 @@ +use mina_p2p_messages::v2::MinaStateProtocolStateBodyValueStableV2; + +use crate::{scan_state::pending_coinbase::Stack, sparse_ledger::SparseLedger}; + +use super::{Transaction, TransactionStatus}; +use crate::scan_state::currency::Slot; + +/// +#[derive(Debug)] +pub struct TransactionWitness { + pub transaction: Transaction, + pub first_pass_ledger: SparseLedger, + pub second_pass_ledger: SparseLedger, + pub protocol_state_body: MinaStateProtocolStateBodyValueStableV2, + pub init_stack: Stack, + pub status: TransactionStatus, + pub block_global_slot: Slot, +} From dcf3dae97e3244f36876a059f10861d56679ad70 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 20:57:31 +0200 Subject: [PATCH 08/16] Ledger/transaction_logic: extract protocol_state module to separate file Extract the protocol_state module from transaction_logic/mod.rs into its own file. This module contains protocol state types including ProtocolStateView, EpochData, EpochLedger, and GlobalState. Changes: - Extract protocol_state module to transaction_logic/protocol_state.rs - Use explicit imports instead of 'use super::*' - Update mod.rs to reference the new module file --- .../src/scan_state/transaction_logic/mod.rs | 159 +---------------- .../transaction_logic/protocol_state.rs | 161 ++++++++++++++++++ 2 files changed, 162 insertions(+), 158 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/protocol_state.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 7d4aceec6..c8c81a6de 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1035,164 +1035,7 @@ impl From<&Transaction> for MinaTransactionTransactionStableV2 { pub mod transaction_applied; pub mod transaction_witness; -pub mod protocol_state { - use mina_p2p_messages::v2::{self, MinaStateProtocolStateValueStableV2}; - - use crate::proofs::field::FieldWitness; - - use super::*; - - #[derive(Debug, Clone)] - pub struct EpochLedger { - pub hash: F, - pub total_currency: Amount, - } - - #[derive(Debug, Clone)] - pub struct EpochData { - pub ledger: EpochLedger, - pub seed: F, - pub start_checkpoint: F, - pub lock_checkpoint: F, - pub epoch_length: Length, - } - - #[derive(Debug, Clone)] - pub struct ProtocolStateView { - pub snarked_ledger_hash: Fp, - pub blockchain_length: Length, - pub min_window_density: Length, - pub total_currency: Amount, - pub global_slot_since_genesis: Slot, - pub staking_epoch_data: EpochData, - pub next_epoch_data: EpochData, - } - - /// - pub fn protocol_state_view( - state: &MinaStateProtocolStateValueStableV2, - ) -> Result { - let MinaStateProtocolStateValueStableV2 { - previous_state_hash: _, - body, - } = state; - - protocol_state_body_view(body) - } - - pub fn protocol_state_body_view( - body: &v2::MinaStateProtocolStateBodyValueStableV2, - ) -> Result { - let cs = &body.consensus_state; - let sed = &cs.staking_epoch_data; - let ned = &cs.next_epoch_data; - - Ok(ProtocolStateView { - // - // - snarked_ledger_hash: body - .blockchain_state - .ledger_proof_statement - .target - .first_pass_ledger - .to_field()?, - blockchain_length: Length(cs.blockchain_length.as_u32()), - min_window_density: Length(cs.min_window_density.as_u32()), - total_currency: Amount(cs.total_currency.as_u64()), - global_slot_since_genesis: (&cs.global_slot_since_genesis).into(), - staking_epoch_data: EpochData { - ledger: EpochLedger { - hash: sed.ledger.hash.to_field()?, - total_currency: Amount(sed.ledger.total_currency.as_u64()), - }, - seed: sed.seed.to_field()?, - start_checkpoint: sed.start_checkpoint.to_field()?, - lock_checkpoint: sed.lock_checkpoint.to_field()?, - epoch_length: Length(sed.epoch_length.as_u32()), - }, - next_epoch_data: EpochData { - ledger: EpochLedger { - hash: ned.ledger.hash.to_field()?, - total_currency: Amount(ned.ledger.total_currency.as_u64()), - }, - seed: ned.seed.to_field()?, - start_checkpoint: ned.start_checkpoint.to_field()?, - lock_checkpoint: ned.lock_checkpoint.to_field()?, - epoch_length: Length(ned.epoch_length.as_u32()), - }, - }) - } - - pub type GlobalState = GlobalStateSkeleton, Slot>; - - #[derive(Debug, Clone)] - pub struct GlobalStateSkeleton { - pub first_pass_ledger: L, - pub second_pass_ledger: L, - pub fee_excess: SignedAmount, - pub supply_increase: SignedAmount, - pub protocol_state: ProtocolStateView, - /// Slot of block when the transaction is applied. - /// NOTE: This is at least 1 slot after the protocol_state's view, - /// which is for the *previous* slot. - pub block_global_slot: Slot, - } - - impl GlobalState { - pub fn first_pass_ledger(&self) -> L { - self.first_pass_ledger.create_masked() - } - - #[must_use] - pub fn set_first_pass_ledger(&self, should_update: bool, ledger: L) -> Self { - let mut this = self.clone(); - if should_update { - this.first_pass_ledger.apply_mask(ledger); - } - this - } - - pub fn second_pass_ledger(&self) -> L { - self.second_pass_ledger.create_masked() - } - - #[must_use] - pub fn set_second_pass_ledger(&self, should_update: bool, ledger: L) -> Self { - let mut this = self.clone(); - if should_update { - this.second_pass_ledger.apply_mask(ledger); - } - this - } - - pub fn fee_excess(&self) -> Signed { - self.fee_excess - } - - #[must_use] - pub fn set_fee_excess(&self, fee_excess: Signed) -> Self { - let mut this = self.clone(); - this.fee_excess = fee_excess; - this - } - - pub fn supply_increase(&self) -> Signed { - self.supply_increase - } - - #[must_use] - pub fn set_supply_increase(&self, supply_increase: Signed) -> Self { - let mut this = self.clone(); - this.supply_increase = supply_increase; - this - } - - pub fn block_global_slot(&self) -> Slot { - self.block_global_slot - } - } -} - +pub mod protocol_state; pub mod local_state { use std::{cell::RefCell, rc::Rc}; diff --git a/ledger/src/scan_state/transaction_logic/protocol_state.rs b/ledger/src/scan_state/transaction_logic/protocol_state.rs new file mode 100644 index 000000000..b8fa75787 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/protocol_state.rs @@ -0,0 +1,161 @@ +use mina_hasher::Fp; +use mina_p2p_messages::{ + bigint::InvalidBigInt, + v2::{self, MinaStateProtocolStateValueStableV2}, +}; + +use crate::{ + proofs::field::FieldWitness, + scan_state::currency::{Amount, Length, Signed, Slot}, + sparse_ledger::LedgerIntf, +}; + +#[derive(Debug, Clone)] +pub struct EpochLedger { + pub hash: F, + pub total_currency: Amount, +} + +#[derive(Debug, Clone)] +pub struct EpochData { + pub ledger: EpochLedger, + pub seed: F, + pub start_checkpoint: F, + pub lock_checkpoint: F, + pub epoch_length: Length, +} + +#[derive(Debug, Clone)] +pub struct ProtocolStateView { + pub snarked_ledger_hash: Fp, + pub blockchain_length: Length, + pub min_window_density: Length, + pub total_currency: Amount, + pub global_slot_since_genesis: Slot, + pub staking_epoch_data: EpochData, + pub next_epoch_data: EpochData, +} + +/// +pub fn protocol_state_view( + state: &MinaStateProtocolStateValueStableV2, +) -> Result { + let MinaStateProtocolStateValueStableV2 { + previous_state_hash: _, + body, + } = state; + + protocol_state_body_view(body) +} + +pub fn protocol_state_body_view( + body: &v2::MinaStateProtocolStateBodyValueStableV2, +) -> Result { + let cs = &body.consensus_state; + let sed = &cs.staking_epoch_data; + let ned = &cs.next_epoch_data; + + Ok(ProtocolStateView { + // + // + snarked_ledger_hash: body + .blockchain_state + .ledger_proof_statement + .target + .first_pass_ledger + .to_field()?, + blockchain_length: Length(cs.blockchain_length.as_u32()), + min_window_density: Length(cs.min_window_density.as_u32()), + total_currency: Amount(cs.total_currency.as_u64()), + global_slot_since_genesis: (&cs.global_slot_since_genesis).into(), + staking_epoch_data: EpochData { + ledger: EpochLedger { + hash: sed.ledger.hash.to_field()?, + total_currency: Amount(sed.ledger.total_currency.as_u64()), + }, + seed: sed.seed.to_field()?, + start_checkpoint: sed.start_checkpoint.to_field()?, + lock_checkpoint: sed.lock_checkpoint.to_field()?, + epoch_length: Length(sed.epoch_length.as_u32()), + }, + next_epoch_data: EpochData { + ledger: EpochLedger { + hash: ned.ledger.hash.to_field()?, + total_currency: Amount(ned.ledger.total_currency.as_u64()), + }, + seed: ned.seed.to_field()?, + start_checkpoint: ned.start_checkpoint.to_field()?, + lock_checkpoint: ned.lock_checkpoint.to_field()?, + epoch_length: Length(ned.epoch_length.as_u32()), + }, + }) +} + +pub type GlobalState = GlobalStateSkeleton, Slot>; + +#[derive(Debug, Clone)] +pub struct GlobalStateSkeleton { + pub first_pass_ledger: L, + pub second_pass_ledger: L, + pub fee_excess: SignedAmount, + pub supply_increase: SignedAmount, + pub protocol_state: ProtocolStateView, + /// Slot of block when the transaction is applied. + /// NOTE: This is at least 1 slot after the protocol_state's view, + /// which is for the *previous* slot. + pub block_global_slot: Slot, +} + +impl GlobalState { + pub fn first_pass_ledger(&self) -> L { + self.first_pass_ledger.create_masked() + } + + #[must_use] + pub fn set_first_pass_ledger(&self, should_update: bool, ledger: L) -> Self { + let mut this = self.clone(); + if should_update { + this.first_pass_ledger.apply_mask(ledger); + } + this + } + + pub fn second_pass_ledger(&self) -> L { + self.second_pass_ledger.create_masked() + } + + #[must_use] + pub fn set_second_pass_ledger(&self, should_update: bool, ledger: L) -> Self { + let mut this = self.clone(); + if should_update { + this.second_pass_ledger.apply_mask(ledger); + } + this + } + + pub fn fee_excess(&self) -> Signed { + self.fee_excess + } + + #[must_use] + pub fn set_fee_excess(&self, fee_excess: Signed) -> Self { + let mut this = self.clone(); + this.fee_excess = fee_excess; + this + } + + pub fn supply_increase(&self) -> Signed { + self.supply_increase + } + + #[must_use] + pub fn set_supply_increase(&self, supply_increase: Signed) -> Self { + let mut this = self.clone(); + this.supply_increase = supply_increase; + this + } + + pub fn block_global_slot(&self) -> Slot { + self.block_global_slot + } +} From a7ff1b46783ce790187671ff892664112f76da03 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 21:14:44 +0200 Subject: [PATCH 09/16] Ledger/transaction_logic: extract local_state module to separate file Split the large transaction_logic.rs file by extracting the local_state module (947 lines) into its own file at ledger/src/scan_state/transaction_logic/local_state.rs. Changes: - Created local_state.rs with StackFrame, CallStack, LocalState, LocalStateEnv types and zkApp command application functions - Made apply_zkapp_command_first_pass and apply_zkapp_command_second_pass public - Added module declaration in mod.rs - Updated import paths in sparse_ledger.rs - Added all necessary imports (ark_ff::Zero, itertools, BTreeMap, AccountIdOrderable, AppendToInputs, zkapps, etc.) - Removed unused imports from mod.rs (ark_ff::Zero, Itertools) - Fixed ambiguous Index::zero() call to use IndexInterface::zero() --- .../transaction_logic/local_state.rs | 965 ++++++++++++++++++ .../src/scan_state/transaction_logic/mod.rs | 958 +---------------- ledger/src/sparse_ledger/sparse_ledger.rs | 6 +- 3 files changed, 975 insertions(+), 954 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/local_state.rs diff --git a/ledger/src/scan_state/transaction_logic/local_state.rs b/ledger/src/scan_state/transaction_logic/local_state.rs new file mode 100644 index 000000000..1310009c9 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/local_state.rs @@ -0,0 +1,965 @@ +use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; + +use ark_ff::Zero; +use itertools::{FoldWhile, Itertools}; +use mina_core::constants::ConstraintConstants; +use mina_hasher::Fp; +use poseidon::hash::{ + hash_with_kimchi, params::MINA_ACCOUNT_UPDATE_STACK_FRAME, Inputs, +}; + +use crate::{ + proofs::{ + field::{field, Boolean, ToBoolean}, + numbers::nat::CheckedNat, + to_field_elements::ToFieldElements, + witness::Witness, + }, + scan_state::currency::{Amount, Index, Magnitude, Signed, Slot}, + sparse_ledger::LedgerIntf, + zkapps::{ + self, + interfaces::{ + CallStackInterface, IndexInterface, SignedAmountInterface, StackFrameInterface, + }, + non_snark::{LedgerNonSnark, ZkappNonSnark}, + }, + AccountId, AccountIdOrderable, AppendToInputs, ToInputs, TokenId, +}; + +use super::{ + protocol_state::{GlobalState, ProtocolStateView}, + transaction_applied::ZkappCommandApplied, + transaction_partially_applied::ZkappCommandPartiallyApplied, + zkapp_command::{AccountUpdate, CallForest, WithHash, ZkAppCommand}, + TransactionFailure, TransactionStatus, WithStatus, +}; + +#[derive(Debug, Clone)] +pub struct StackFrame { + pub caller: TokenId, + pub caller_caller: TokenId, + pub calls: CallForest, // TODO +} + +// +#[derive(Debug, Clone)] +pub struct StackFrameCheckedFrame { + pub caller: TokenId, + pub caller_caller: TokenId, + pub calls: WithHash>, + /// Hack until we have proper cvar + pub is_default: bool, +} + +impl ToFieldElements for StackFrameCheckedFrame { + fn to_field_elements(&self, fields: &mut Vec) { + let Self { + caller, + caller_caller, + calls, + is_default: _, + } = self; + + // calls.hash().to_field_elements(fields); + calls.hash.to_field_elements(fields); + caller_caller.to_field_elements(fields); + caller.to_field_elements(fields); + } +} + +enum LazyValueInner { + Value(T), + Fun(Box T>), + None, +} + +impl Default for LazyValueInner { + fn default() -> Self { + Self::None + } +} + +pub struct LazyValue { + value: Rc>>, +} + +impl Clone for LazyValue { + fn clone(&self) -> Self { + Self { + value: Rc::clone(&self.value), + } + } +} + +impl std::fmt::Debug for LazyValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let v = self.try_get(); + f.debug_struct("LazyValue").field("value", &v).finish() + } +} + +impl LazyValue { + pub fn make(fun: F) -> Self + where + F: FnOnce(&mut D) -> T + 'static, + { + Self { + value: Rc::new(RefCell::new(LazyValueInner::Fun(Box::new(fun)))), + } + } + + fn get_impl(&self) -> std::cell::Ref<'_, T> { + use std::cell::Ref; + + let inner = self.value.borrow(); + Ref::map(inner, |inner| { + let LazyValueInner::Value(value) = inner else { + panic!("invalid state"); + }; + value + }) + } + + /// Returns the value when it already has been "computed" + pub fn try_get(&self) -> Option> { + let inner = self.value.borrow(); + + match &*inner { + LazyValueInner::Value(_) => {} + LazyValueInner::Fun(_) => return None, + LazyValueInner::None => panic!("invalid state"), + } + + Some(self.get_impl()) + } + + pub fn get(&self, data: &mut D) -> std::cell::Ref<'_, T> { + let v = self.value.borrow(); + + if let LazyValueInner::Fun(_) = &*v { + std::mem::drop(v); + + let LazyValueInner::Fun(fun) = self.value.take() else { + panic!("invalid state"); + }; + + let data = fun(data); + self.value.replace(LazyValueInner::Value(data)); + }; + + self.get_impl() + } +} + +#[derive(Clone, Debug)] +pub struct WithLazyHash { + pub data: T, + hash: LazyValue>, +} + +impl WithLazyHash { + pub fn new(data: T, fun: F) -> Self + where + F: FnOnce(&mut Witness) -> Fp + 'static, + { + Self { + data, + hash: LazyValue::make(fun), + } + } + + pub fn hash(&self, w: &mut Witness) -> Fp { + *self.hash.get(w) + } +} + +impl std::ops::Deref for WithLazyHash { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl ToFieldElements for WithLazyHash { + fn to_field_elements(&self, fields: &mut Vec) { + let hash = self.hash.try_get().expect("hash hasn't been computed yet"); + hash.to_field_elements(fields) + } +} + +// +pub type StackFrameChecked = WithLazyHash; + +impl Default for StackFrame { + fn default() -> Self { + StackFrame { + caller: TokenId::default(), + caller_caller: TokenId::default(), + calls: CallForest::new(), + } + } +} + +impl StackFrame { + pub fn empty() -> Self { + Self { + caller: TokenId::default(), + caller_caller: TokenId::default(), + calls: CallForest(Vec::new()), + } + } + + /// TODO: this needs to be tested + /// + /// + pub fn hash(&self) -> Fp { + let mut inputs = Inputs::new(); + + inputs.append_field(self.caller.0); + inputs.append_field(self.caller_caller.0); + + self.calls.ensure_hashed(); + let field = match self.calls.0.first() { + None => Fp::zero(), + Some(calls) => calls.stack_hash.get().unwrap(), // Never fail, we called `ensure_hashed` + }; + inputs.append_field(field); + + hash_with_kimchi(&MINA_ACCOUNT_UPDATE_STACK_FRAME, &inputs.to_fields()) + } + + pub fn digest(&self) -> Fp { + self.hash() + } + + pub fn unhash(&self, _h: Fp, w: &mut Witness) -> StackFrameChecked { + let v = self.exists_elt(w); + v.hash(w); + v + } + + pub fn exists_elt(&self, w: &mut Witness) -> StackFrameChecked { + // We decompose this way because of OCaml evaluation order + let calls = WithHash { + data: self.calls.clone(), + hash: w.exists(self.calls.hash()), + }; + let caller_caller = w.exists(self.caller_caller.clone()); + let caller = w.exists(self.caller.clone()); + + let frame = StackFrameCheckedFrame { + caller, + caller_caller, + calls, + is_default: false, + }; + + StackFrameChecked::of_frame(frame) + } +} + +impl StackFrameCheckedFrame { + pub fn hash(&self, w: &mut Witness) -> Fp { + let mut inputs = Inputs::new(); + + inputs.append(&self.caller); + inputs.append(&self.caller_caller.0); + inputs.append(&self.calls.hash); + + let fields = inputs.to_fields(); + + if self.is_default { + use crate::proofs::transaction::transaction_snark::checked_hash3; + checked_hash3(&MINA_ACCOUNT_UPDATE_STACK_FRAME, &fields, w) + } else { + use crate::proofs::transaction::transaction_snark::checked_hash; + checked_hash(&MINA_ACCOUNT_UPDATE_STACK_FRAME, &fields, w) + } + } +} + +impl StackFrameChecked { + pub fn of_frame(frame: StackFrameCheckedFrame) -> Self { + // TODO: Don't clone here + let frame2 = frame.clone(); + let hash = LazyValue::make(move |w: &mut Witness| frame2.hash(w)); + + Self { data: frame, hash } + } +} + +#[derive(Debug, Clone)] +pub struct CallStack(pub Vec); + +impl Default for CallStack { + fn default() -> Self { + Self::new() + } +} + +impl CallStack { + pub fn new() -> Self { + CallStack(Vec::new()) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter().rev() + } + + pub fn push(&self, stack_frame: &StackFrame) -> Self { + let mut ret = self.0.clone(); + ret.push(stack_frame.clone()); + Self(ret) + } + + pub fn pop(&self) -> Option<(StackFrame, CallStack)> { + let mut ret = self.0.clone(); + ret.pop().map(|frame| (frame, Self(ret))) + } + + pub fn pop_exn(&self) -> (StackFrame, CallStack) { + let mut ret = self.0.clone(); + if let Some(frame) = ret.pop() { + (frame, Self(ret)) + } else { + panic!() + } + } +} + +// NOTE: It looks like there are different instances of the polymorphic LocalEnv type +// One with concrete types for the stack frame, call stack, and ledger. Created from the Env +// And the other with their hashes. To differentiate them I renamed the first LocalStateEnv +// Maybe a better solution is to keep the LocalState name and put it under a different module +// pub type LocalStateEnv = LocalStateSkeleton< +// L, // ledger +// StackFrame, // stack_frame +// CallStack, // call_stack +// ReceiptChainHash, // commitments +// Signed, // excess & supply_increase +// Vec>, // failure_status_tbl +// bool, // success & will_succeed +// Index, // account_update_index +// >; + +pub type LocalStateEnv = crate::zkapps::zkapp_logic::LocalState>; + +// TODO: Dedub this with `crate::zkapps::zkapp_logic::LocalState` +#[derive(Debug, Clone)] +pub struct LocalStateSkeleton< + L: LedgerIntf + Clone, + StackFrame: StackFrameInterface, + CallStack: CallStackInterface, + TC, + SignedAmount: SignedAmountInterface, + FailuresTable, + Bool, + Index: IndexInterface, +> { + pub stack_frame: StackFrame, + pub call_stack: CallStack, + pub transaction_commitment: TC, + pub full_transaction_commitment: TC, + pub excess: SignedAmount, + pub supply_increase: SignedAmount, + pub ledger: L, + pub success: Bool, + pub account_update_index: Index, + // TODO: optimize by reversing the insertion order + pub failure_status_tbl: FailuresTable, + pub will_succeed: Bool, +} + +// impl LocalStateEnv +// where +// L: LedgerNonSnark, +// { +// pub fn add_new_failure_status_bucket(&self) -> Self { +// let mut failure_status_tbl = self.failure_status_tbl.clone(); +// failure_status_tbl.insert(0, Vec::new()); +// Self { +// failure_status_tbl, +// ..self.clone() +// } +// } + +// pub fn add_check(&self, failure: TransactionFailure, b: bool) -> Self { +// let failure_status_tbl = if !b { +// let mut failure_status_tbl = self.failure_status_tbl.clone(); +// failure_status_tbl[0].insert(0, failure); +// failure_status_tbl +// } else { +// self.failure_status_tbl.clone() +// }; + +// Self { +// failure_status_tbl, +// success: self.success && b, +// ..self.clone() +// } +// } +// } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LocalState { + pub stack_frame: Fp, + pub call_stack: Fp, + pub transaction_commitment: Fp, + pub full_transaction_commitment: Fp, + pub excess: Signed, + pub supply_increase: Signed, + pub ledger: Fp, + pub success: bool, + pub account_update_index: Index, + pub failure_status_tbl: Vec>, + pub will_succeed: bool, +} + +impl ToInputs for LocalState { + /// + fn to_inputs(&self, inputs: &mut Inputs) { + let Self { + stack_frame, + call_stack, + transaction_commitment, + full_transaction_commitment, + excess, + supply_increase, + ledger, + success, + account_update_index, + failure_status_tbl: _, + will_succeed, + } = self; + + inputs.append(stack_frame); + inputs.append(call_stack); + inputs.append(transaction_commitment); + inputs.append(full_transaction_commitment); + inputs.append(excess); + inputs.append(supply_increase); + inputs.append(ledger); + inputs.append(account_update_index); + inputs.append(success); + inputs.append(will_succeed); + } +} + +impl LocalState { + /// + pub fn dummy() -> Self { + Self { + stack_frame: StackFrame::empty().hash(), + call_stack: Fp::zero(), + transaction_commitment: Fp::zero(), + full_transaction_commitment: Fp::zero(), + excess: Signed::::zero(), + supply_increase: Signed::::zero(), + ledger: Fp::zero(), + success: true, + account_update_index: ::zero(), + failure_status_tbl: Vec::new(), + will_succeed: true, + } + } + + pub fn empty() -> Self { + Self::dummy() + } + + pub fn equal_without_ledger(&self, other: &Self) -> bool { + let Self { + stack_frame, + call_stack, + transaction_commitment, + full_transaction_commitment, + excess, + supply_increase, + ledger: _, + success, + account_update_index, + failure_status_tbl, + will_succeed, + } = self; + + stack_frame == &other.stack_frame + && call_stack == &other.call_stack + && transaction_commitment == &other.transaction_commitment + && full_transaction_commitment == &other.full_transaction_commitment + && excess == &other.excess + && supply_increase == &other.supply_increase + && success == &other.success + && account_update_index == &other.account_update_index + && failure_status_tbl == &other.failure_status_tbl + && will_succeed == &other.will_succeed + } + + pub fn checked_equal_prime(&self, other: &Self, w: &mut Witness) -> [Boolean; 11] { + let Self { + stack_frame, + call_stack, + transaction_commitment, + full_transaction_commitment, + excess, + supply_increase, + ledger, + success, + account_update_index, + failure_status_tbl: _, + will_succeed, + } = self; + + // { stack_frame : 'stack_frame + // ; call_stack : 'call_stack + // ; transaction_commitment : 'comm + // ; full_transaction_commitment : 'comm + // ; excess : 'signed_amount + // ; supply_increase : 'signed_amount + // ; ledger : 'ledger + // ; success : 'bool + // ; account_update_index : 'length + // ; failure_status_tbl : 'failure_status_tbl + // ; will_succeed : 'bool + // } + + let mut alls = [ + field::equal(*stack_frame, other.stack_frame, w), + field::equal(*call_stack, other.call_stack, w), + field::equal(*transaction_commitment, other.transaction_commitment, w), + field::equal( + *full_transaction_commitment, + other.full_transaction_commitment, + w, + ), + excess + .to_checked::() + .equal(&other.excess.to_checked(), w), + supply_increase + .to_checked::() + .equal(&other.supply_increase.to_checked(), w), + field::equal(*ledger, other.ledger, w), + success.to_boolean().equal(&other.success.to_boolean(), w), + account_update_index + .to_checked::() + .equal(&other.account_update_index.to_checked(), w), + Boolean::True, + will_succeed + .to_boolean() + .equal(&other.will_succeed.to_boolean(), w), + ]; + alls.reverse(); + alls + } +} + +fn step_all( +_constraint_constants: &ConstraintConstants, +f: &impl Fn(&mut A, &GlobalState, &LocalStateEnv), +user_acc: &mut A, +(g_state, l_state): (&mut GlobalState, &mut LocalStateEnv), +) -> Result>, String> +where +L: LedgerNonSnark, +{ +while !l_state.stack_frame.calls.is_empty() { + zkapps::non_snark::step(g_state, l_state)?; + f(user_acc, g_state, l_state); +} +Ok(l_state.failure_status_tbl.clone()) +} + +/// apply zkapp command fee payer's while stubbing out the second pass ledger +/// CAUTION: If you use the intermediate local states, you MUST update the +/// [`LocalStateEnv::will_succeed`] field to `false` if the `status` is [`TransactionStatus::Failed`].*) +pub fn apply_zkapp_command_first_pass_aux( +constraint_constants: &ConstraintConstants, +global_slot: Slot, +state_view: &ProtocolStateView, +init: &mut A, +f: F, +fee_excess: Option>, +supply_increase: Option>, +ledger: &mut L, +command: &ZkAppCommand, +) -> Result, String> +where +L: LedgerNonSnark, +F: Fn(&mut A, &GlobalState, &LocalStateEnv), +{ +let fee_excess = fee_excess.unwrap_or_else(Signed::zero); +let supply_increase = supply_increase.unwrap_or_else(Signed::zero); + +let previous_hash = ledger.merkle_root(); +let original_first_pass_account_states = { + let id = command.fee_payer(); + let location = { + let loc = ledger.location_of_account(&id); + let account = loc.as_ref().and_then(|loc| ledger.get(loc)); + loc.zip(account) + }; + + vec![(id, location)] +}; +// let perform = |eff: Eff| Env::perform(eff); + +let (mut global_state, mut local_state) = ( + GlobalState { + protocol_state: state_view.clone(), + first_pass_ledger: ledger.clone(), + second_pass_ledger: { + // We stub out the second_pass_ledger initially, and then poke the + // correct value in place after the first pass is finished. + ::empty(0) + }, + fee_excess, + supply_increase, + block_global_slot: global_slot, + }, + LocalStateEnv { + stack_frame: StackFrame::default(), + call_stack: CallStack::new(), + transaction_commitment: Fp::zero(), + full_transaction_commitment: Fp::zero(), + excess: Signed::::zero(), + supply_increase, + ledger: ::empty(0), + success: true, + account_update_index: IndexInterface::zero(), + failure_status_tbl: Vec::new(), + will_succeed: true, + }, +); + +f(init, &global_state, &local_state); +let account_updates = command.all_account_updates(); + +zkapps::non_snark::start( + &mut global_state, + &mut local_state, + zkapps::non_snark::StartData { + account_updates, + memo_hash: command.memo.hash(), + // It's always valid to set this value to true, and it will + // have no effect outside of the snark. + will_succeed: true, + }, +)?; + +let command = command.clone(); +let constraint_constants = constraint_constants.clone(); +let state_view = state_view.clone(); + +let res = ZkappCommandPartiallyApplied { + command, + previous_hash, + original_first_pass_account_states, + constraint_constants, + state_view, + global_state, + local_state, +}; + +Ok(res) +} + +pub fn apply_zkapp_command_first_pass( +constraint_constants: &ConstraintConstants, +global_slot: Slot, +state_view: &ProtocolStateView, +fee_excess: Option>, +supply_increase: Option>, +ledger: &mut L, +command: &ZkAppCommand, +) -> Result, String> +where +L: LedgerNonSnark, +{ +let mut acc = (); +let partial_stmt = apply_zkapp_command_first_pass_aux( + constraint_constants, + global_slot, + state_view, + &mut acc, + |_acc, _g, _l| {}, + fee_excess, + supply_increase, + ledger, + command, +)?; + +Ok(partial_stmt) +} + +pub fn apply_zkapp_command_second_pass_aux( +constraint_constants: &ConstraintConstants, +init: &mut A, +f: F, +ledger: &mut L, +c: ZkappCommandPartiallyApplied, +) -> Result +where +L: LedgerNonSnark, +F: Fn(&mut A, &GlobalState, &LocalStateEnv), +{ +// let perform = |eff: Eff| Env::perform(eff); + +let original_account_states: Vec<(AccountId, Option<_>)> = { + // get the original states of all the accounts in each pass. + // If an account updated in the first pass is referenced in account + // updates, then retain the value before first pass application*) + + let accounts_referenced = c.command.accounts_referenced(); + + let mut account_states = BTreeMap::>::new(); + + let referenced = accounts_referenced.into_iter().map(|id| { + let location = { + let loc = ledger.location_of_account(&id); + let account = loc.as_ref().and_then(|loc| ledger.get(loc)); + loc.zip(account) + }; + (id, location) + }); + + c.original_first_pass_account_states + .into_iter() + .chain(referenced) + .for_each(|(id, acc_opt)| { + use std::collections::btree_map::Entry::Vacant; + + let id_with_order: AccountIdOrderable = id.into(); + if let Vacant(entry) = account_states.entry(id_with_order) { + entry.insert(acc_opt); + }; + }); + + account_states + .into_iter() + // Convert back the `AccountIdOrder` into `AccountId`, now that they are sorted + .map(|(id, account): (AccountIdOrderable, Option<_>)| (id.into(), account)) + .collect() +}; + +let mut account_states_after_fee_payer = { + // To check if the accounts remain unchanged in the event the transaction + // fails. First pass updates will remain even if the transaction fails to + // apply zkapp account updates*) + + c.command.accounts_referenced().into_iter().map(|id| { + let loc = ledger.location_of_account(&id); + let a = loc.as_ref().and_then(|loc| ledger.get(loc)); + + match a { + Some(a) => (id, Some((loc.unwrap(), a))), + None => (id, None), + } + }) +}; + +let accounts = || { + original_account_states + .iter() + .map(|(id, account)| (id.clone(), account.as_ref().map(|(_loc, acc)| acc.clone()))) + .collect::>() +}; + +// Warning(OCaml): This is an abstraction leak / hack. +// Here, we update global second pass ledger to be the input ledger, and +// then update the local ledger to be the input ledger *IF AND ONLY IF* +// there are more transaction segments to be processed in this pass. + +// TODO(OCaml): Remove this, and uplift the logic into the call in staged ledger. + +let mut global_state = GlobalState { + second_pass_ledger: ledger.clone(), + ..c.global_state +}; + +let mut local_state = { + if c.local_state.stack_frame.calls.is_empty() { + // Don't mess with the local state; we've already finished the + // transaction after the fee payer. + c.local_state + } else { + // Install the ledger that should already be in the local state, but + // may not be in some situations depending on who the caller is. + LocalStateEnv { + ledger: global_state.second_pass_ledger(), + ..c.local_state + } + } +}; + +f(init, &global_state, &local_state); +let start = (&mut global_state, &mut local_state); + +let reversed_failure_status_tbl = step_all(constraint_constants, &f, init, start)?; + +let failure_status_tbl = reversed_failure_status_tbl + .into_iter() + .rev() + .collect::>(); + +let account_ids_originally_not_in_ledger = + original_account_states + .iter() + .filter_map(|(acct_id, loc_and_acct)| { + if loc_and_acct.is_none() { + Some(acct_id) + } else { + None + } + }); + +let successfully_applied = failure_status_tbl.concat().is_empty(); + +// if the zkapp command fails in at least 1 account update, +// then all the account updates would be cancelled except +// the fee payer one +let failure_status_tbl = if successfully_applied { + failure_status_tbl +} else { + failure_status_tbl + .into_iter() + .enumerate() + .map(|(idx, fs)| { + if idx > 0 && fs.is_empty() { + vec![TransactionFailure::Cancelled] + } else { + fs + } + }) + .collect() +}; + +// accounts not originally in ledger, now present in ledger +let new_accounts = account_ids_originally_not_in_ledger + .filter(|acct_id| ledger.location_of_account(acct_id).is_some()) + .cloned() + .collect::>(); + +let new_accounts_is_empty = new_accounts.is_empty(); + +let valid_result = Ok(ZkappCommandApplied { + accounts: accounts(), + command: WithStatus { + data: c.command, + status: if successfully_applied { + TransactionStatus::Applied + } else { + TransactionStatus::Failed(failure_status_tbl) + }, + }, + new_accounts, +}); + +if successfully_applied { + valid_result +} else { + let other_account_update_accounts_unchanged = account_states_after_fee_payer + .fold_while(true, |acc, (_, loc_opt)| match loc_opt { + Some((loc, a)) => match ledger.get(&loc) { + Some(a_) if !(a == a_) => FoldWhile::Done(false), + _ => FoldWhile::Continue(acc), + }, + _ => FoldWhile::Continue(acc), + }) + .into_inner(); + + // Other zkapp_command failed, therefore, updates in those should not get applied + if new_accounts_is_empty && other_account_update_accounts_unchanged { + valid_result + } else { + Err("Zkapp_command application failed but new accounts created or some of the other account_update updates applied".to_string()) + } +} +} + +pub fn apply_zkapp_command_second_pass( +constraint_constants: &ConstraintConstants, +ledger: &mut L, +c: ZkappCommandPartiallyApplied, +) -> Result +where +L: LedgerNonSnark, +{ +let x = apply_zkapp_command_second_pass_aux( + constraint_constants, + &mut (), + |_, _, _| {}, + ledger, + c, +)?; +Ok(x) +} + +fn apply_zkapp_command_unchecked_aux( +constraint_constants: &ConstraintConstants, +global_slot: Slot, +state_view: &ProtocolStateView, +init: &mut A, +f: F, +fee_excess: Option>, +supply_increase: Option>, +ledger: &mut L, +command: &ZkAppCommand, +) -> Result +where +L: LedgerNonSnark, +F: Fn(&mut A, &GlobalState, &LocalStateEnv), +{ +let partial_stmt = apply_zkapp_command_first_pass_aux( + constraint_constants, + global_slot, + state_view, + init, + &f, + fee_excess, + supply_increase, + ledger, + command, +)?; + +apply_zkapp_command_second_pass_aux(constraint_constants, init, &f, ledger, partial_stmt) +} + +fn apply_zkapp_command_unchecked( +constraint_constants: &ConstraintConstants, +global_slot: Slot, +state_view: &ProtocolStateView, +ledger: &mut L, +command: &ZkAppCommand, +) -> Result<(ZkappCommandApplied, (LocalStateEnv, Signed)), String> +where +L: LedgerNonSnark, +{ +let zkapp_partially_applied: ZkappCommandPartiallyApplied = apply_zkapp_command_first_pass( + constraint_constants, + global_slot, + state_view, + None, + None, + ledger, + command, +)?; + +let mut state_res = None; +let account_update_applied = apply_zkapp_command_second_pass_aux( + constraint_constants, + &mut state_res, + |acc, global_state, local_state| { + *acc = Some((local_state.clone(), global_state.fee_excess)) + }, + ledger, + zkapp_partially_applied, +)?; +let (state, amount) = state_res.unwrap(); + +Ok((account_update_applied, (state.clone(), amount))) +} diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index c8c81a6de..6005900d0 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -3,8 +3,7 @@ use std::{ fmt::Display, }; -use ark_ff::Zero; -use itertools::{FoldWhile, Itertools}; +use itertools::FoldWhile; use mina_core::constants::ConstraintConstants; use mina_hasher::Fp; use mina_macros::SerdeYojsonEnum; @@ -35,7 +34,10 @@ use crate::{ }; use self::{ - local_state::{CallStack, LocalStateEnv, StackFrame}, + local_state::{ + apply_zkapp_command_first_pass, apply_zkapp_command_second_pass, CallStack, + LocalStateEnv, StackFrame, + }, protocol_state::{GlobalState, ProtocolStateView}, signed_command::{SignedCommand, SignedCommandPayload}, transaction_applied::{ @@ -1036,955 +1038,7 @@ impl From<&Transaction> for MinaTransactionTransactionStableV2 { pub mod transaction_applied; pub mod transaction_witness; pub mod protocol_state; -pub mod local_state { - use std::{cell::RefCell, rc::Rc}; - - use poseidon::hash::params::MINA_ACCOUNT_UPDATE_STACK_FRAME; - - use crate::{ - proofs::{ - field::{field, Boolean, ToBoolean}, - numbers::nat::CheckedNat, - to_field_elements::ToFieldElements, - }, - zkapps::interfaces::{ - CallStackInterface, IndexInterface, SignedAmountInterface, StackFrameInterface, - }, - ToInputs, - }; - - use super::{zkapp_command::CallForest, *}; - - #[derive(Debug, Clone)] - pub struct StackFrame { - pub caller: TokenId, - pub caller_caller: TokenId, - pub calls: CallForest, // TODO - } - - // - #[derive(Debug, Clone)] - pub struct StackFrameCheckedFrame { - pub caller: TokenId, - pub caller_caller: TokenId, - pub calls: WithHash>, - /// Hack until we have proper cvar - pub is_default: bool, - } - - impl ToFieldElements for StackFrameCheckedFrame { - fn to_field_elements(&self, fields: &mut Vec) { - let Self { - caller, - caller_caller, - calls, - is_default: _, - } = self; - - // calls.hash().to_field_elements(fields); - calls.hash.to_field_elements(fields); - caller_caller.to_field_elements(fields); - caller.to_field_elements(fields); - } - } - - enum LazyValueInner { - Value(T), - Fun(Box T>), - None, - } - - impl Default for LazyValueInner { - fn default() -> Self { - Self::None - } - } - - pub struct LazyValue { - value: Rc>>, - } - - impl Clone for LazyValue { - fn clone(&self) -> Self { - Self { - value: Rc::clone(&self.value), - } - } - } - - impl std::fmt::Debug for LazyValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let v = self.try_get(); - f.debug_struct("LazyValue").field("value", &v).finish() - } - } - - impl LazyValue { - pub fn make(fun: F) -> Self - where - F: FnOnce(&mut D) -> T + 'static, - { - Self { - value: Rc::new(RefCell::new(LazyValueInner::Fun(Box::new(fun)))), - } - } - - fn get_impl(&self) -> std::cell::Ref<'_, T> { - use std::cell::Ref; - - let inner = self.value.borrow(); - Ref::map(inner, |inner| { - let LazyValueInner::Value(value) = inner else { - panic!("invalid state"); - }; - value - }) - } - - /// Returns the value when it already has been "computed" - pub fn try_get(&self) -> Option> { - let inner = self.value.borrow(); - - match &*inner { - LazyValueInner::Value(_) => {} - LazyValueInner::Fun(_) => return None, - LazyValueInner::None => panic!("invalid state"), - } - - Some(self.get_impl()) - } - - pub fn get(&self, data: &mut D) -> std::cell::Ref<'_, T> { - let v = self.value.borrow(); - - if let LazyValueInner::Fun(_) = &*v { - std::mem::drop(v); - - let LazyValueInner::Fun(fun) = self.value.take() else { - panic!("invalid state"); - }; - - let data = fun(data); - self.value.replace(LazyValueInner::Value(data)); - }; - - self.get_impl() - } - } - - #[derive(Clone, Debug)] - pub struct WithLazyHash { - pub data: T, - hash: LazyValue>, - } - - impl WithLazyHash { - pub fn new(data: T, fun: F) -> Self - where - F: FnOnce(&mut Witness) -> Fp + 'static, - { - Self { - data, - hash: LazyValue::make(fun), - } - } - - pub fn hash(&self, w: &mut Witness) -> Fp { - *self.hash.get(w) - } - } - - impl std::ops::Deref for WithLazyHash { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.data - } - } - - impl ToFieldElements for WithLazyHash { - fn to_field_elements(&self, fields: &mut Vec) { - let hash = self.hash.try_get().expect("hash hasn't been computed yet"); - hash.to_field_elements(fields) - } - } - - // - pub type StackFrameChecked = WithLazyHash; - - impl Default for StackFrame { - fn default() -> Self { - StackFrame { - caller: TokenId::default(), - caller_caller: TokenId::default(), - calls: CallForest::new(), - } - } - } - - impl StackFrame { - pub fn empty() -> Self { - Self { - caller: TokenId::default(), - caller_caller: TokenId::default(), - calls: CallForest(Vec::new()), - } - } - - /// TODO: this needs to be tested - /// - /// - pub fn hash(&self) -> Fp { - let mut inputs = Inputs::new(); - - inputs.append_field(self.caller.0); - inputs.append_field(self.caller_caller.0); - - self.calls.ensure_hashed(); - let field = match self.calls.0.first() { - None => Fp::zero(), - Some(calls) => calls.stack_hash.get().unwrap(), // Never fail, we called `ensure_hashed` - }; - inputs.append_field(field); - - hash_with_kimchi(&MINA_ACCOUNT_UPDATE_STACK_FRAME, &inputs.to_fields()) - } - - pub fn digest(&self) -> Fp { - self.hash() - } - - pub fn unhash(&self, _h: Fp, w: &mut Witness) -> StackFrameChecked { - let v = self.exists_elt(w); - v.hash(w); - v - } - - pub fn exists_elt(&self, w: &mut Witness) -> StackFrameChecked { - // We decompose this way because of OCaml evaluation order - let calls = WithHash { - data: self.calls.clone(), - hash: w.exists(self.calls.hash()), - }; - let caller_caller = w.exists(self.caller_caller.clone()); - let caller = w.exists(self.caller.clone()); - - let frame = StackFrameCheckedFrame { - caller, - caller_caller, - calls, - is_default: false, - }; - - StackFrameChecked::of_frame(frame) - } - } - - impl StackFrameCheckedFrame { - pub fn hash(&self, w: &mut Witness) -> Fp { - let mut inputs = Inputs::new(); - - inputs.append(&self.caller); - inputs.append(&self.caller_caller.0); - inputs.append(&self.calls.hash); - - let fields = inputs.to_fields(); - - if self.is_default { - use crate::proofs::transaction::transaction_snark::checked_hash3; - checked_hash3(&MINA_ACCOUNT_UPDATE_STACK_FRAME, &fields, w) - } else { - use crate::proofs::transaction::transaction_snark::checked_hash; - checked_hash(&MINA_ACCOUNT_UPDATE_STACK_FRAME, &fields, w) - } - } - } - - impl StackFrameChecked { - pub fn of_frame(frame: StackFrameCheckedFrame) -> Self { - // TODO: Don't clone here - let frame2 = frame.clone(); - let hash = LazyValue::make(move |w: &mut Witness| frame2.hash(w)); - - Self { data: frame, hash } - } - } - - #[derive(Debug, Clone)] - pub struct CallStack(pub Vec); - - impl Default for CallStack { - fn default() -> Self { - Self::new() - } - } - - impl CallStack { - pub fn new() -> Self { - CallStack(Vec::new()) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn iter(&self) -> impl Iterator { - self.0.iter().rev() - } - - pub fn push(&self, stack_frame: &StackFrame) -> Self { - let mut ret = self.0.clone(); - ret.push(stack_frame.clone()); - Self(ret) - } - - pub fn pop(&self) -> Option<(StackFrame, CallStack)> { - let mut ret = self.0.clone(); - ret.pop().map(|frame| (frame, Self(ret))) - } - - pub fn pop_exn(&self) -> (StackFrame, CallStack) { - let mut ret = self.0.clone(); - if let Some(frame) = ret.pop() { - (frame, Self(ret)) - } else { - panic!() - } - } - } - - // NOTE: It looks like there are different instances of the polymorphic LocalEnv type - // One with concrete types for the stack frame, call stack, and ledger. Created from the Env - // And the other with their hashes. To differentiate them I renamed the first LocalStateEnv - // Maybe a better solution is to keep the LocalState name and put it under a different module - // pub type LocalStateEnv = LocalStateSkeleton< - // L, // ledger - // StackFrame, // stack_frame - // CallStack, // call_stack - // ReceiptChainHash, // commitments - // Signed, // excess & supply_increase - // Vec>, // failure_status_tbl - // bool, // success & will_succeed - // Index, // account_update_index - // >; - - pub type LocalStateEnv = crate::zkapps::zkapp_logic::LocalState>; - - // TODO: Dedub this with `crate::zkapps::zkapp_logic::LocalState` - #[derive(Debug, Clone)] - pub struct LocalStateSkeleton< - L: LedgerIntf + Clone, - StackFrame: StackFrameInterface, - CallStack: CallStackInterface, - TC, - SignedAmount: SignedAmountInterface, - FailuresTable, - Bool, - Index: IndexInterface, - > { - pub stack_frame: StackFrame, - pub call_stack: CallStack, - pub transaction_commitment: TC, - pub full_transaction_commitment: TC, - pub excess: SignedAmount, - pub supply_increase: SignedAmount, - pub ledger: L, - pub success: Bool, - pub account_update_index: Index, - // TODO: optimize by reversing the insertion order - pub failure_status_tbl: FailuresTable, - pub will_succeed: Bool, - } - - // impl LocalStateEnv - // where - // L: LedgerNonSnark, - // { - // pub fn add_new_failure_status_bucket(&self) -> Self { - // let mut failure_status_tbl = self.failure_status_tbl.clone(); - // failure_status_tbl.insert(0, Vec::new()); - // Self { - // failure_status_tbl, - // ..self.clone() - // } - // } - - // pub fn add_check(&self, failure: TransactionFailure, b: bool) -> Self { - // let failure_status_tbl = if !b { - // let mut failure_status_tbl = self.failure_status_tbl.clone(); - // failure_status_tbl[0].insert(0, failure); - // failure_status_tbl - // } else { - // self.failure_status_tbl.clone() - // }; - - // Self { - // failure_status_tbl, - // success: self.success && b, - // ..self.clone() - // } - // } - // } - - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct LocalState { - pub stack_frame: Fp, - pub call_stack: Fp, - pub transaction_commitment: Fp, - pub full_transaction_commitment: Fp, - pub excess: Signed, - pub supply_increase: Signed, - pub ledger: Fp, - pub success: bool, - pub account_update_index: Index, - pub failure_status_tbl: Vec>, - pub will_succeed: bool, - } - - impl ToInputs for LocalState { - /// - fn to_inputs(&self, inputs: &mut Inputs) { - let Self { - stack_frame, - call_stack, - transaction_commitment, - full_transaction_commitment, - excess, - supply_increase, - ledger, - success, - account_update_index, - failure_status_tbl: _, - will_succeed, - } = self; - - inputs.append(stack_frame); - inputs.append(call_stack); - inputs.append(transaction_commitment); - inputs.append(full_transaction_commitment); - inputs.append(excess); - inputs.append(supply_increase); - inputs.append(ledger); - inputs.append(account_update_index); - inputs.append(success); - inputs.append(will_succeed); - } - } - - impl LocalState { - /// - pub fn dummy() -> Self { - Self { - stack_frame: StackFrame::empty().hash(), - call_stack: Fp::zero(), - transaction_commitment: Fp::zero(), - full_transaction_commitment: Fp::zero(), - excess: Signed::::zero(), - supply_increase: Signed::::zero(), - ledger: Fp::zero(), - success: true, - account_update_index: ::zero(), - failure_status_tbl: Vec::new(), - will_succeed: true, - } - } - - pub fn empty() -> Self { - Self::dummy() - } - - pub fn equal_without_ledger(&self, other: &Self) -> bool { - let Self { - stack_frame, - call_stack, - transaction_commitment, - full_transaction_commitment, - excess, - supply_increase, - ledger: _, - success, - account_update_index, - failure_status_tbl, - will_succeed, - } = self; - - stack_frame == &other.stack_frame - && call_stack == &other.call_stack - && transaction_commitment == &other.transaction_commitment - && full_transaction_commitment == &other.full_transaction_commitment - && excess == &other.excess - && supply_increase == &other.supply_increase - && success == &other.success - && account_update_index == &other.account_update_index - && failure_status_tbl == &other.failure_status_tbl - && will_succeed == &other.will_succeed - } - - pub fn checked_equal_prime(&self, other: &Self, w: &mut Witness) -> [Boolean; 11] { - let Self { - stack_frame, - call_stack, - transaction_commitment, - full_transaction_commitment, - excess, - supply_increase, - ledger, - success, - account_update_index, - failure_status_tbl: _, - will_succeed, - } = self; - - // { stack_frame : 'stack_frame - // ; call_stack : 'call_stack - // ; transaction_commitment : 'comm - // ; full_transaction_commitment : 'comm - // ; excess : 'signed_amount - // ; supply_increase : 'signed_amount - // ; ledger : 'ledger - // ; success : 'bool - // ; account_update_index : 'length - // ; failure_status_tbl : 'failure_status_tbl - // ; will_succeed : 'bool - // } - - let mut alls = [ - field::equal(*stack_frame, other.stack_frame, w), - field::equal(*call_stack, other.call_stack, w), - field::equal(*transaction_commitment, other.transaction_commitment, w), - field::equal( - *full_transaction_commitment, - other.full_transaction_commitment, - w, - ), - excess - .to_checked::() - .equal(&other.excess.to_checked(), w), - supply_increase - .to_checked::() - .equal(&other.supply_increase.to_checked(), w), - field::equal(*ledger, other.ledger, w), - success.to_boolean().equal(&other.success.to_boolean(), w), - account_update_index - .to_checked::() - .equal(&other.account_update_index.to_checked(), w), - Boolean::True, - will_succeed - .to_boolean() - .equal(&other.will_succeed.to_boolean(), w), - ]; - alls.reverse(); - alls - } - } -} - -fn step_all( - _constraint_constants: &ConstraintConstants, - f: &impl Fn(&mut A, &GlobalState, &LocalStateEnv), - user_acc: &mut A, - (g_state, l_state): (&mut GlobalState, &mut LocalStateEnv), -) -> Result>, String> -where - L: LedgerNonSnark, -{ - while !l_state.stack_frame.calls.is_empty() { - zkapps::non_snark::step(g_state, l_state)?; - f(user_acc, g_state, l_state); - } - Ok(l_state.failure_status_tbl.clone()) -} - -/// apply zkapp command fee payer's while stubbing out the second pass ledger -/// CAUTION: If you use the intermediate local states, you MUST update the -/// [`LocalStateEnv::will_succeed`] field to `false` if the `status` is [`TransactionStatus::Failed`].*) -pub fn apply_zkapp_command_first_pass_aux( - constraint_constants: &ConstraintConstants, - global_slot: Slot, - state_view: &ProtocolStateView, - init: &mut A, - f: F, - fee_excess: Option>, - supply_increase: Option>, - ledger: &mut L, - command: &ZkAppCommand, -) -> Result, String> -where - L: LedgerNonSnark, - F: Fn(&mut A, &GlobalState, &LocalStateEnv), -{ - let fee_excess = fee_excess.unwrap_or_else(Signed::zero); - let supply_increase = supply_increase.unwrap_or_else(Signed::zero); - - let previous_hash = ledger.merkle_root(); - let original_first_pass_account_states = { - let id = command.fee_payer(); - let location = { - let loc = ledger.location_of_account(&id); - let account = loc.as_ref().and_then(|loc| ledger.get(loc)); - loc.zip(account) - }; - - vec![(id, location)] - }; - // let perform = |eff: Eff| Env::perform(eff); - - let (mut global_state, mut local_state) = ( - GlobalState { - protocol_state: state_view.clone(), - first_pass_ledger: ledger.clone(), - second_pass_ledger: { - // We stub out the second_pass_ledger initially, and then poke the - // correct value in place after the first pass is finished. - ::empty(0) - }, - fee_excess, - supply_increase, - block_global_slot: global_slot, - }, - LocalStateEnv { - stack_frame: StackFrame::default(), - call_stack: CallStack::new(), - transaction_commitment: Fp::zero(), - full_transaction_commitment: Fp::zero(), - excess: Signed::::zero(), - supply_increase, - ledger: ::empty(0), - success: true, - account_update_index: Index::zero(), - failure_status_tbl: Vec::new(), - will_succeed: true, - }, - ); - - f(init, &global_state, &local_state); - let account_updates = command.all_account_updates(); - - zkapps::non_snark::start( - &mut global_state, - &mut local_state, - zkapps::non_snark::StartData { - account_updates, - memo_hash: command.memo.hash(), - // It's always valid to set this value to true, and it will - // have no effect outside of the snark. - will_succeed: true, - }, - )?; - - let command = command.clone(); - let constraint_constants = constraint_constants.clone(); - let state_view = state_view.clone(); - - let res = ZkappCommandPartiallyApplied { - command, - previous_hash, - original_first_pass_account_states, - constraint_constants, - state_view, - global_state, - local_state, - }; - - Ok(res) -} - -fn apply_zkapp_command_first_pass( - constraint_constants: &ConstraintConstants, - global_slot: Slot, - state_view: &ProtocolStateView, - fee_excess: Option>, - supply_increase: Option>, - ledger: &mut L, - command: &ZkAppCommand, -) -> Result, String> -where - L: LedgerNonSnark, -{ - let mut acc = (); - let partial_stmt = apply_zkapp_command_first_pass_aux( - constraint_constants, - global_slot, - state_view, - &mut acc, - |_acc, _g, _l| {}, - fee_excess, - supply_increase, - ledger, - command, - )?; - - Ok(partial_stmt) -} - -pub fn apply_zkapp_command_second_pass_aux( - constraint_constants: &ConstraintConstants, - init: &mut A, - f: F, - ledger: &mut L, - c: ZkappCommandPartiallyApplied, -) -> Result -where - L: LedgerNonSnark, - F: Fn(&mut A, &GlobalState, &LocalStateEnv), -{ - // let perform = |eff: Eff| Env::perform(eff); - - let original_account_states: Vec<(AccountId, Option<_>)> = { - // get the original states of all the accounts in each pass. - // If an account updated in the first pass is referenced in account - // updates, then retain the value before first pass application*) - - let accounts_referenced = c.command.accounts_referenced(); - - let mut account_states = BTreeMap::>::new(); - - let referenced = accounts_referenced.into_iter().map(|id| { - let location = { - let loc = ledger.location_of_account(&id); - let account = loc.as_ref().and_then(|loc| ledger.get(loc)); - loc.zip(account) - }; - (id, location) - }); - - c.original_first_pass_account_states - .into_iter() - .chain(referenced) - .for_each(|(id, acc_opt)| { - use std::collections::btree_map::Entry::Vacant; - - let id_with_order: AccountIdOrderable = id.into(); - if let Vacant(entry) = account_states.entry(id_with_order) { - entry.insert(acc_opt); - }; - }); - - account_states - .into_iter() - // Convert back the `AccountIdOrder` into `AccountId`, now that they are sorted - .map(|(id, account): (AccountIdOrderable, Option<_>)| (id.into(), account)) - .collect() - }; - - let mut account_states_after_fee_payer = { - // To check if the accounts remain unchanged in the event the transaction - // fails. First pass updates will remain even if the transaction fails to - // apply zkapp account updates*) - - c.command.accounts_referenced().into_iter().map(|id| { - let loc = ledger.location_of_account(&id); - let a = loc.as_ref().and_then(|loc| ledger.get(loc)); - - match a { - Some(a) => (id, Some((loc.unwrap(), a))), - None => (id, None), - } - }) - }; - - let accounts = || { - original_account_states - .iter() - .map(|(id, account)| (id.clone(), account.as_ref().map(|(_loc, acc)| acc.clone()))) - .collect::>() - }; - - // Warning(OCaml): This is an abstraction leak / hack. - // Here, we update global second pass ledger to be the input ledger, and - // then update the local ledger to be the input ledger *IF AND ONLY IF* - // there are more transaction segments to be processed in this pass. - - // TODO(OCaml): Remove this, and uplift the logic into the call in staged ledger. - - let mut global_state = GlobalState { - second_pass_ledger: ledger.clone(), - ..c.global_state - }; - - let mut local_state = { - if c.local_state.stack_frame.calls.is_empty() { - // Don't mess with the local state; we've already finished the - // transaction after the fee payer. - c.local_state - } else { - // Install the ledger that should already be in the local state, but - // may not be in some situations depending on who the caller is. - LocalStateEnv { - ledger: global_state.second_pass_ledger(), - ..c.local_state - } - } - }; - - f(init, &global_state, &local_state); - let start = (&mut global_state, &mut local_state); - - let reversed_failure_status_tbl = step_all(constraint_constants, &f, init, start)?; - - let failure_status_tbl = reversed_failure_status_tbl - .into_iter() - .rev() - .collect::>(); - - let account_ids_originally_not_in_ledger = - original_account_states - .iter() - .filter_map(|(acct_id, loc_and_acct)| { - if loc_and_acct.is_none() { - Some(acct_id) - } else { - None - } - }); - - let successfully_applied = failure_status_tbl.concat().is_empty(); - - // if the zkapp command fails in at least 1 account update, - // then all the account updates would be cancelled except - // the fee payer one - let failure_status_tbl = if successfully_applied { - failure_status_tbl - } else { - failure_status_tbl - .into_iter() - .enumerate() - .map(|(idx, fs)| { - if idx > 0 && fs.is_empty() { - vec![TransactionFailure::Cancelled] - } else { - fs - } - }) - .collect() - }; - - // accounts not originally in ledger, now present in ledger - let new_accounts = account_ids_originally_not_in_ledger - .filter(|acct_id| ledger.location_of_account(acct_id).is_some()) - .cloned() - .collect::>(); - - let new_accounts_is_empty = new_accounts.is_empty(); - - let valid_result = Ok(ZkappCommandApplied { - accounts: accounts(), - command: WithStatus { - data: c.command, - status: if successfully_applied { - TransactionStatus::Applied - } else { - TransactionStatus::Failed(failure_status_tbl) - }, - }, - new_accounts, - }); - - if successfully_applied { - valid_result - } else { - let other_account_update_accounts_unchanged = account_states_after_fee_payer - .fold_while(true, |acc, (_, loc_opt)| match loc_opt { - Some((loc, a)) => match ledger.get(&loc) { - Some(a_) if !(a == a_) => FoldWhile::Done(false), - _ => FoldWhile::Continue(acc), - }, - _ => FoldWhile::Continue(acc), - }) - .into_inner(); - - // Other zkapp_command failed, therefore, updates in those should not get applied - if new_accounts_is_empty && other_account_update_accounts_unchanged { - valid_result - } else { - Err("Zkapp_command application failed but new accounts created or some of the other account_update updates applied".to_string()) - } - } -} - -fn apply_zkapp_command_second_pass( - constraint_constants: &ConstraintConstants, - ledger: &mut L, - c: ZkappCommandPartiallyApplied, -) -> Result -where - L: LedgerNonSnark, -{ - let x = apply_zkapp_command_second_pass_aux( - constraint_constants, - &mut (), - |_, _, _| {}, - ledger, - c, - )?; - Ok(x) -} - -fn apply_zkapp_command_unchecked_aux( - constraint_constants: &ConstraintConstants, - global_slot: Slot, - state_view: &ProtocolStateView, - init: &mut A, - f: F, - fee_excess: Option>, - supply_increase: Option>, - ledger: &mut L, - command: &ZkAppCommand, -) -> Result -where - L: LedgerNonSnark, - F: Fn(&mut A, &GlobalState, &LocalStateEnv), -{ - let partial_stmt = apply_zkapp_command_first_pass_aux( - constraint_constants, - global_slot, - state_view, - init, - &f, - fee_excess, - supply_increase, - ledger, - command, - )?; - - apply_zkapp_command_second_pass_aux(constraint_constants, init, &f, ledger, partial_stmt) -} - -fn apply_zkapp_command_unchecked( - constraint_constants: &ConstraintConstants, - global_slot: Slot, - state_view: &ProtocolStateView, - ledger: &mut L, - command: &ZkAppCommand, -) -> Result<(ZkappCommandApplied, (LocalStateEnv, Signed)), String> -where - L: LedgerNonSnark, -{ - let zkapp_partially_applied: ZkappCommandPartiallyApplied = apply_zkapp_command_first_pass( - constraint_constants, - global_slot, - state_view, - None, - None, - ledger, - command, - )?; - - let mut state_res = None; - let account_update_applied = apply_zkapp_command_second_pass_aux( - constraint_constants, - &mut state_res, - |acc, global_state, local_state| { - *acc = Some((local_state.clone(), global_state.fee_excess)) - }, - ledger, - zkapp_partially_applied, - )?; - let (state, amount) = state_res.unwrap(); - - Ok((account_update_applied, (state.clone(), amount))) -} - +pub mod local_state; pub mod transaction_partially_applied { use super::{ transaction_applied::{CoinbaseApplied, FeeTransferApplied}, diff --git a/ledger/src/sparse_ledger/sparse_ledger.rs b/ledger/src/sparse_ledger/sparse_ledger.rs index 994e3b0f5..7a9fa4be6 100644 --- a/ledger/src/sparse_ledger/sparse_ledger.rs +++ b/ledger/src/sparse_ledger/sparse_ledger.rs @@ -13,8 +13,10 @@ use crate::{ conv::to_ledger_hash, currency::{Amount, Signed, Slot}, transaction_logic::{ - apply_zkapp_command_first_pass_aux, apply_zkapp_command_second_pass_aux, - local_state::LocalStateEnv, + local_state::{ + apply_zkapp_command_first_pass_aux, apply_zkapp_command_second_pass_aux, + LocalStateEnv, + }, protocol_state::{GlobalState, ProtocolStateView}, transaction_applied::ZkappCommandApplied, transaction_partially_applied::ZkappCommandPartiallyApplied, From 6c564ffc1b09c5510703ae79a3d4f42d71958c68 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 21:17:39 +0200 Subject: [PATCH 10/16] Ledger/transaction_logic: extract transaction_partially_applied module to separate file Split the large transaction_logic.rs file by extracting the transaction_partially_applied module (1076 lines) into its own file at ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs. Changes: - Created transaction_partially_applied.rs with ZkappCommandPartiallyApplied, TransactionPartiallyApplied, and FullyApplied types - Includes apply_transaction_first_pass/second_pass and apply_transactions functions - Includes apply_coinbase, apply_fee_transfer, apply_user_command and related helper functions - Added module declaration and re-exports in mod.rs for apply_transaction_first_pass, apply_transaction_second_pass, apply_transactions, apply_user_command, set_with_location, and AccountState - Removed self-import statement and extra closing brace from extracted file --- .../src/scan_state/transaction_logic/mod.rs | 1083 +---------------- .../transaction_partially_applied.rs | 1076 ++++++++++++++++ 2 files changed, 1081 insertions(+), 1078 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 6005900d0..469e4f994 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1039,1084 +1039,11 @@ pub mod transaction_applied; pub mod transaction_witness; pub mod protocol_state; pub mod local_state; -pub mod transaction_partially_applied { - use super::{ - transaction_applied::{CoinbaseApplied, FeeTransferApplied}, - *, - }; - - #[derive(Clone, Debug)] - pub struct ZkappCommandPartiallyApplied { - pub command: ZkAppCommand, - pub previous_hash: Fp, - pub original_first_pass_account_states: - Vec<(AccountId, Option<(L::Location, Box)>)>, - pub constraint_constants: ConstraintConstants, - pub state_view: ProtocolStateView, - pub global_state: GlobalState, - pub local_state: LocalStateEnv, - } - - #[derive(Clone, Debug)] - pub struct FullyApplied { - pub previous_hash: Fp, - pub applied: T, - } - - #[derive(Clone, Debug)] - pub enum TransactionPartiallyApplied { - SignedCommand(FullyApplied), - ZkappCommand(Box>), - FeeTransfer(FullyApplied), - Coinbase(FullyApplied), - } - - impl TransactionPartiallyApplied - where - L: LedgerNonSnark, - { - pub fn command(self) -> Transaction { - use Transaction as T; - - match self { - Self::SignedCommand(s) => T::Command(UserCommand::SignedCommand(Box::new( - s.applied.common.user_command.data, - ))), - Self::ZkappCommand(z) => T::Command(UserCommand::ZkAppCommand(Box::new(z.command))), - Self::FeeTransfer(ft) => T::FeeTransfer(ft.applied.fee_transfer.data), - Self::Coinbase(cb) => T::Coinbase(cb.applied.coinbase.data), - } - } - } -} - -use transaction_partially_applied::{TransactionPartiallyApplied, ZkappCommandPartiallyApplied}; - -pub fn apply_transaction_first_pass( - constraint_constants: &ConstraintConstants, - global_slot: Slot, - txn_state_view: &ProtocolStateView, - ledger: &mut L, - transaction: &Transaction, -) -> Result, String> -where - L: LedgerNonSnark, -{ - use Transaction::*; - use UserCommand::*; - - let previous_hash = ledger.merkle_root(); - let txn_global_slot = &global_slot; - - match transaction { - Command(SignedCommand(cmd)) => apply_user_command( - constraint_constants, - txn_state_view, - txn_global_slot, - ledger, - cmd, - ) - .map(|applied| { - TransactionPartiallyApplied::SignedCommand(FullyApplied { - previous_hash, - applied, - }) - }), - Command(ZkAppCommand(txn)) => apply_zkapp_command_first_pass( - constraint_constants, - global_slot, - txn_state_view, - None, - None, - ledger, - txn, - ) - .map(Box::new) - .map(TransactionPartiallyApplied::ZkappCommand), - FeeTransfer(fee_transfer) => { - apply_fee_transfer(constraint_constants, txn_global_slot, ledger, fee_transfer).map( - |applied| { - TransactionPartiallyApplied::FeeTransfer(FullyApplied { - previous_hash, - applied, - }) - }, - ) - } - Coinbase(coinbase) => { - apply_coinbase(constraint_constants, txn_global_slot, ledger, coinbase).map(|applied| { - TransactionPartiallyApplied::Coinbase(FullyApplied { - previous_hash, - applied, - }) - }) - } - } -} - -pub fn apply_transaction_second_pass( - constraint_constants: &ConstraintConstants, - ledger: &mut L, - partial_transaction: TransactionPartiallyApplied, -) -> Result -where - L: LedgerNonSnark, -{ - use TransactionPartiallyApplied as P; - - match partial_transaction { - P::SignedCommand(FullyApplied { - previous_hash, - applied, - }) => Ok(TransactionApplied { - previous_hash, - varying: Varying::Command(CommandApplied::SignedCommand(Box::new(applied))), - }), - P::ZkappCommand(partially_applied) => { - // TODO(OCaml): either here or in second phase of apply, need to update the - // prior global state statement for the fee payer segment to add the - // second phase ledger at the end - - let previous_hash = partially_applied.previous_hash; - let applied = - apply_zkapp_command_second_pass(constraint_constants, ledger, *partially_applied)?; - - Ok(TransactionApplied { - previous_hash, - varying: Varying::Command(CommandApplied::ZkappCommand(Box::new(applied))), - }) - } - P::FeeTransfer(FullyApplied { - previous_hash, - applied, - }) => Ok(TransactionApplied { - previous_hash, - varying: Varying::FeeTransfer(applied), - }), - P::Coinbase(FullyApplied { - previous_hash, - applied, - }) => Ok(TransactionApplied { - previous_hash, - varying: Varying::Coinbase(applied), - }), - } -} - -pub fn apply_transactions( - constraint_constants: &ConstraintConstants, - global_slot: Slot, - txn_state_view: &ProtocolStateView, - ledger: &mut L, - txns: &[Transaction], -) -> Result, String> -where - L: LedgerNonSnark, -{ - let first_pass: Vec<_> = txns - .iter() - .map(|txn| { - apply_transaction_first_pass( - constraint_constants, - global_slot, - txn_state_view, - ledger, - txn, - ) - }) - .collect::>, _>>()?; - - first_pass - .into_iter() - .map(|partial_transaction| { - apply_transaction_second_pass(constraint_constants, ledger, partial_transaction) - }) - .collect() -} - -struct FailureCollection { - inner: Vec>, -} - -/// -impl FailureCollection { - fn empty() -> Self { - Self { - inner: Vec::default(), - } - } - - fn no_failure() -> Vec { - vec![] - } - - /// - fn single_failure() -> Self { - Self { - inner: vec![vec![TransactionFailure::UpdateNotPermittedBalance]], - } - } - - fn update_failed() -> Vec { - vec![TransactionFailure::UpdateNotPermittedBalance] - } - - /// - fn append_entry(list: Vec, mut s: Self) -> Self { - if s.inner.is_empty() { - Self { inner: vec![list] } - } else { - s.inner.insert(1, list); - s - } - } - - fn is_empty(&self) -> bool { - self.inner.iter().all(Vec::is_empty) - } - - fn take(self) -> Vec> { - self.inner - } -} - -/// Structure of the failure status: -/// I. No fee transfer and coinbase transfer fails: `[[failure]]` -/// II. With fee transfer- -/// Both fee transfer and coinbase fails: -/// `[[failure-of-fee-transfer]; [failure-of-coinbase]]` -/// Fee transfer succeeds and coinbase fails: -/// `[[];[failure-of-coinbase]]` -/// Fee transfer fails and coinbase succeeds: -/// `[[failure-of-fee-transfer];[]]` -/// -/// -fn apply_coinbase( - constraint_constants: &ConstraintConstants, - txn_global_slot: &Slot, - ledger: &mut L, - coinbase: &Coinbase, -) -> Result -where - L: LedgerIntf, -{ - let Coinbase { - receiver, - amount: coinbase_amount, - fee_transfer, - } = &coinbase; - - let ( - receiver_reward, - new_accounts1, - transferee_update, - transferee_timing_prev, - failures1, - burned_tokens1, - ) = match fee_transfer { - None => ( - *coinbase_amount, - None, - None, - None, - FailureCollection::empty(), - Amount::zero(), - ), - Some( - ft @ CoinbaseFeeTransfer { - receiver_pk: transferee, - fee, - }, - ) => { - assert_ne!(transferee, receiver); - - let transferee_id = ft.receiver(); - let fee = Amount::of_fee(fee); - - let receiver_reward = coinbase_amount - .checked_sub(&fee) - .ok_or_else(|| "Coinbase fee transfer too large".to_string())?; - - let (transferee_account, action, can_receive) = - has_permission_to_receive(ledger, &transferee_id); - let new_accounts = get_new_accounts(action, transferee_id.clone()); - - let timing = update_timing_when_no_deduction(txn_global_slot, &transferee_account)?; - - let balance = { - let amount = sub_account_creation_fee(constraint_constants, action, fee)?; - add_amount(transferee_account.balance, amount)? - }; - - if can_receive.0 { - let (_, mut transferee_account, transferee_location) = - ledger.get_or_create(&transferee_id)?; - - transferee_account.balance = balance; - transferee_account.timing = timing; - - let timing = transferee_account.timing.clone(); - - ( - receiver_reward, - new_accounts, - Some((transferee_location, transferee_account)), - Some(timing), - FailureCollection::append_entry( - FailureCollection::no_failure(), - FailureCollection::empty(), - ), - Amount::zero(), - ) - } else { - ( - receiver_reward, - None, - None, - None, - FailureCollection::single_failure(), - fee, - ) - } - } - }; - - let receiver_id = AccountId::new(receiver.clone(), TokenId::default()); - let (receiver_account, action2, can_receive) = has_permission_to_receive(ledger, &receiver_id); - let new_accounts2 = get_new_accounts(action2, receiver_id.clone()); - - // Note: Updating coinbase receiver timing only if there is no fee transfer. - // This is so as to not add any extra constraints in transaction snark for checking - // "receiver" timings. This is OK because timing rules will not be violated when - // balance increases and will be checked whenever an amount is deducted from the - // account (#5973) - - let coinbase_receiver_timing = match transferee_timing_prev { - None => update_timing_when_no_deduction(txn_global_slot, &receiver_account)?, - Some(_) => receiver_account.timing.clone(), - }; - - let receiver_balance = { - let amount = sub_account_creation_fee(constraint_constants, action2, receiver_reward)?; - add_amount(receiver_account.balance, amount)? - }; - - let (failures, burned_tokens2) = if can_receive.0 { - let (_action2, mut receiver_account, receiver_location) = - ledger.get_or_create(&receiver_id)?; - - receiver_account.balance = receiver_balance; - receiver_account.timing = coinbase_receiver_timing; - - ledger.set(&receiver_location, receiver_account); - - ( - FailureCollection::append_entry(FailureCollection::no_failure(), failures1), - Amount::zero(), - ) - } else { - ( - FailureCollection::append_entry(FailureCollection::update_failed(), failures1), - receiver_reward, - ) - }; - - if let Some((addr, account)) = transferee_update { - ledger.set(&addr, account); - }; - - let burned_tokens = burned_tokens1 - .checked_add(&burned_tokens2) - .ok_or_else(|| "burned tokens overflow".to_string())?; - - let status = if failures.is_empty() { - TransactionStatus::Applied - } else { - TransactionStatus::Failed(failures.take()) - }; - - let new_accounts: Vec<_> = [new_accounts1, new_accounts2] - .into_iter() - .flatten() - .collect(); - - Ok(transaction_applied::CoinbaseApplied { - coinbase: WithStatus { - data: coinbase.clone(), - status, - }, - new_accounts, - burned_tokens, - }) -} - -/// -fn apply_fee_transfer( - constraint_constants: &ConstraintConstants, - txn_global_slot: &Slot, - ledger: &mut L, - fee_transfer: &FeeTransfer, -) -> Result -where - L: LedgerIntf, -{ - let (new_accounts, failures, burned_tokens) = process_fee_transfer( - ledger, - fee_transfer, - |action, _, balance, fee| { - let amount = { - let amount = Amount::of_fee(fee); - sub_account_creation_fee(constraint_constants, action, amount)? - }; - add_amount(balance, amount) - }, - |account| update_timing_when_no_deduction(txn_global_slot, account), - )?; - - let status = if failures.is_empty() { - TransactionStatus::Applied - } else { - TransactionStatus::Failed(failures.take()) - }; - - Ok(transaction_applied::FeeTransferApplied { - fee_transfer: WithStatus { - data: fee_transfer.clone(), - status, - }, - new_accounts, - burned_tokens, - }) -} - -/// -fn sub_account_creation_fee( - constraint_constants: &ConstraintConstants, - action: AccountState, - amount: Amount, -) -> Result { - let account_creation_fee = Amount::from_u64(constraint_constants.account_creation_fee); - - match action { - AccountState::Added => { - if let Some(amount) = amount.checked_sub(&account_creation_fee) { - return Ok(amount); - } - Err(format!( - "Error subtracting account creation fee {:?}; transaction amount {:?} insufficient", - account_creation_fee, amount - )) - } - AccountState::Existed => Ok(amount), - } -} - -fn update_timing_when_no_deduction( - txn_global_slot: &Slot, - account: &Account, -) -> Result { - validate_timing(account, Amount::zero(), txn_global_slot) -} - -// /// TODO: Move this to the ledger -// /// -// fn get_or_create( -// ledger: &mut L, -// account_id: &AccountId, -// ) -> Result<(AccountState, Account, Address), String> -// where -// L: LedgerIntf, -// { -// let location = ledger -// .get_or_create_account(account_id.clone(), Account::initialize(account_id)) -// .map_err(|e| format!("{:?}", e))?; - -// let action = match location { -// GetOrCreated::Added(_) => AccountState::Added, -// GetOrCreated::Existed(_) => AccountState::Existed, -// }; - -// let addr = location.addr(); - -// let account = ledger -// .get(addr.clone()) -// .expect("get_or_create: Account was not found in the ledger after creation"); - -// Ok((action, account, addr)) -// } - -fn get_new_accounts(action: AccountState, data: T) -> Option { - match action { - AccountState::Added => Some(data), - AccountState::Existed => None, - } -} - -/// Structure of the failure status: -/// I. Only one fee transfer in the transaction (`One) and it fails: -/// [[failure]] -/// II. Two fee transfers in the transaction (`Two)- -/// Both fee transfers fail: -/// [[failure-of-first-fee-transfer]; [failure-of-second-fee-transfer]] -/// First succeeds and second one fails: -/// [[];[failure-of-second-fee-transfer]] -/// First fails and second succeeds: -/// [[failure-of-first-fee-transfer];[]] -fn process_fee_transfer( - ledger: &mut L, - fee_transfer: &FeeTransfer, - modify_balance: FunBalance, - modify_timing: FunTiming, -) -> Result<(Vec, FailureCollection, Amount), String> -where - L: LedgerIntf, - FunTiming: Fn(&Account) -> Result, - FunBalance: Fn(AccountState, &AccountId, Balance, &Fee) -> Result, -{ - if !fee_transfer.fee_tokens().all(TokenId::is_default) { - return Err("Cannot pay fees in non-default tokens.".to_string()); - } - - match &**fee_transfer { - OneOrTwo::One(fee_transfer) => { - let account_id = fee_transfer.receiver(); - let (a, action, can_receive) = has_permission_to_receive(ledger, &account_id); - - let timing = modify_timing(&a)?; - let balance = modify_balance(action, &account_id, a.balance, &fee_transfer.fee)?; - - if can_receive.0 { - let (_, mut account, loc) = ledger.get_or_create(&account_id)?; - let new_accounts = get_new_accounts(action, account_id.clone()); - - account.balance = balance; - account.timing = timing; - - ledger.set(&loc, account); - - let new_accounts: Vec<_> = new_accounts.into_iter().collect(); - Ok((new_accounts, FailureCollection::empty(), Amount::zero())) - } else { - Ok(( - vec![], - FailureCollection::single_failure(), - Amount::of_fee(&fee_transfer.fee), - )) - } - } - OneOrTwo::Two((fee_transfer1, fee_transfer2)) => { - let account_id1 = fee_transfer1.receiver(); - let (a1, action1, can_receive1) = has_permission_to_receive(ledger, &account_id1); - - let account_id2 = fee_transfer2.receiver(); - - if account_id1 == account_id2 { - let fee = fee_transfer1 - .fee - .checked_add(&fee_transfer2.fee) - .ok_or_else(|| "Overflow".to_string())?; - - let timing = modify_timing(&a1)?; - let balance = modify_balance(action1, &account_id1, a1.balance, &fee)?; - - if can_receive1.0 { - let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?; - let new_accounts1 = get_new_accounts(action1, account_id1); - - a1.balance = balance; - a1.timing = timing; - - ledger.set(&l1, a1); - - let new_accounts: Vec<_> = new_accounts1.into_iter().collect(); - Ok((new_accounts, FailureCollection::empty(), Amount::zero())) - } else { - // failure for each fee transfer single - - Ok(( - vec![], - FailureCollection::append_entry( - FailureCollection::update_failed(), - FailureCollection::single_failure(), - ), - Amount::of_fee(&fee), - )) - } - } else { - let (a2, action2, can_receive2) = has_permission_to_receive(ledger, &account_id2); - - let balance1 = - modify_balance(action1, &account_id1, a1.balance, &fee_transfer1.fee)?; - - // Note: Not updating the timing field of a1 to avoid additional check - // in transactions snark (check_timing for "receiver"). This is OK - // because timing rules will not be violated when balance increases - // and will be checked whenever an amount is deducted from the account. (#5973)*) - - let timing2 = modify_timing(&a2)?; - let balance2 = - modify_balance(action2, &account_id2, a2.balance, &fee_transfer2.fee)?; - - let (new_accounts1, failures, burned_tokens1) = if can_receive1.0 { - let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?; - let new_accounts1 = get_new_accounts(action1, account_id1); - - a1.balance = balance1; - ledger.set(&l1, a1); - - ( - new_accounts1, - FailureCollection::append_entry( - FailureCollection::no_failure(), - FailureCollection::empty(), - ), - Amount::zero(), - ) - } else { - ( - None, - FailureCollection::single_failure(), - Amount::of_fee(&fee_transfer1.fee), - ) - }; - - let (new_accounts2, failures, burned_tokens2) = if can_receive2.0 { - let (_, mut a2, l2) = ledger.get_or_create(&account_id2)?; - let new_accounts2 = get_new_accounts(action2, account_id2); - - a2.balance = balance2; - a2.timing = timing2; - - ledger.set(&l2, a2); - - ( - new_accounts2, - FailureCollection::append_entry(FailureCollection::no_failure(), failures), - Amount::zero(), - ) - } else { - ( - None, - FailureCollection::append_entry( - FailureCollection::update_failed(), - failures, - ), - Amount::of_fee(&fee_transfer2.fee), - ) - }; - - let burned_tokens = burned_tokens1 - .checked_add(&burned_tokens2) - .ok_or_else(|| "burned tokens overflow".to_string())?; - - let new_accounts: Vec<_> = [new_accounts1, new_accounts2] - .into_iter() - .flatten() - .collect(); - - Ok((new_accounts, failures, burned_tokens)) - } - } - } -} - -#[derive(Copy, Clone, Debug)] -pub enum AccountState { - Added, - Existed, -} - -#[derive(Debug)] -struct HasPermissionToReceive(bool); - -/// -fn has_permission_to_receive( - ledger: &mut L, - receiver_account_id: &AccountId, -) -> (Box, AccountState, HasPermissionToReceive) -where - L: LedgerIntf, -{ - use crate::PermissionTo::*; - use AccountState::*; - - let init_account = Account::initialize(receiver_account_id); - - match ledger.location_of_account(receiver_account_id) { - None => { - // new account, check that default permissions allow receiving - let perm = init_account.has_permission_to(ControlTag::NoneGiven, Receive); - (Box::new(init_account), Added, HasPermissionToReceive(perm)) - } - Some(location) => match ledger.get(&location) { - None => panic!("Ledger location with no account"), - Some(receiver_account) => { - let perm = receiver_account.has_permission_to(ControlTag::NoneGiven, Receive); - (receiver_account, Existed, HasPermissionToReceive(perm)) - } - }, - } -} - -pub fn validate_time(valid_until: &Slot, current_global_slot: &Slot) -> Result<(), String> { - if current_global_slot <= valid_until { - return Ok(()); - } - - Err(format!( - "Current global slot {:?} greater than transaction expiry slot {:?}", - current_global_slot, valid_until - )) -} - -pub fn is_timed(a: &Account) -> bool { - matches!(&a.timing, Timing::Timed { .. }) -} - -pub fn set_with_location( - ledger: &mut L, - location: &ExistingOrNew, - account: Box, -) -> Result<(), String> -where - L: LedgerIntf, -{ - match location { - ExistingOrNew::Existing(location) => { - ledger.set(location, account); - Ok(()) - } - ExistingOrNew::New => ledger - .create_new_account(account.id(), *account) - .map_err(|_| "set_with_location".to_string()), - } -} - -pub struct Updates { - pub located_accounts: Vec<(ExistingOrNew, Box)>, - pub applied_body: signed_command_applied::Body, -} - -pub fn compute_updates( - constraint_constants: &ConstraintConstants, - receiver: AccountId, - ledger: &mut L, - current_global_slot: &Slot, - user_command: &SignedCommand, - fee_payer: &AccountId, - fee_payer_account: &Account, - fee_payer_location: &ExistingOrNew, - reject_command: &mut bool, -) -> Result, TransactionFailure> -where - L: LedgerIntf, -{ - match &user_command.payload.body { - signed_command::Body::StakeDelegation(_) => { - let (receiver_location, _) = get_with_location(ledger, &receiver).unwrap(); - - if let ExistingOrNew::New = receiver_location { - return Err(TransactionFailure::ReceiverNotPresent); - } - if !fee_payer_account.has_permission_to_set_delegate() { - return Err(TransactionFailure::UpdateNotPermittedDelegate); - } - - let previous_delegate = fee_payer_account.delegate.clone(); - - // Timing is always valid, but we need to record any switch from - // timed to untimed here to stay in sync with the snark. - let fee_payer_account = { - let timing = timing_error_to_user_command_status(validate_timing( - fee_payer_account, - Amount::zero(), - current_global_slot, - ))?; - - Box::new(Account { - delegate: Some(receiver.public_key.clone()), - timing, - ..fee_payer_account.clone() - }) - }; - - Ok(Updates { - located_accounts: vec![(fee_payer_location.clone(), fee_payer_account)], - applied_body: signed_command_applied::Body::StakeDelegation { previous_delegate }, - }) - } - signed_command::Body::Payment(payment) => { - let get_fee_payer_account = || { - let balance = fee_payer_account - .balance - .sub_amount(payment.amount) - .ok_or(TransactionFailure::SourceInsufficientBalance)?; - - let timing = timing_error_to_user_command_status(validate_timing( - fee_payer_account, - payment.amount, - current_global_slot, - ))?; - - Ok(Box::new(Account { - balance, - timing, - ..fee_payer_account.clone() - })) - }; - - let fee_payer_account = match get_fee_payer_account() { - Ok(fee_payer_account) => fee_payer_account, - Err(e) => { - // OCaml throw an exception when an error occurs here - // Here in Rust we set `reject_command` to differentiate the 3 cases (Ok, Err, exception) - // - // - - // Don't accept transactions with insufficient balance from the fee-payer. - // TODO(OCaml): eliminate this condition and accept transaction with failed status - *reject_command = true; - return Err(e); - } - }; - - let (receiver_location, mut receiver_account) = if fee_payer == &receiver { - (fee_payer_location.clone(), fee_payer_account.clone()) - } else { - get_with_location(ledger, &receiver).unwrap() - }; - - if !fee_payer_account.has_permission_to_send() { - return Err(TransactionFailure::UpdateNotPermittedBalance); - } - - if !receiver_account.has_permission_to_receive() { - return Err(TransactionFailure::UpdateNotPermittedBalance); - } - - let receiver_amount = match &receiver_location { - ExistingOrNew::Existing(_) => payment.amount, - ExistingOrNew::New => { - match payment - .amount - .checked_sub(&Amount::from_u64(constraint_constants.account_creation_fee)) - { - Some(amount) => amount, - None => return Err(TransactionFailure::AmountInsufficientToCreateAccount), - } - } - }; - - let balance = match receiver_account.balance.add_amount(receiver_amount) { - Some(balance) => balance, - None => return Err(TransactionFailure::Overflow), - }; - - let new_accounts = match receiver_location { - ExistingOrNew::New => vec![receiver.clone()], - ExistingOrNew::Existing(_) => vec![], - }; - - receiver_account.balance = balance; - - let updated_accounts = if fee_payer == &receiver { - // [receiver_account] at this point has all the updates - vec![(receiver_location, receiver_account)] - } else { - vec![ - (receiver_location, receiver_account), - (fee_payer_location.clone(), fee_payer_account), - ] - }; - - Ok(Updates { - located_accounts: updated_accounts, - applied_body: signed_command_applied::Body::Payments { new_accounts }, - }) - } - } -} - -pub fn apply_user_command_unchecked( - constraint_constants: &ConstraintConstants, - _txn_state_view: &ProtocolStateView, - txn_global_slot: &Slot, - ledger: &mut L, - user_command: &SignedCommand, -) -> Result -where - L: LedgerIntf, -{ - let SignedCommand { - payload: _, - signer: signer_pk, - signature: _, - } = &user_command; - let current_global_slot = txn_global_slot; - - let valid_until = user_command.valid_until(); - validate_time(&valid_until, current_global_slot)?; - - // Fee-payer information - let fee_payer = user_command.fee_payer(); - let (fee_payer_location, fee_payer_account) = - pay_fee(user_command, signer_pk, ledger, current_global_slot)?; - - if !fee_payer_account.has_permission_to_send() { - return Err(TransactionFailure::UpdateNotPermittedBalance.to_string()); - } - if !fee_payer_account.has_permission_to_increment_nonce() { - return Err(TransactionFailure::UpdateNotPermittedNonce.to_string()); - } - - // Charge the fee. This must happen, whether or not the command itself - // succeeds, to ensure that the network is compensated for processing this - // command. - set_with_location(ledger, &fee_payer_location, fee_payer_account.clone())?; - - let receiver = user_command.receiver(); - - let mut reject_command = false; - - match compute_updates( - constraint_constants, - receiver, - ledger, - current_global_slot, - user_command, - &fee_payer, - &fee_payer_account, - &fee_payer_location, - &mut reject_command, - ) { - Ok(Updates { - located_accounts, - applied_body, - }) => { - for (location, account) in located_accounts { - set_with_location(ledger, &location, account)?; - } - - Ok(SignedCommandApplied { - common: signed_command_applied::Common { - user_command: WithStatus:: { - data: user_command.clone(), - status: TransactionStatus::Applied, - }, - }, - body: applied_body, - }) - } - Err(failure) if !reject_command => Ok(SignedCommandApplied { - common: signed_command_applied::Common { - user_command: WithStatus:: { - data: user_command.clone(), - status: TransactionStatus::Failed(vec![vec![failure]]), - }, - }, - body: signed_command_applied::Body::Failed, - }), - Err(failure) => { - // This case occurs when an exception is throwned in OCaml - // - assert!(reject_command); - Err(failure.to_string()) - } - } -} - -pub fn apply_user_command( - constraint_constants: &ConstraintConstants, - txn_state_view: &ProtocolStateView, - txn_global_slot: &Slot, - ledger: &mut L, - user_command: &SignedCommand, -) -> Result -where - L: LedgerIntf, -{ - apply_user_command_unchecked( - constraint_constants, - txn_state_view, - txn_global_slot, - ledger, - user_command, - ) -} - -pub fn pay_fee( - user_command: &SignedCommand, - signer_pk: &CompressedPubKey, - ledger: &mut L, - current_global_slot: &Slot, -) -> Result<(ExistingOrNew, Box), String> -where - L: LedgerIntf, -{ - let nonce = user_command.nonce(); - let fee_payer = user_command.fee_payer(); - let fee_token = user_command.fee_token(); - - if &fee_payer.public_key != signer_pk { - return Err("Cannot pay fees from a public key that did not sign the transaction".into()); - } - - if fee_token != TokenId::default() { - return Err("Cannot create transactions with fee_token different from the default".into()); - } - - pay_fee_impl( - &user_command.payload, - nonce, - fee_payer, - user_command.fee(), - ledger, - current_global_slot, - ) -} - -fn pay_fee_impl( - command: &SignedCommandPayload, - nonce: Nonce, - fee_payer: AccountId, - fee: Fee, - ledger: &mut L, - current_global_slot: &Slot, -) -> Result<(ExistingOrNew, Box), String> -where - L: LedgerIntf, -{ - // Fee-payer information - let (location, mut account) = get_with_location(ledger, &fee_payer)?; - - if let ExistingOrNew::New = location { - return Err("The fee-payer account does not exist".to_string()); - }; - - let fee = Amount::of_fee(&fee); - let balance = sub_amount(account.balance, fee)?; - - validate_nonces(nonce, account.nonce)?; - let timing = validate_timing(&account, fee, current_global_slot)?; - - account.balance = balance; - account.nonce = account.nonce.incr(); // TODO: Not sure if OCaml wraps - account.receipt_chain_hash = cons_signed_command_payload(command, account.receipt_chain_hash); - account.timing = timing; - - Ok((location, account)) - - // in - // ( location - // , { account with - // balance - // ; nonce = Account.Nonce.succ account.nonce - // ; receipt_chain_hash = - // Receipt.Chain_hash.cons_signed_command_payload command - // account.receipt_chain_hash - // ; timing - // } ) -} +pub mod transaction_partially_applied; +pub use transaction_partially_applied::{ + apply_transaction_first_pass, apply_transaction_second_pass, apply_transactions, + apply_user_command, set_with_location, AccountState, +}; pub mod transaction_union_payload { use ark_ff::PrimeField; diff --git a/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs new file mode 100644 index 000000000..b45d1b13c --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs @@ -0,0 +1,1076 @@ +use super::{ + transaction_applied::{CoinbaseApplied, FeeTransferApplied}, + *, +}; + +#[derive(Clone, Debug)] +pub struct ZkappCommandPartiallyApplied { + pub command: ZkAppCommand, + pub previous_hash: Fp, + pub original_first_pass_account_states: + Vec<(AccountId, Option<(L::Location, Box)>)>, + pub constraint_constants: ConstraintConstants, + pub state_view: ProtocolStateView, + pub global_state: GlobalState, + pub local_state: LocalStateEnv, +} + +#[derive(Clone, Debug)] +pub struct FullyApplied { + pub previous_hash: Fp, + pub applied: T, +} + +#[derive(Clone, Debug)] +pub enum TransactionPartiallyApplied { + SignedCommand(FullyApplied), + ZkappCommand(Box>), + FeeTransfer(FullyApplied), + Coinbase(FullyApplied), +} + +impl TransactionPartiallyApplied +where + L: LedgerNonSnark, +{ + pub fn command(self) -> Transaction { + use Transaction as T; + + match self { + Self::SignedCommand(s) => T::Command(UserCommand::SignedCommand(Box::new( + s.applied.common.user_command.data, + ))), + Self::ZkappCommand(z) => T::Command(UserCommand::ZkAppCommand(Box::new(z.command))), + Self::FeeTransfer(ft) => T::FeeTransfer(ft.applied.fee_transfer.data), + Self::Coinbase(cb) => T::Coinbase(cb.applied.coinbase.data), + } + } +} + + +pub fn apply_transaction_first_pass( +constraint_constants: &ConstraintConstants, +global_slot: Slot, +txn_state_view: &ProtocolStateView, +ledger: &mut L, +transaction: &Transaction, +) -> Result, String> +where +L: LedgerNonSnark, +{ +use Transaction::*; +use UserCommand::*; + +let previous_hash = ledger.merkle_root(); +let txn_global_slot = &global_slot; + +match transaction { + Command(SignedCommand(cmd)) => apply_user_command( + constraint_constants, + txn_state_view, + txn_global_slot, + ledger, + cmd, + ) + .map(|applied| { + TransactionPartiallyApplied::SignedCommand(FullyApplied { + previous_hash, + applied, + }) + }), + Command(ZkAppCommand(txn)) => apply_zkapp_command_first_pass( + constraint_constants, + global_slot, + txn_state_view, + None, + None, + ledger, + txn, + ) + .map(Box::new) + .map(TransactionPartiallyApplied::ZkappCommand), + FeeTransfer(fee_transfer) => { + apply_fee_transfer(constraint_constants, txn_global_slot, ledger, fee_transfer).map( + |applied| { + TransactionPartiallyApplied::FeeTransfer(FullyApplied { + previous_hash, + applied, + }) + }, + ) + } + Coinbase(coinbase) => { + apply_coinbase(constraint_constants, txn_global_slot, ledger, coinbase).map(|applied| { + TransactionPartiallyApplied::Coinbase(FullyApplied { + previous_hash, + applied, + }) + }) + } +} +} + +pub fn apply_transaction_second_pass( +constraint_constants: &ConstraintConstants, +ledger: &mut L, +partial_transaction: TransactionPartiallyApplied, +) -> Result +where +L: LedgerNonSnark, +{ +use TransactionPartiallyApplied as P; + +match partial_transaction { + P::SignedCommand(FullyApplied { + previous_hash, + applied, + }) => Ok(TransactionApplied { + previous_hash, + varying: Varying::Command(CommandApplied::SignedCommand(Box::new(applied))), + }), + P::ZkappCommand(partially_applied) => { + // TODO(OCaml): either here or in second phase of apply, need to update the + // prior global state statement for the fee payer segment to add the + // second phase ledger at the end + + let previous_hash = partially_applied.previous_hash; + let applied = + apply_zkapp_command_second_pass(constraint_constants, ledger, *partially_applied)?; + + Ok(TransactionApplied { + previous_hash, + varying: Varying::Command(CommandApplied::ZkappCommand(Box::new(applied))), + }) + } + P::FeeTransfer(FullyApplied { + previous_hash, + applied, + }) => Ok(TransactionApplied { + previous_hash, + varying: Varying::FeeTransfer(applied), + }), + P::Coinbase(FullyApplied { + previous_hash, + applied, + }) => Ok(TransactionApplied { + previous_hash, + varying: Varying::Coinbase(applied), + }), +} +} + +pub fn apply_transactions( +constraint_constants: &ConstraintConstants, +global_slot: Slot, +txn_state_view: &ProtocolStateView, +ledger: &mut L, +txns: &[Transaction], +) -> Result, String> +where +L: LedgerNonSnark, +{ +let first_pass: Vec<_> = txns + .iter() + .map(|txn| { + apply_transaction_first_pass( + constraint_constants, + global_slot, + txn_state_view, + ledger, + txn, + ) + }) + .collect::>, _>>()?; + +first_pass + .into_iter() + .map(|partial_transaction| { + apply_transaction_second_pass(constraint_constants, ledger, partial_transaction) + }) + .collect() +} + +struct FailureCollection { +inner: Vec>, +} + +/// +impl FailureCollection { +fn empty() -> Self { + Self { + inner: Vec::default(), + } +} + +fn no_failure() -> Vec { + vec![] +} + +/// +fn single_failure() -> Self { + Self { + inner: vec![vec![TransactionFailure::UpdateNotPermittedBalance]], + } +} + +fn update_failed() -> Vec { + vec![TransactionFailure::UpdateNotPermittedBalance] +} + +/// +fn append_entry(list: Vec, mut s: Self) -> Self { + if s.inner.is_empty() { + Self { inner: vec![list] } + } else { + s.inner.insert(1, list); + s + } +} + +fn is_empty(&self) -> bool { + self.inner.iter().all(Vec::is_empty) +} + +fn take(self) -> Vec> { + self.inner +} +} + +/// Structure of the failure status: +/// I. No fee transfer and coinbase transfer fails: `[[failure]]` +/// II. With fee transfer- +/// Both fee transfer and coinbase fails: +/// `[[failure-of-fee-transfer]; [failure-of-coinbase]]` +/// Fee transfer succeeds and coinbase fails: +/// `[[];[failure-of-coinbase]]` +/// Fee transfer fails and coinbase succeeds: +/// `[[failure-of-fee-transfer];[]]` +/// +/// +fn apply_coinbase( +constraint_constants: &ConstraintConstants, +txn_global_slot: &Slot, +ledger: &mut L, +coinbase: &Coinbase, +) -> Result +where +L: LedgerIntf, +{ +let Coinbase { + receiver, + amount: coinbase_amount, + fee_transfer, +} = &coinbase; + +let ( + receiver_reward, + new_accounts1, + transferee_update, + transferee_timing_prev, + failures1, + burned_tokens1, +) = match fee_transfer { + None => ( + *coinbase_amount, + None, + None, + None, + FailureCollection::empty(), + Amount::zero(), + ), + Some( + ft @ CoinbaseFeeTransfer { + receiver_pk: transferee, + fee, + }, + ) => { + assert_ne!(transferee, receiver); + + let transferee_id = ft.receiver(); + let fee = Amount::of_fee(fee); + + let receiver_reward = coinbase_amount + .checked_sub(&fee) + .ok_or_else(|| "Coinbase fee transfer too large".to_string())?; + + let (transferee_account, action, can_receive) = + has_permission_to_receive(ledger, &transferee_id); + let new_accounts = get_new_accounts(action, transferee_id.clone()); + + let timing = update_timing_when_no_deduction(txn_global_slot, &transferee_account)?; + + let balance = { + let amount = sub_account_creation_fee(constraint_constants, action, fee)?; + add_amount(transferee_account.balance, amount)? + }; + + if can_receive.0 { + let (_, mut transferee_account, transferee_location) = + ledger.get_or_create(&transferee_id)?; + + transferee_account.balance = balance; + transferee_account.timing = timing; + + let timing = transferee_account.timing.clone(); + + ( + receiver_reward, + new_accounts, + Some((transferee_location, transferee_account)), + Some(timing), + FailureCollection::append_entry( + FailureCollection::no_failure(), + FailureCollection::empty(), + ), + Amount::zero(), + ) + } else { + ( + receiver_reward, + None, + None, + None, + FailureCollection::single_failure(), + fee, + ) + } + } +}; + +let receiver_id = AccountId::new(receiver.clone(), TokenId::default()); +let (receiver_account, action2, can_receive) = has_permission_to_receive(ledger, &receiver_id); +let new_accounts2 = get_new_accounts(action2, receiver_id.clone()); + +// Note: Updating coinbase receiver timing only if there is no fee transfer. +// This is so as to not add any extra constraints in transaction snark for checking +// "receiver" timings. This is OK because timing rules will not be violated when +// balance increases and will be checked whenever an amount is deducted from the +// account (#5973) + +let coinbase_receiver_timing = match transferee_timing_prev { + None => update_timing_when_no_deduction(txn_global_slot, &receiver_account)?, + Some(_) => receiver_account.timing.clone(), +}; + +let receiver_balance = { + let amount = sub_account_creation_fee(constraint_constants, action2, receiver_reward)?; + add_amount(receiver_account.balance, amount)? +}; + +let (failures, burned_tokens2) = if can_receive.0 { + let (_action2, mut receiver_account, receiver_location) = + ledger.get_or_create(&receiver_id)?; + + receiver_account.balance = receiver_balance; + receiver_account.timing = coinbase_receiver_timing; + + ledger.set(&receiver_location, receiver_account); + + ( + FailureCollection::append_entry(FailureCollection::no_failure(), failures1), + Amount::zero(), + ) +} else { + ( + FailureCollection::append_entry(FailureCollection::update_failed(), failures1), + receiver_reward, + ) +}; + +if let Some((addr, account)) = transferee_update { + ledger.set(&addr, account); +}; + +let burned_tokens = burned_tokens1 + .checked_add(&burned_tokens2) + .ok_or_else(|| "burned tokens overflow".to_string())?; + +let status = if failures.is_empty() { + TransactionStatus::Applied +} else { + TransactionStatus::Failed(failures.take()) +}; + +let new_accounts: Vec<_> = [new_accounts1, new_accounts2] + .into_iter() + .flatten() + .collect(); + +Ok(transaction_applied::CoinbaseApplied { + coinbase: WithStatus { + data: coinbase.clone(), + status, + }, + new_accounts, + burned_tokens, +}) +} + +/// +fn apply_fee_transfer( +constraint_constants: &ConstraintConstants, +txn_global_slot: &Slot, +ledger: &mut L, +fee_transfer: &FeeTransfer, +) -> Result +where +L: LedgerIntf, +{ +let (new_accounts, failures, burned_tokens) = process_fee_transfer( + ledger, + fee_transfer, + |action, _, balance, fee| { + let amount = { + let amount = Amount::of_fee(fee); + sub_account_creation_fee(constraint_constants, action, amount)? + }; + add_amount(balance, amount) + }, + |account| update_timing_when_no_deduction(txn_global_slot, account), +)?; + +let status = if failures.is_empty() { + TransactionStatus::Applied +} else { + TransactionStatus::Failed(failures.take()) +}; + +Ok(transaction_applied::FeeTransferApplied { + fee_transfer: WithStatus { + data: fee_transfer.clone(), + status, + }, + new_accounts, + burned_tokens, +}) +} + +/// +fn sub_account_creation_fee( +constraint_constants: &ConstraintConstants, +action: AccountState, +amount: Amount, +) -> Result { +let account_creation_fee = Amount::from_u64(constraint_constants.account_creation_fee); + +match action { + AccountState::Added => { + if let Some(amount) = amount.checked_sub(&account_creation_fee) { + return Ok(amount); + } + Err(format!( + "Error subtracting account creation fee {:?}; transaction amount {:?} insufficient", + account_creation_fee, amount + )) + } + AccountState::Existed => Ok(amount), +} +} + +fn update_timing_when_no_deduction( +txn_global_slot: &Slot, +account: &Account, +) -> Result { +validate_timing(account, Amount::zero(), txn_global_slot) +} + +// /// TODO: Move this to the ledger +// /// +// fn get_or_create( +// ledger: &mut L, +// account_id: &AccountId, +// ) -> Result<(AccountState, Account, Address), String> +// where +// L: LedgerIntf, +// { +// let location = ledger +// .get_or_create_account(account_id.clone(), Account::initialize(account_id)) +// .map_err(|e| format!("{:?}", e))?; + +// let action = match location { +// GetOrCreated::Added(_) => AccountState::Added, +// GetOrCreated::Existed(_) => AccountState::Existed, +// }; + +// let addr = location.addr(); + +// let account = ledger +// .get(addr.clone()) +// .expect("get_or_create: Account was not found in the ledger after creation"); + +// Ok((action, account, addr)) +// } + +fn get_new_accounts(action: AccountState, data: T) -> Option { +match action { + AccountState::Added => Some(data), + AccountState::Existed => None, +} +} + +/// Structure of the failure status: +/// I. Only one fee transfer in the transaction (`One) and it fails: +/// [[failure]] +/// II. Two fee transfers in the transaction (`Two)- +/// Both fee transfers fail: +/// [[failure-of-first-fee-transfer]; [failure-of-second-fee-transfer]] +/// First succeeds and second one fails: +/// [[];[failure-of-second-fee-transfer]] +/// First fails and second succeeds: +/// [[failure-of-first-fee-transfer];[]] +fn process_fee_transfer( +ledger: &mut L, +fee_transfer: &FeeTransfer, +modify_balance: FunBalance, +modify_timing: FunTiming, +) -> Result<(Vec, FailureCollection, Amount), String> +where +L: LedgerIntf, +FunTiming: Fn(&Account) -> Result, +FunBalance: Fn(AccountState, &AccountId, Balance, &Fee) -> Result, +{ +if !fee_transfer.fee_tokens().all(TokenId::is_default) { + return Err("Cannot pay fees in non-default tokens.".to_string()); +} + +match &**fee_transfer { + OneOrTwo::One(fee_transfer) => { + let account_id = fee_transfer.receiver(); + let (a, action, can_receive) = has_permission_to_receive(ledger, &account_id); + + let timing = modify_timing(&a)?; + let balance = modify_balance(action, &account_id, a.balance, &fee_transfer.fee)?; + + if can_receive.0 { + let (_, mut account, loc) = ledger.get_or_create(&account_id)?; + let new_accounts = get_new_accounts(action, account_id.clone()); + + account.balance = balance; + account.timing = timing; + + ledger.set(&loc, account); + + let new_accounts: Vec<_> = new_accounts.into_iter().collect(); + Ok((new_accounts, FailureCollection::empty(), Amount::zero())) + } else { + Ok(( + vec![], + FailureCollection::single_failure(), + Amount::of_fee(&fee_transfer.fee), + )) + } + } + OneOrTwo::Two((fee_transfer1, fee_transfer2)) => { + let account_id1 = fee_transfer1.receiver(); + let (a1, action1, can_receive1) = has_permission_to_receive(ledger, &account_id1); + + let account_id2 = fee_transfer2.receiver(); + + if account_id1 == account_id2 { + let fee = fee_transfer1 + .fee + .checked_add(&fee_transfer2.fee) + .ok_or_else(|| "Overflow".to_string())?; + + let timing = modify_timing(&a1)?; + let balance = modify_balance(action1, &account_id1, a1.balance, &fee)?; + + if can_receive1.0 { + let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?; + let new_accounts1 = get_new_accounts(action1, account_id1); + + a1.balance = balance; + a1.timing = timing; + + ledger.set(&l1, a1); + + let new_accounts: Vec<_> = new_accounts1.into_iter().collect(); + Ok((new_accounts, FailureCollection::empty(), Amount::zero())) + } else { + // failure for each fee transfer single + + Ok(( + vec![], + FailureCollection::append_entry( + FailureCollection::update_failed(), + FailureCollection::single_failure(), + ), + Amount::of_fee(&fee), + )) + } + } else { + let (a2, action2, can_receive2) = has_permission_to_receive(ledger, &account_id2); + + let balance1 = + modify_balance(action1, &account_id1, a1.balance, &fee_transfer1.fee)?; + + // Note: Not updating the timing field of a1 to avoid additional check + // in transactions snark (check_timing for "receiver"). This is OK + // because timing rules will not be violated when balance increases + // and will be checked whenever an amount is deducted from the account. (#5973)*) + + let timing2 = modify_timing(&a2)?; + let balance2 = + modify_balance(action2, &account_id2, a2.balance, &fee_transfer2.fee)?; + + let (new_accounts1, failures, burned_tokens1) = if can_receive1.0 { + let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?; + let new_accounts1 = get_new_accounts(action1, account_id1); + + a1.balance = balance1; + ledger.set(&l1, a1); + + ( + new_accounts1, + FailureCollection::append_entry( + FailureCollection::no_failure(), + FailureCollection::empty(), + ), + Amount::zero(), + ) + } else { + ( + None, + FailureCollection::single_failure(), + Amount::of_fee(&fee_transfer1.fee), + ) + }; + + let (new_accounts2, failures, burned_tokens2) = if can_receive2.0 { + let (_, mut a2, l2) = ledger.get_or_create(&account_id2)?; + let new_accounts2 = get_new_accounts(action2, account_id2); + + a2.balance = balance2; + a2.timing = timing2; + + ledger.set(&l2, a2); + + ( + new_accounts2, + FailureCollection::append_entry(FailureCollection::no_failure(), failures), + Amount::zero(), + ) + } else { + ( + None, + FailureCollection::append_entry( + FailureCollection::update_failed(), + failures, + ), + Amount::of_fee(&fee_transfer2.fee), + ) + }; + + let burned_tokens = burned_tokens1 + .checked_add(&burned_tokens2) + .ok_or_else(|| "burned tokens overflow".to_string())?; + + let new_accounts: Vec<_> = [new_accounts1, new_accounts2] + .into_iter() + .flatten() + .collect(); + + Ok((new_accounts, failures, burned_tokens)) + } + } +} +} + +#[derive(Copy, Clone, Debug)] +pub enum AccountState { +Added, +Existed, +} + +#[derive(Debug)] +struct HasPermissionToReceive(bool); + +/// +fn has_permission_to_receive( +ledger: &mut L, +receiver_account_id: &AccountId, +) -> (Box, AccountState, HasPermissionToReceive) +where +L: LedgerIntf, +{ +use crate::PermissionTo::*; +use AccountState::*; + +let init_account = Account::initialize(receiver_account_id); + +match ledger.location_of_account(receiver_account_id) { + None => { + // new account, check that default permissions allow receiving + let perm = init_account.has_permission_to(ControlTag::NoneGiven, Receive); + (Box::new(init_account), Added, HasPermissionToReceive(perm)) + } + Some(location) => match ledger.get(&location) { + None => panic!("Ledger location with no account"), + Some(receiver_account) => { + let perm = receiver_account.has_permission_to(ControlTag::NoneGiven, Receive); + (receiver_account, Existed, HasPermissionToReceive(perm)) + } + }, +} +} + +pub fn validate_time(valid_until: &Slot, current_global_slot: &Slot) -> Result<(), String> { +if current_global_slot <= valid_until { + return Ok(()); +} + +Err(format!( + "Current global slot {:?} greater than transaction expiry slot {:?}", + current_global_slot, valid_until +)) +} + +pub fn is_timed(a: &Account) -> bool { +matches!(&a.timing, Timing::Timed { .. }) +} + +pub fn set_with_location( +ledger: &mut L, +location: &ExistingOrNew, +account: Box, +) -> Result<(), String> +where +L: LedgerIntf, +{ +match location { + ExistingOrNew::Existing(location) => { + ledger.set(location, account); + Ok(()) + } + ExistingOrNew::New => ledger + .create_new_account(account.id(), *account) + .map_err(|_| "set_with_location".to_string()), +} +} + +pub struct Updates { +pub located_accounts: Vec<(ExistingOrNew, Box)>, +pub applied_body: signed_command_applied::Body, +} + +pub fn compute_updates( +constraint_constants: &ConstraintConstants, +receiver: AccountId, +ledger: &mut L, +current_global_slot: &Slot, +user_command: &SignedCommand, +fee_payer: &AccountId, +fee_payer_account: &Account, +fee_payer_location: &ExistingOrNew, +reject_command: &mut bool, +) -> Result, TransactionFailure> +where +L: LedgerIntf, +{ +match &user_command.payload.body { + signed_command::Body::StakeDelegation(_) => { + let (receiver_location, _) = get_with_location(ledger, &receiver).unwrap(); + + if let ExistingOrNew::New = receiver_location { + return Err(TransactionFailure::ReceiverNotPresent); + } + if !fee_payer_account.has_permission_to_set_delegate() { + return Err(TransactionFailure::UpdateNotPermittedDelegate); + } + + let previous_delegate = fee_payer_account.delegate.clone(); + + // Timing is always valid, but we need to record any switch from + // timed to untimed here to stay in sync with the snark. + let fee_payer_account = { + let timing = timing_error_to_user_command_status(validate_timing( + fee_payer_account, + Amount::zero(), + current_global_slot, + ))?; + + Box::new(Account { + delegate: Some(receiver.public_key.clone()), + timing, + ..fee_payer_account.clone() + }) + }; + + Ok(Updates { + located_accounts: vec![(fee_payer_location.clone(), fee_payer_account)], + applied_body: signed_command_applied::Body::StakeDelegation { previous_delegate }, + }) + } + signed_command::Body::Payment(payment) => { + let get_fee_payer_account = || { + let balance = fee_payer_account + .balance + .sub_amount(payment.amount) + .ok_or(TransactionFailure::SourceInsufficientBalance)?; + + let timing = timing_error_to_user_command_status(validate_timing( + fee_payer_account, + payment.amount, + current_global_slot, + ))?; + + Ok(Box::new(Account { + balance, + timing, + ..fee_payer_account.clone() + })) + }; + + let fee_payer_account = match get_fee_payer_account() { + Ok(fee_payer_account) => fee_payer_account, + Err(e) => { + // OCaml throw an exception when an error occurs here + // Here in Rust we set `reject_command` to differentiate the 3 cases (Ok, Err, exception) + // + // + + // Don't accept transactions with insufficient balance from the fee-payer. + // TODO(OCaml): eliminate this condition and accept transaction with failed status + *reject_command = true; + return Err(e); + } + }; + + let (receiver_location, mut receiver_account) = if fee_payer == &receiver { + (fee_payer_location.clone(), fee_payer_account.clone()) + } else { + get_with_location(ledger, &receiver).unwrap() + }; + + if !fee_payer_account.has_permission_to_send() { + return Err(TransactionFailure::UpdateNotPermittedBalance); + } + + if !receiver_account.has_permission_to_receive() { + return Err(TransactionFailure::UpdateNotPermittedBalance); + } + + let receiver_amount = match &receiver_location { + ExistingOrNew::Existing(_) => payment.amount, + ExistingOrNew::New => { + match payment + .amount + .checked_sub(&Amount::from_u64(constraint_constants.account_creation_fee)) + { + Some(amount) => amount, + None => return Err(TransactionFailure::AmountInsufficientToCreateAccount), + } + } + }; + + let balance = match receiver_account.balance.add_amount(receiver_amount) { + Some(balance) => balance, + None => return Err(TransactionFailure::Overflow), + }; + + let new_accounts = match receiver_location { + ExistingOrNew::New => vec![receiver.clone()], + ExistingOrNew::Existing(_) => vec![], + }; + + receiver_account.balance = balance; + + let updated_accounts = if fee_payer == &receiver { + // [receiver_account] at this point has all the updates + vec![(receiver_location, receiver_account)] + } else { + vec![ + (receiver_location, receiver_account), + (fee_payer_location.clone(), fee_payer_account), + ] + }; + + Ok(Updates { + located_accounts: updated_accounts, + applied_body: signed_command_applied::Body::Payments { new_accounts }, + }) + } +} +} + +pub fn apply_user_command_unchecked( +constraint_constants: &ConstraintConstants, +_txn_state_view: &ProtocolStateView, +txn_global_slot: &Slot, +ledger: &mut L, +user_command: &SignedCommand, +) -> Result +where +L: LedgerIntf, +{ +let SignedCommand { + payload: _, + signer: signer_pk, + signature: _, +} = &user_command; +let current_global_slot = txn_global_slot; + +let valid_until = user_command.valid_until(); +validate_time(&valid_until, current_global_slot)?; + +// Fee-payer information +let fee_payer = user_command.fee_payer(); +let (fee_payer_location, fee_payer_account) = + pay_fee(user_command, signer_pk, ledger, current_global_slot)?; + +if !fee_payer_account.has_permission_to_send() { + return Err(TransactionFailure::UpdateNotPermittedBalance.to_string()); +} +if !fee_payer_account.has_permission_to_increment_nonce() { + return Err(TransactionFailure::UpdateNotPermittedNonce.to_string()); +} + +// Charge the fee. This must happen, whether or not the command itself +// succeeds, to ensure that the network is compensated for processing this +// command. +set_with_location(ledger, &fee_payer_location, fee_payer_account.clone())?; + +let receiver = user_command.receiver(); + +let mut reject_command = false; + +match compute_updates( + constraint_constants, + receiver, + ledger, + current_global_slot, + user_command, + &fee_payer, + &fee_payer_account, + &fee_payer_location, + &mut reject_command, +) { + Ok(Updates { + located_accounts, + applied_body, + }) => { + for (location, account) in located_accounts { + set_with_location(ledger, &location, account)?; + } + + Ok(SignedCommandApplied { + common: signed_command_applied::Common { + user_command: WithStatus:: { + data: user_command.clone(), + status: TransactionStatus::Applied, + }, + }, + body: applied_body, + }) + } + Err(failure) if !reject_command => Ok(SignedCommandApplied { + common: signed_command_applied::Common { + user_command: WithStatus:: { + data: user_command.clone(), + status: TransactionStatus::Failed(vec![vec![failure]]), + }, + }, + body: signed_command_applied::Body::Failed, + }), + Err(failure) => { + // This case occurs when an exception is throwned in OCaml + // + assert!(reject_command); + Err(failure.to_string()) + } +} +} + +pub fn apply_user_command( +constraint_constants: &ConstraintConstants, +txn_state_view: &ProtocolStateView, +txn_global_slot: &Slot, +ledger: &mut L, +user_command: &SignedCommand, +) -> Result +where +L: LedgerIntf, +{ +apply_user_command_unchecked( + constraint_constants, + txn_state_view, + txn_global_slot, + ledger, + user_command, +) +} + +pub fn pay_fee( +user_command: &SignedCommand, +signer_pk: &CompressedPubKey, +ledger: &mut L, +current_global_slot: &Slot, +) -> Result<(ExistingOrNew, Box), String> +where +L: LedgerIntf, +{ +let nonce = user_command.nonce(); +let fee_payer = user_command.fee_payer(); +let fee_token = user_command.fee_token(); + +if &fee_payer.public_key != signer_pk { + return Err("Cannot pay fees from a public key that did not sign the transaction".into()); +} + +if fee_token != TokenId::default() { + return Err("Cannot create transactions with fee_token different from the default".into()); +} + +pay_fee_impl( + &user_command.payload, + nonce, + fee_payer, + user_command.fee(), + ledger, + current_global_slot, +) +} + +fn pay_fee_impl( +command: &SignedCommandPayload, +nonce: Nonce, +fee_payer: AccountId, +fee: Fee, +ledger: &mut L, +current_global_slot: &Slot, +) -> Result<(ExistingOrNew, Box), String> +where +L: LedgerIntf, +{ +// Fee-payer information +let (location, mut account) = get_with_location(ledger, &fee_payer)?; + +if let ExistingOrNew::New = location { + return Err("The fee-payer account does not exist".to_string()); +}; + +let fee = Amount::of_fee(&fee); +let balance = sub_amount(account.balance, fee)?; + +validate_nonces(nonce, account.nonce)?; +let timing = validate_timing(&account, fee, current_global_slot)?; + +account.balance = balance; +account.nonce = account.nonce.incr(); // TODO: Not sure if OCaml wraps +account.receipt_chain_hash = cons_signed_command_payload(command, account.receipt_chain_hash); +account.timing = timing; + +Ok((location, account)) + +// in +// ( location +// , { account with +// balance +// ; nonce = Account.Nonce.succ account.nonce +// ; receipt_chain_hash = +// Receipt.Chain_hash.cons_signed_command_payload command +// account.receipt_chain_hash +// ; timing +// } ) +} + From 2d903f2c365453201f060f05e350eb77f4d4f002 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 21:47:55 +0200 Subject: [PATCH 11/16] Ledger/transaction_logic: extract transaction_union_payload module to separate file Split the large transaction_logic.rs file by extracting the transaction_union_payload module (679 lines) into its own file at ledger/src/scan_state/transaction_logic/transaction_union_payload.rs. Changes: - Created transaction_union_payload.rs with TransactionUnionPayload, TransactionUnion, Common, Body, Tag types - Includes timing validation functions (validate_timing, validate_nonces, account_check_timing, timing_error_to_user_command_status) - Includes receipt chain hash functions (cons_signed_command_payload, checked_cons_signed_command_payload, cons_zkapp_command_commitment) - Includes helper functions (add_amount, sub_amount, get_with_location, set_account, get_account) - Includes ExistingOrNew and TimingValidation types - Made helper functions public for use in transaction_partially_applied - Added module declaration and re-exports in mod.rs for all public items - Added explicit imports including OneOrTwo from scan_state::scan_state::transaction_snark - Removed dangling cfg attribute at end of file - Added cfg attribute to for_tests module declaration - Removed duplicate TransactionUnionPayload import from self block --- .../src/scan_state/transaction_logic/mod.rs | 675 +---------------- .../transaction_union_payload.rs | 677 ++++++++++++++++++ 2 files changed, 684 insertions(+), 668 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/transaction_union_payload.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 469e4f994..dc1edf570 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -44,7 +44,6 @@ use self::{ signed_command_applied::{self, SignedCommandApplied}, TransactionApplied, ZkappCommandApplied, }, - transaction_union_payload::TransactionUnionPayload, zkapp_command::{AccessedOrNot, AccountUpdate, WithHash, ZkAppCommand}, }; @@ -1045,673 +1044,13 @@ pub use transaction_partially_applied::{ apply_user_command, set_with_location, AccountState, }; -pub mod transaction_union_payload { - use ark_ff::PrimeField; - use mina_hasher::{Hashable, ROInput as LegacyInput}; - use mina_signer::{NetworkId, PubKey, Signature}; - - use crate::{ - decompress_pk, - proofs::field::Boolean, - scan_state::transaction_logic::signed_command::{PaymentPayload, StakeDelegationPayload}, - }; - - use super::*; - - #[derive(Clone)] - pub struct Common { - pub fee: Fee, - pub fee_token: TokenId, - pub fee_payer_pk: CompressedPubKey, - pub nonce: Nonce, - pub valid_until: Slot, - pub memo: Memo, - } - - #[derive(Clone, Debug)] - pub enum Tag { - Payment = 0, - StakeDelegation = 1, - FeeTransfer = 2, - Coinbase = 3, - } - - impl Tag { - pub fn is_user_command(&self) -> Boolean { - match self { - Tag::Payment | Tag::StakeDelegation => Boolean::True, - Tag::FeeTransfer | Tag::Coinbase => Boolean::False, - } - } - - pub fn is_payment(&self) -> Boolean { - match self { - Tag::Payment => Boolean::True, - Tag::FeeTransfer | Tag::Coinbase | Tag::StakeDelegation => Boolean::False, - } - } - - pub fn is_stake_delegation(&self) -> Boolean { - match self { - Tag::StakeDelegation => Boolean::True, - Tag::FeeTransfer | Tag::Coinbase | Tag::Payment => Boolean::False, - } - } - - pub fn is_fee_transfer(&self) -> Boolean { - match self { - Tag::FeeTransfer => Boolean::True, - Tag::StakeDelegation | Tag::Coinbase | Tag::Payment => Boolean::False, - } - } - - pub fn is_coinbase(&self) -> Boolean { - match self { - Tag::Coinbase => Boolean::True, - Tag::StakeDelegation | Tag::FeeTransfer | Tag::Payment => Boolean::False, - } - } - - pub fn to_bits(&self) -> [bool; 3] { - let tag = self.clone() as u8; - let mut bits = [false; 3]; - for (index, bit) in [4, 2, 1].iter().enumerate() { - bits[index] = tag & bit != 0; - } - bits - } - - pub fn to_untagged_bits(&self) -> [bool; 5] { - let mut is_payment = false; - let mut is_stake_delegation = false; - let mut is_fee_transfer = false; - let mut is_coinbase = false; - let mut is_user_command = false; - - match self { - Tag::Payment => { - is_payment = true; - is_user_command = true; - } - Tag::StakeDelegation => { - is_stake_delegation = true; - is_user_command = true; - } - Tag::FeeTransfer => is_fee_transfer = true, - Tag::Coinbase => is_coinbase = true, - } - - [ - is_payment, - is_stake_delegation, - is_fee_transfer, - is_coinbase, - is_user_command, - ] - } - } - - #[derive(Clone)] - pub struct Body { - pub tag: Tag, - pub source_pk: CompressedPubKey, - pub receiver_pk: CompressedPubKey, - pub token_id: TokenId, - pub amount: Amount, - } - - #[derive(Clone)] - pub struct TransactionUnionPayload { - pub common: Common, - pub body: Body, - } - - impl Hashable for TransactionUnionPayload { - type D = NetworkId; - - fn to_roinput(&self) -> LegacyInput { - /* - Payment transactions only use the default token-id value 1. - The old transaction format encoded the token-id as an u64, - however zkApps encode the token-id as a Fp. - - For testing/fuzzing purposes we want the ability to encode - arbitrary values different from the default token-id, for this - we will extract the LS u64 of the token-id. - */ - let fee_token_id = self.common.fee_token.0.into_bigint().0[0]; - let token_id = self.body.token_id.0.into_bigint().0[0]; - - let mut roi = LegacyInput::new() - .append_field(self.common.fee_payer_pk.x) - .append_field(self.body.source_pk.x) - .append_field(self.body.receiver_pk.x) - .append_u64(self.common.fee.as_u64()) - .append_u64(fee_token_id) - .append_bool(self.common.fee_payer_pk.is_odd) - .append_u32(self.common.nonce.as_u32()) - .append_u32(self.common.valid_until.as_u32()) - .append_bytes(&self.common.memo.0); - - let tag = self.body.tag.clone() as u8; - for bit in [4, 2, 1] { - roi = roi.append_bool(tag & bit != 0); - } - - roi.append_bool(self.body.source_pk.is_odd) - .append_bool(self.body.receiver_pk.is_odd) - .append_u64(token_id) - .append_u64(self.body.amount.as_u64()) - .append_bool(false) // Used to be `self.body.token_locked` - } - - // TODO: this is unused, is it needed? - fn domain_string(network_id: NetworkId) -> Option { - // Domain strings must have length <= 20 - match network_id { - NetworkId::MAINNET => mina_core::network::mainnet::SIGNATURE_PREFIX, - NetworkId::TESTNET => mina_core::network::devnet::SIGNATURE_PREFIX, - } - .to_string() - .into() - } - } - - impl TransactionUnionPayload { - pub fn of_user_command_payload(payload: &SignedCommandPayload) -> Self { - use signed_command::Body::{Payment, StakeDelegation}; - - Self { - common: Common { - fee: payload.common.fee, - fee_token: TokenId::default(), - fee_payer_pk: payload.common.fee_payer_pk.clone(), - nonce: payload.common.nonce, - valid_until: payload.common.valid_until, - memo: payload.common.memo.clone(), - }, - body: match &payload.body { - Payment(PaymentPayload { - receiver_pk, - amount, - }) => Body { - tag: Tag::Payment, - source_pk: payload.common.fee_payer_pk.clone(), - receiver_pk: receiver_pk.clone(), - token_id: TokenId::default(), - amount: *amount, - }, - StakeDelegation(StakeDelegationPayload::SetDelegate { new_delegate }) => Body { - tag: Tag::StakeDelegation, - source_pk: payload.common.fee_payer_pk.clone(), - receiver_pk: new_delegate.clone(), - token_id: TokenId::default(), - amount: Amount::zero(), - }, - }, - } - } - - /// - pub fn to_input_legacy(&self) -> ::poseidon::hash::legacy::Inputs { - let mut roi = ::poseidon::hash::legacy::Inputs::new(); - - // Self.common - { - roi.append_u64(self.common.fee.0); - - // TokenId.default - // - roi.append_bool(true); - for _ in 0..63 { - roi.append_bool(false); - } - - // fee_payer_pk - roi.append_field(self.common.fee_payer_pk.x); - roi.append_bool(self.common.fee_payer_pk.is_odd); - - // nonce - roi.append_u32(self.common.nonce.0); - - // valid_until - roi.append_u32(self.common.valid_until.0); - - // memo - roi.append_bytes(&self.common.memo.0); - } - - // Self.body - { - // tag - let tag = self.body.tag.clone() as u8; - for bit in [4, 2, 1] { - roi.append_bool(tag & bit != 0); - } - - // source_pk - roi.append_field(self.body.source_pk.x); - roi.append_bool(self.body.source_pk.is_odd); - - // receiver_pk - roi.append_field(self.body.receiver_pk.x); - roi.append_bool(self.body.receiver_pk.is_odd); - - // default token_id - roi.append_u64(1); - - // amount - roi.append_u64(self.body.amount.0); - - // token_locked - roi.append_bool(false); - } - - roi - } - } - - pub struct TransactionUnion { - pub payload: TransactionUnionPayload, - pub signer: PubKey, - pub signature: Signature, - } - - impl TransactionUnion { - /// For SNARK purposes, we inject [Transaction.t]s into a single-variant 'tagged-union' record capable of - /// representing all the variants. We interpret the fields of this union in different ways depending on - /// the value of the [payload.body.tag] field, which represents which variant of [Transaction.t] the value - /// corresponds to. - /// - /// Sometimes we interpret fields in surprising ways in different cases to save as much space in the SNARK as possible (e.g., - /// [payload.body.public_key] is interpreted as the recipient of a payment, the new delegate of a stake - /// delegation command, and a fee transfer recipient for both coinbases and fee-transfers. - pub fn of_transaction(tx: &Transaction) -> Self { - match tx { - Transaction::Command(cmd) => { - let UserCommand::SignedCommand(cmd) = cmd else { - unreachable!(); - }; - - let SignedCommand { - payload, - signer, - signature, - } = cmd.as_ref(); - - TransactionUnion { - payload: TransactionUnionPayload::of_user_command_payload(payload), - signer: decompress_pk(signer).unwrap(), - signature: signature.clone(), - } - } - Transaction::Coinbase(Coinbase { - receiver, - amount, - fee_transfer, - }) => { - let CoinbaseFeeTransfer { - receiver_pk: other_pk, - fee: other_amount, - } = fee_transfer.clone().unwrap_or_else(|| { - CoinbaseFeeTransfer::create(receiver.clone(), Fee::zero()) - }); - - let signer = decompress_pk(&other_pk).unwrap(); - let payload = TransactionUnionPayload { - common: Common { - fee: other_amount, - fee_token: TokenId::default(), - fee_payer_pk: other_pk.clone(), - nonce: Nonce::zero(), - valid_until: Slot::max(), - memo: Memo::empty(), - }, - body: Body { - source_pk: other_pk, - receiver_pk: receiver.clone(), - token_id: TokenId::default(), - amount: *amount, - tag: Tag::Coinbase, - }, - }; - - TransactionUnion { - payload, - signer, - signature: Signature::dummy(), - } - } - Transaction::FeeTransfer(tr) => { - let two = |SingleFeeTransfer { - receiver_pk: pk1, - fee: fee1, - fee_token, - }, - SingleFeeTransfer { - receiver_pk: pk2, - fee: fee2, - fee_token: token_id, - }| { - let signer = decompress_pk(&pk2).unwrap(); - let payload = TransactionUnionPayload { - common: Common { - fee: fee2, - fee_token, - fee_payer_pk: pk2.clone(), - nonce: Nonce::zero(), - valid_until: Slot::max(), - memo: Memo::empty(), - }, - body: Body { - source_pk: pk2, - receiver_pk: pk1, - token_id, - amount: Amount::of_fee(&fee1), - tag: Tag::FeeTransfer, - }, - }; - - TransactionUnion { - payload, - signer, - signature: Signature::dummy(), - } - }; - - match tr.0.clone() { - OneOrTwo::One(t) => { - let other = SingleFeeTransfer::create( - t.receiver_pk.clone(), - Fee::zero(), - t.fee_token.clone(), - ); - two(t, other) - } - OneOrTwo::Two((t1, t2)) => two(t1, t2), - } - } - } - } - } -} - -/// Returns the new `receipt_chain_hash` -pub fn cons_signed_command_payload( - command_payload: &SignedCommandPayload, - last_receipt_chain_hash: ReceiptChainHash, -) -> ReceiptChainHash { - // Note: Not sure why they use the legacy way of hashing here - - use poseidon::hash::legacy; - - let ReceiptChainHash(last_receipt_chain_hash) = last_receipt_chain_hash; - let union = TransactionUnionPayload::of_user_command_payload(command_payload); - - let mut inputs = union.to_input_legacy(); - inputs.append_field(last_receipt_chain_hash); - let hash = legacy::hash_with_kimchi(&legacy::params::CODA_RECEIPT_UC, &inputs.to_fields()); - - ReceiptChainHash(hash) -} - -/// Returns the new `receipt_chain_hash` -pub fn checked_cons_signed_command_payload( - payload: &TransactionUnionPayload, - last_receipt_chain_hash: ReceiptChainHash, - w: &mut Witness, -) -> ReceiptChainHash { - use crate::proofs::transaction::{ - legacy_input::CheckedLegacyInput, transaction_snark::checked_legacy_hash, - }; - use poseidon::hash::legacy; - - let mut inputs = payload.to_checked_legacy_input_owned(w); - inputs.append_field(last_receipt_chain_hash.0); - - let receipt_chain_hash = checked_legacy_hash(&legacy::params::CODA_RECEIPT_UC, inputs, w); - - ReceiptChainHash(receipt_chain_hash) -} - -/// prepend account_update index computed by Zkapp_command_logic.apply -/// -/// -pub fn cons_zkapp_command_commitment( - index: Index, - e: ZkAppCommandElt, - receipt_hash: &ReceiptChainHash, -) -> ReceiptChainHash { - let ZkAppCommandElt::ZkAppCommandCommitment(x) = e; - - let mut inputs = Inputs::new(); - - inputs.append(&index); - inputs.append_field(x.0); - inputs.append(receipt_hash); - - ReceiptChainHash(hash_with_kimchi(&CODA_RECEIPT_UC, &inputs.to_fields())) -} - -fn validate_nonces(txn_nonce: Nonce, account_nonce: Nonce) -> Result<(), String> { - if account_nonce == txn_nonce { - return Ok(()); - } - - Err(format!( - "Nonce in account {:?} different from nonce in transaction {:?}", - account_nonce, txn_nonce, - )) -} - -pub fn validate_timing( - account: &Account, - txn_amount: Amount, - txn_global_slot: &Slot, -) -> Result { - let (timing, _) = validate_timing_with_min_balance(account, txn_amount, txn_global_slot)?; - - Ok(timing) -} - -pub fn account_check_timing( - txn_global_slot: &Slot, - account: &Account, -) -> (TimingValidation, Timing) { - let (invalid_timing, timing, _) = - validate_timing_with_min_balance_impl(account, Amount::from_u64(0), txn_global_slot); - // TODO: In OCaml the returned Timing is actually converted to None/Some(fields of Timing structure) - (invalid_timing, timing) -} - -fn validate_timing_with_min_balance( - account: &Account, - txn_amount: Amount, - txn_global_slot: &Slot, -) -> Result<(Timing, MinBalance), String> { - use TimingValidation::*; - - let (possibly_error, timing, min_balance) = - validate_timing_with_min_balance_impl(account, txn_amount, txn_global_slot); - - match possibly_error { - InsufficientBalance(true) => Err(format!( - "For timed account, the requested transaction for amount {:?} \ - at global slot {:?}, the balance {:?} \ - is insufficient", - txn_amount, txn_global_slot, account.balance - )), - InvalidTiming(true) => Err(format!( - "For timed account {}, the requested transaction for amount {:?} \ - at global slot {:?}, applying the transaction would put the \ - balance below the calculated minimum balance of {:?}", - account.public_key.into_address(), - txn_amount, - txn_global_slot, - min_balance.0 - )), - InsufficientBalance(false) => { - panic!("Broken invariant in validate_timing_with_min_balance'") - } - InvalidTiming(false) => Ok((timing, min_balance)), - } -} - -pub fn timing_error_to_user_command_status( - timing_result: Result, -) -> Result { - match timing_result { - Ok(timing) => Ok(timing), - Err(err_str) => { - /* - HACK: we are matching over the full error string instead - of including an extra tag string to the Err variant - */ - if err_str.contains("minimum balance") { - return Err(TransactionFailure::SourceMinimumBalanceViolation); - } - - if err_str.contains("is insufficient") { - return Err(TransactionFailure::SourceInsufficientBalance); - } - - panic!("Unexpected timed account validation error") - } - } -} - -pub enum TimingValidation { - InsufficientBalance(B), - InvalidTiming(B), -} - -#[derive(Debug)] -struct MinBalance(Balance); - -fn validate_timing_with_min_balance_impl( - account: &Account, - txn_amount: Amount, - txn_global_slot: &Slot, -) -> (TimingValidation, Timing, MinBalance) { - use crate::Timing::*; - use TimingValidation::*; - - match &account.timing { - Untimed => { - // no time restrictions - match account.balance.sub_amount(txn_amount) { - None => ( - InsufficientBalance(true), - Untimed, - MinBalance(Balance::zero()), - ), - Some(_) => (InvalidTiming(false), Untimed, MinBalance(Balance::zero())), - } - } - Timed { - initial_minimum_balance, - .. - } => { - let account_balance = account.balance; - - let (invalid_balance, invalid_timing, curr_min_balance) = - match account_balance.sub_amount(txn_amount) { - None => { - // NB: The [initial_minimum_balance] here is the incorrect value, - // but: - // * we don't use it anywhere in this error case; and - // * we don't want to waste time computing it if it will be unused. - (true, false, *initial_minimum_balance) - } - Some(proposed_new_balance) => { - let curr_min_balance = account.min_balance_at_slot(*txn_global_slot); - - if proposed_new_balance < curr_min_balance { - (false, true, curr_min_balance) - } else { - (false, false, curr_min_balance) - } - } - }; - - // once the calculated minimum balance becomes zero, the account becomes untimed - let possibly_error = if invalid_balance { - InsufficientBalance(invalid_balance) - } else { - InvalidTiming(invalid_timing) - }; - - if curr_min_balance > Balance::zero() { - ( - possibly_error, - account.timing.clone(), - MinBalance(curr_min_balance), - ) - } else { - (possibly_error, Untimed, MinBalance(Balance::zero())) - } - } - } -} - -fn sub_amount(balance: Balance, amount: Amount) -> Result { - balance - .sub_amount(amount) - .ok_or_else(|| "insufficient funds".to_string()) -} - -fn add_amount(balance: Balance, amount: Amount) -> Result { - balance - .add_amount(amount) - .ok_or_else(|| "overflow".to_string()) -} - -#[derive(Clone, Debug)] -pub enum ExistingOrNew { - Existing(Loc), - New, -} - -fn get_with_location( - ledger: &mut L, - account_id: &AccountId, -) -> Result<(ExistingOrNew, Box), String> -where - L: LedgerIntf, -{ - match ledger.location_of_account(account_id) { - Some(location) => match ledger.get(&location) { - Some(account) => Ok((ExistingOrNew::Existing(location), account)), - None => panic!("Ledger location with no account"), - }, - None => Ok(( - ExistingOrNew::New, - Box::new(Account::create_with(account_id.clone(), Balance::zero())), - )), - } -} - -pub fn get_account( - ledger: &mut L, - account_id: AccountId, -) -> (Box, ExistingOrNew) -where - L: LedgerIntf, -{ - let (loc, account) = get_with_location(ledger, &account_id).unwrap(); - (account, loc) -} - -pub fn set_account<'a, L>( - l: &'a mut L, - (a, loc): (Box, &ExistingOrNew), -) -> &'a mut L -where - L: LedgerIntf, -{ - set_with_location(l, loc, a).unwrap(); - l -} +pub mod transaction_union_payload; +pub use transaction_union_payload::{ + account_check_timing, add_amount, checked_cons_signed_command_payload, + cons_signed_command_payload, cons_zkapp_command_commitment, get_with_location, sub_amount, + timing_error_to_user_command_status, validate_nonces, validate_timing, Body, Common, + ExistingOrNew, Tag, TimingValidation, TransactionUnion, TransactionUnionPayload, +}; #[cfg(any(test, feature = "fuzzing"))] pub mod for_tests { diff --git a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs new file mode 100644 index 000000000..93948be05 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs @@ -0,0 +1,677 @@ +use ark_ff::{PrimeField, Zero}; +use mina_hasher::{Hashable, ROInput as LegacyInput, Fp}; +use mina_signer::{CompressedPubKey, NetworkId, PubKey, Signature}; +use poseidon::hash::{hash_with_kimchi, params::CODA_RECEIPT_UC, Inputs}; + +use crate::{ + decompress_pk, + proofs::{field::Boolean, witness::Witness}, + scan_state::currency::{Amount, Balance, Fee, Index, Magnitude, Nonce, Slot}, + sparse_ledger::LedgerIntf, + zkapps::zkapp_logic::ZkAppCommandElt, + Account, AccountId, AppendToInputs, ReceiptChainHash, Timing, TokenId, +}; + +use crate::scan_state::scan_state::transaction_snark::OneOrTwo; + +use super::{ + signed_command::{self, PaymentPayload, SignedCommand, SignedCommandPayload, StakeDelegationPayload}, + transaction_partially_applied::set_with_location, + Coinbase, CoinbaseFeeTransfer, Memo, SingleFeeTransfer, Transaction, TransactionFailure, + UserCommand, +}; + +#[derive(Clone)] +pub struct Common { + pub fee: Fee, + pub fee_token: TokenId, + pub fee_payer_pk: CompressedPubKey, + pub nonce: Nonce, + pub valid_until: Slot, + pub memo: Memo, +} + +#[derive(Clone, Debug)] +pub enum Tag { + Payment = 0, + StakeDelegation = 1, + FeeTransfer = 2, + Coinbase = 3, +} + +impl Tag { + pub fn is_user_command(&self) -> Boolean { + match self { + Tag::Payment | Tag::StakeDelegation => Boolean::True, + Tag::FeeTransfer | Tag::Coinbase => Boolean::False, + } + } + + pub fn is_payment(&self) -> Boolean { + match self { + Tag::Payment => Boolean::True, + Tag::FeeTransfer | Tag::Coinbase | Tag::StakeDelegation => Boolean::False, + } + } + + pub fn is_stake_delegation(&self) -> Boolean { + match self { + Tag::StakeDelegation => Boolean::True, + Tag::FeeTransfer | Tag::Coinbase | Tag::Payment => Boolean::False, + } + } + + pub fn is_fee_transfer(&self) -> Boolean { + match self { + Tag::FeeTransfer => Boolean::True, + Tag::StakeDelegation | Tag::Coinbase | Tag::Payment => Boolean::False, + } + } + + pub fn is_coinbase(&self) -> Boolean { + match self { + Tag::Coinbase => Boolean::True, + Tag::StakeDelegation | Tag::FeeTransfer | Tag::Payment => Boolean::False, + } + } + + pub fn to_bits(&self) -> [bool; 3] { + let tag = self.clone() as u8; + let mut bits = [false; 3]; + for (index, bit) in [4, 2, 1].iter().enumerate() { + bits[index] = tag & bit != 0; + } + bits + } + + pub fn to_untagged_bits(&self) -> [bool; 5] { + let mut is_payment = false; + let mut is_stake_delegation = false; + let mut is_fee_transfer = false; + let mut is_coinbase = false; + let mut is_user_command = false; + + match self { + Tag::Payment => { + is_payment = true; + is_user_command = true; + } + Tag::StakeDelegation => { + is_stake_delegation = true; + is_user_command = true; + } + Tag::FeeTransfer => is_fee_transfer = true, + Tag::Coinbase => is_coinbase = true, + } + + [ + is_payment, + is_stake_delegation, + is_fee_transfer, + is_coinbase, + is_user_command, + ] + } +} + +#[derive(Clone)] +pub struct Body { + pub tag: Tag, + pub source_pk: CompressedPubKey, + pub receiver_pk: CompressedPubKey, + pub token_id: TokenId, + pub amount: Amount, +} + +#[derive(Clone)] +pub struct TransactionUnionPayload { + pub common: Common, + pub body: Body, +} + +impl Hashable for TransactionUnionPayload { + type D = NetworkId; + + fn to_roinput(&self) -> LegacyInput { + /* + Payment transactions only use the default token-id value 1. + The old transaction format encoded the token-id as an u64, + however zkApps encode the token-id as a Fp. + + For testing/fuzzing purposes we want the ability to encode + arbitrary values different from the default token-id, for this + we will extract the LS u64 of the token-id. + */ + let fee_token_id = self.common.fee_token.0.into_bigint().0[0]; + let token_id = self.body.token_id.0.into_bigint().0[0]; + + let mut roi = LegacyInput::new() + .append_field(self.common.fee_payer_pk.x) + .append_field(self.body.source_pk.x) + .append_field(self.body.receiver_pk.x) + .append_u64(self.common.fee.as_u64()) + .append_u64(fee_token_id) + .append_bool(self.common.fee_payer_pk.is_odd) + .append_u32(self.common.nonce.as_u32()) + .append_u32(self.common.valid_until.as_u32()) + .append_bytes(&self.common.memo.0); + + let tag = self.body.tag.clone() as u8; + for bit in [4, 2, 1] { + roi = roi.append_bool(tag & bit != 0); + } + + roi.append_bool(self.body.source_pk.is_odd) + .append_bool(self.body.receiver_pk.is_odd) + .append_u64(token_id) + .append_u64(self.body.amount.as_u64()) + .append_bool(false) // Used to be `self.body.token_locked` + } + + // TODO: this is unused, is it needed? + fn domain_string(network_id: NetworkId) -> Option { + // Domain strings must have length <= 20 + match network_id { + NetworkId::MAINNET => mina_core::network::mainnet::SIGNATURE_PREFIX, + NetworkId::TESTNET => mina_core::network::devnet::SIGNATURE_PREFIX, + } + .to_string() + .into() + } +} + +impl TransactionUnionPayload { + pub fn of_user_command_payload(payload: &SignedCommandPayload) -> Self { + use signed_command::Body::{Payment, StakeDelegation}; + + Self { + common: Common { + fee: payload.common.fee, + fee_token: TokenId::default(), + fee_payer_pk: payload.common.fee_payer_pk.clone(), + nonce: payload.common.nonce, + valid_until: payload.common.valid_until, + memo: payload.common.memo.clone(), + }, + body: match &payload.body { + Payment(PaymentPayload { + receiver_pk, + amount, + }) => Body { + tag: Tag::Payment, + source_pk: payload.common.fee_payer_pk.clone(), + receiver_pk: receiver_pk.clone(), + token_id: TokenId::default(), + amount: *amount, + }, + StakeDelegation(StakeDelegationPayload::SetDelegate { new_delegate }) => Body { + tag: Tag::StakeDelegation, + source_pk: payload.common.fee_payer_pk.clone(), + receiver_pk: new_delegate.clone(), + token_id: TokenId::default(), + amount: Amount::zero(), + }, + }, + } + } + + /// + pub fn to_input_legacy(&self) -> ::poseidon::hash::legacy::Inputs { + let mut roi = ::poseidon::hash::legacy::Inputs::new(); + + // Self.common + { + roi.append_u64(self.common.fee.0); + + // TokenId.default + // + roi.append_bool(true); + for _ in 0..63 { + roi.append_bool(false); + } + + // fee_payer_pk + roi.append_field(self.common.fee_payer_pk.x); + roi.append_bool(self.common.fee_payer_pk.is_odd); + + // nonce + roi.append_u32(self.common.nonce.0); + + // valid_until + roi.append_u32(self.common.valid_until.0); + + // memo + roi.append_bytes(&self.common.memo.0); + } + + // Self.body + { + // tag + let tag = self.body.tag.clone() as u8; + for bit in [4, 2, 1] { + roi.append_bool(tag & bit != 0); + } + + // source_pk + roi.append_field(self.body.source_pk.x); + roi.append_bool(self.body.source_pk.is_odd); + + // receiver_pk + roi.append_field(self.body.receiver_pk.x); + roi.append_bool(self.body.receiver_pk.is_odd); + + // default token_id + roi.append_u64(1); + + // amount + roi.append_u64(self.body.amount.0); + + // token_locked + roi.append_bool(false); + } + + roi + } +} + +pub struct TransactionUnion { + pub payload: TransactionUnionPayload, + pub signer: PubKey, + pub signature: Signature, +} + +impl TransactionUnion { + /// For SNARK purposes, we inject [Transaction.t]s into a single-variant 'tagged-union' record capable of + /// representing all the variants. We interpret the fields of this union in different ways depending on + /// the value of the [payload.body.tag] field, which represents which variant of [Transaction.t] the value + /// corresponds to. + /// + /// Sometimes we interpret fields in surprising ways in different cases to save as much space in the SNARK as possible (e.g., + /// [payload.body.public_key] is interpreted as the recipient of a payment, the new delegate of a stake + /// delegation command, and a fee transfer recipient for both coinbases and fee-transfers. + pub fn of_transaction(tx: &Transaction) -> Self { + match tx { + Transaction::Command(cmd) => { + let UserCommand::SignedCommand(cmd) = cmd else { + unreachable!(); + }; + + let SignedCommand { + payload, + signer, + signature, + } = cmd.as_ref(); + + TransactionUnion { + payload: TransactionUnionPayload::of_user_command_payload(payload), + signer: decompress_pk(signer).unwrap(), + signature: signature.clone(), + } + } + Transaction::Coinbase(Coinbase { + receiver, + amount, + fee_transfer, + }) => { + let CoinbaseFeeTransfer { + receiver_pk: other_pk, + fee: other_amount, + } = fee_transfer.clone().unwrap_or_else(|| { + CoinbaseFeeTransfer::create(receiver.clone(), Fee::zero()) + }); + + let signer = decompress_pk(&other_pk).unwrap(); + let payload = TransactionUnionPayload { + common: Common { + fee: other_amount, + fee_token: TokenId::default(), + fee_payer_pk: other_pk.clone(), + nonce: Nonce::zero(), + valid_until: Slot::max(), + memo: Memo::empty(), + }, + body: Body { + source_pk: other_pk, + receiver_pk: receiver.clone(), + token_id: TokenId::default(), + amount: *amount, + tag: Tag::Coinbase, + }, + }; + + TransactionUnion { + payload, + signer, + signature: Signature::dummy(), + } + } + Transaction::FeeTransfer(tr) => { + let two = |SingleFeeTransfer { + receiver_pk: pk1, + fee: fee1, + fee_token, + }, + SingleFeeTransfer { + receiver_pk: pk2, + fee: fee2, + fee_token: token_id, + }| { + let signer = decompress_pk(&pk2).unwrap(); + let payload = TransactionUnionPayload { + common: Common { + fee: fee2, + fee_token, + fee_payer_pk: pk2.clone(), + nonce: Nonce::zero(), + valid_until: Slot::max(), + memo: Memo::empty(), + }, + body: Body { + source_pk: pk2, + receiver_pk: pk1, + token_id, + amount: Amount::of_fee(&fee1), + tag: Tag::FeeTransfer, + }, + }; + + TransactionUnion { + payload, + signer, + signature: Signature::dummy(), + } + }; + + match tr.0.clone() { + OneOrTwo::One(t) => { + let other = SingleFeeTransfer::create( + t.receiver_pk.clone(), + Fee::zero(), + t.fee_token.clone(), + ); + two(t, other) + } + OneOrTwo::Two((t1, t2)) => two(t1, t2), + } + } + } + } +} + +/// Returns the new `receipt_chain_hash` +pub fn cons_signed_command_payload( +command_payload: &SignedCommandPayload, +last_receipt_chain_hash: ReceiptChainHash, +) -> ReceiptChainHash { +// Note: Not sure why they use the legacy way of hashing here + +use poseidon::hash::legacy; + +let ReceiptChainHash(last_receipt_chain_hash) = last_receipt_chain_hash; +let union = TransactionUnionPayload::of_user_command_payload(command_payload); + +let mut inputs = union.to_input_legacy(); +inputs.append_field(last_receipt_chain_hash); +let hash = legacy::hash_with_kimchi(&legacy::params::CODA_RECEIPT_UC, &inputs.to_fields()); + +ReceiptChainHash(hash) +} + +/// Returns the new `receipt_chain_hash` +pub fn checked_cons_signed_command_payload( +payload: &TransactionUnionPayload, +last_receipt_chain_hash: ReceiptChainHash, +w: &mut Witness, +) -> ReceiptChainHash { +use crate::proofs::transaction::{ + legacy_input::CheckedLegacyInput, transaction_snark::checked_legacy_hash, +}; +use poseidon::hash::legacy; + +let mut inputs = payload.to_checked_legacy_input_owned(w); +inputs.append_field(last_receipt_chain_hash.0); + +let receipt_chain_hash = checked_legacy_hash(&legacy::params::CODA_RECEIPT_UC, inputs, w); + +ReceiptChainHash(receipt_chain_hash) +} + +/// prepend account_update index computed by Zkapp_command_logic.apply +/// +/// +pub fn cons_zkapp_command_commitment( +index: Index, +e: ZkAppCommandElt, +receipt_hash: &ReceiptChainHash, +) -> ReceiptChainHash { +let ZkAppCommandElt::ZkAppCommandCommitment(x) = e; + +let mut inputs = Inputs::new(); + +inputs.append(&index); +inputs.append_field(x.0); +inputs.append(receipt_hash); + +ReceiptChainHash(hash_with_kimchi(&CODA_RECEIPT_UC, &inputs.to_fields())) +} + +pub fn validate_nonces(txn_nonce: Nonce, account_nonce: Nonce) -> Result<(), String> { +if account_nonce == txn_nonce { + return Ok(()); +} + +Err(format!( + "Nonce in account {:?} different from nonce in transaction {:?}", + account_nonce, txn_nonce, +)) +} + +pub fn validate_timing( +account: &Account, +txn_amount: Amount, +txn_global_slot: &Slot, +) -> Result { +let (timing, _) = validate_timing_with_min_balance(account, txn_amount, txn_global_slot)?; + +Ok(timing) +} + +pub fn account_check_timing( +txn_global_slot: &Slot, +account: &Account, +) -> (TimingValidation, Timing) { +let (invalid_timing, timing, _) = + validate_timing_with_min_balance_impl(account, Amount::from_u64(0), txn_global_slot); +// TODO: In OCaml the returned Timing is actually converted to None/Some(fields of Timing structure) +(invalid_timing, timing) +} + +fn validate_timing_with_min_balance( +account: &Account, +txn_amount: Amount, +txn_global_slot: &Slot, +) -> Result<(Timing, MinBalance), String> { +use TimingValidation::*; + +let (possibly_error, timing, min_balance) = + validate_timing_with_min_balance_impl(account, txn_amount, txn_global_slot); + +match possibly_error { + InsufficientBalance(true) => Err(format!( + "For timed account, the requested transaction for amount {:?} \ + at global slot {:?}, the balance {:?} \ + is insufficient", + txn_amount, txn_global_slot, account.balance + )), + InvalidTiming(true) => Err(format!( + "For timed account {}, the requested transaction for amount {:?} \ + at global slot {:?}, applying the transaction would put the \ + balance below the calculated minimum balance of {:?}", + account.public_key.into_address(), + txn_amount, + txn_global_slot, + min_balance.0 + )), + InsufficientBalance(false) => { + panic!("Broken invariant in validate_timing_with_min_balance'") + } + InvalidTiming(false) => Ok((timing, min_balance)), +} +} + +pub fn timing_error_to_user_command_status( +timing_result: Result, +) -> Result { +match timing_result { + Ok(timing) => Ok(timing), + Err(err_str) => { + /* + HACK: we are matching over the full error string instead + of including an extra tag string to the Err variant + */ + if err_str.contains("minimum balance") { + return Err(TransactionFailure::SourceMinimumBalanceViolation); + } + + if err_str.contains("is insufficient") { + return Err(TransactionFailure::SourceInsufficientBalance); + } + + panic!("Unexpected timed account validation error") + } +} +} + +pub enum TimingValidation { +InsufficientBalance(B), +InvalidTiming(B), +} + +#[derive(Debug)] +struct MinBalance(Balance); + +fn validate_timing_with_min_balance_impl( +account: &Account, +txn_amount: Amount, +txn_global_slot: &Slot, +) -> (TimingValidation, Timing, MinBalance) { +use crate::Timing::*; +use TimingValidation::*; + +match &account.timing { + Untimed => { + // no time restrictions + match account.balance.sub_amount(txn_amount) { + None => ( + InsufficientBalance(true), + Untimed, + MinBalance(Balance::zero()), + ), + Some(_) => (InvalidTiming(false), Untimed, MinBalance(Balance::zero())), + } + } + Timed { + initial_minimum_balance, + .. + } => { + let account_balance = account.balance; + + let (invalid_balance, invalid_timing, curr_min_balance) = + match account_balance.sub_amount(txn_amount) { + None => { + // NB: The [initial_minimum_balance] here is the incorrect value, + // but: + // * we don't use it anywhere in this error case; and + // * we don't want to waste time computing it if it will be unused. + (true, false, *initial_minimum_balance) + } + Some(proposed_new_balance) => { + let curr_min_balance = account.min_balance_at_slot(*txn_global_slot); + + if proposed_new_balance < curr_min_balance { + (false, true, curr_min_balance) + } else { + (false, false, curr_min_balance) + } + } + }; + + // once the calculated minimum balance becomes zero, the account becomes untimed + let possibly_error = if invalid_balance { + InsufficientBalance(invalid_balance) + } else { + InvalidTiming(invalid_timing) + }; + + if curr_min_balance > Balance::zero() { + ( + possibly_error, + account.timing.clone(), + MinBalance(curr_min_balance), + ) + } else { + (possibly_error, Untimed, MinBalance(Balance::zero())) + } + } +} +} + +pub fn sub_amount(balance: Balance, amount: Amount) -> Result { +balance + .sub_amount(amount) + .ok_or_else(|| "insufficient funds".to_string()) +} + +pub fn add_amount(balance: Balance, amount: Amount) -> Result { +balance + .add_amount(amount) + .ok_or_else(|| "overflow".to_string()) +} + +#[derive(Clone, Debug)] +pub enum ExistingOrNew { +Existing(Loc), +New, +} + +pub fn get_with_location( +ledger: &mut L, +account_id: &AccountId, +) -> Result<(ExistingOrNew, Box), String> +where +L: LedgerIntf, +{ +match ledger.location_of_account(account_id) { + Some(location) => match ledger.get(&location) { + Some(account) => Ok((ExistingOrNew::Existing(location), account)), + None => panic!("Ledger location with no account"), + }, + None => Ok(( + ExistingOrNew::New, + Box::new(Account::create_with(account_id.clone(), Balance::zero())), + )), +} +} + +pub fn get_account( +ledger: &mut L, +account_id: AccountId, +) -> (Box, ExistingOrNew) +where +L: LedgerIntf, +{ +let (loc, account) = get_with_location(ledger, &account_id).unwrap(); +(account, loc) +} + +pub fn set_account<'a, L>( +l: &'a mut L, +(a, loc): (Box, &ExistingOrNew), +) -> &'a mut L +where +L: LedgerIntf, +{ +set_with_location(l, loc, a).unwrap(); +l +} + From 8061ff3bf5fbb3bd40b5d1d5583497eafe002db8 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 21:55:16 +0200 Subject: [PATCH 12/16] CHANGELOG: add description for patch 1515 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a67a8e8d0..118808c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1500](https://github.com/o1-labs/mina-rust/pull/1500)) - **Dependencies**: move all dependencies to the workspace Cargo.toml ([#1513](https://github.com/o1-labs/mina-rust/pull/1513)) +- **scan_state**: refactorize `transaction_logic.rs` in smaller modules + ([#1515](https://github.com/o1-labs/mina-rust/pull/1515)) ## v0.17.0 From 46c9cc42ee2f33f71133725e8a8a1d989d80cc0c Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 22:00:14 +0200 Subject: [PATCH 13/16] Ledger/scan_state: use mina_curves::pasta::Fp instead of mina_hasher --- .../transaction_logic/local_state.rs | 687 ++++---- .../src/scan_state/transaction_logic/mod.rs | 82 +- .../transaction_logic/protocol_state.rs | 11 +- .../transaction_logic/transaction_applied.rs | 2 +- .../transaction_partially_applied.rs | 1565 ++++++++--------- .../transaction_union_payload.rs | 389 ++-- .../src/scan_state/transaction_logic/valid.rs | 10 +- .../transaction_logic/verifiable.rs | 3 +- .../transaction_logic/zkapp_command.rs | 39 +- .../transaction_logic/zkapp_statement.rs | 6 +- 10 files changed, 1389 insertions(+), 1405 deletions(-) diff --git a/ledger/src/scan_state/transaction_logic/local_state.rs b/ledger/src/scan_state/transaction_logic/local_state.rs index 1310009c9..dacb13774 100644 --- a/ledger/src/scan_state/transaction_logic/local_state.rs +++ b/ledger/src/scan_state/transaction_logic/local_state.rs @@ -1,13 +1,10 @@ -use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; - -use ark_ff::Zero; -use itertools::{FoldWhile, Itertools}; -use mina_core::constants::ConstraintConstants; -use mina_hasher::Fp; -use poseidon::hash::{ - hash_with_kimchi, params::MINA_ACCOUNT_UPDATE_STACK_FRAME, Inputs, +use super::{ + protocol_state::{GlobalState, ProtocolStateView}, + transaction_applied::ZkappCommandApplied, + transaction_partially_applied::ZkappCommandPartiallyApplied, + zkapp_command::{AccountUpdate, CallForest, WithHash, ZkAppCommand}, + TransactionFailure, TransactionStatus, WithStatus, }; - use crate::{ proofs::{ field::{field, Boolean, ToBoolean}, @@ -26,14 +23,12 @@ use crate::{ }, AccountId, AccountIdOrderable, AppendToInputs, ToInputs, TokenId, }; - -use super::{ - protocol_state::{GlobalState, ProtocolStateView}, - transaction_applied::ZkappCommandApplied, - transaction_partially_applied::ZkappCommandPartiallyApplied, - zkapp_command::{AccountUpdate, CallForest, WithHash, ZkAppCommand}, - TransactionFailure, TransactionStatus, WithStatus, -}; +use ark_ff::Zero; +use itertools::{FoldWhile, Itertools}; +use mina_core::constants::ConstraintConstants; +use mina_curves::pasta::Fp; +use poseidon::hash::{hash_with_kimchi, params::MINA_ACCOUNT_UPDATE_STACK_FRAME, Inputs}; +use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; #[derive(Debug, Clone)] pub struct StackFrame { @@ -559,407 +554,407 @@ impl LocalState { } fn step_all( -_constraint_constants: &ConstraintConstants, -f: &impl Fn(&mut A, &GlobalState, &LocalStateEnv), -user_acc: &mut A, -(g_state, l_state): (&mut GlobalState, &mut LocalStateEnv), + _constraint_constants: &ConstraintConstants, + f: &impl Fn(&mut A, &GlobalState, &LocalStateEnv), + user_acc: &mut A, + (g_state, l_state): (&mut GlobalState, &mut LocalStateEnv), ) -> Result>, String> where -L: LedgerNonSnark, + L: LedgerNonSnark, { -while !l_state.stack_frame.calls.is_empty() { - zkapps::non_snark::step(g_state, l_state)?; - f(user_acc, g_state, l_state); -} -Ok(l_state.failure_status_tbl.clone()) + while !l_state.stack_frame.calls.is_empty() { + zkapps::non_snark::step(g_state, l_state)?; + f(user_acc, g_state, l_state); + } + Ok(l_state.failure_status_tbl.clone()) } /// apply zkapp command fee payer's while stubbing out the second pass ledger /// CAUTION: If you use the intermediate local states, you MUST update the /// [`LocalStateEnv::will_succeed`] field to `false` if the `status` is [`TransactionStatus::Failed`].*) pub fn apply_zkapp_command_first_pass_aux( -constraint_constants: &ConstraintConstants, -global_slot: Slot, -state_view: &ProtocolStateView, -init: &mut A, -f: F, -fee_excess: Option>, -supply_increase: Option>, -ledger: &mut L, -command: &ZkAppCommand, + constraint_constants: &ConstraintConstants, + global_slot: Slot, + state_view: &ProtocolStateView, + init: &mut A, + f: F, + fee_excess: Option>, + supply_increase: Option>, + ledger: &mut L, + command: &ZkAppCommand, ) -> Result, String> where -L: LedgerNonSnark, -F: Fn(&mut A, &GlobalState, &LocalStateEnv), + L: LedgerNonSnark, + F: Fn(&mut A, &GlobalState, &LocalStateEnv), { -let fee_excess = fee_excess.unwrap_or_else(Signed::zero); -let supply_increase = supply_increase.unwrap_or_else(Signed::zero); - -let previous_hash = ledger.merkle_root(); -let original_first_pass_account_states = { - let id = command.fee_payer(); - let location = { - let loc = ledger.location_of_account(&id); - let account = loc.as_ref().and_then(|loc| ledger.get(loc)); - loc.zip(account) - }; + let fee_excess = fee_excess.unwrap_or_else(Signed::zero); + let supply_increase = supply_increase.unwrap_or_else(Signed::zero); - vec![(id, location)] -}; -// let perform = |eff: Eff| Env::perform(eff); - -let (mut global_state, mut local_state) = ( - GlobalState { - protocol_state: state_view.clone(), - first_pass_ledger: ledger.clone(), - second_pass_ledger: { - // We stub out the second_pass_ledger initially, and then poke the - // correct value in place after the first pass is finished. - ::empty(0) + let previous_hash = ledger.merkle_root(); + let original_first_pass_account_states = { + let id = command.fee_payer(); + let location = { + let loc = ledger.location_of_account(&id); + let account = loc.as_ref().and_then(|loc| ledger.get(loc)); + loc.zip(account) + }; + + vec![(id, location)] + }; + // let perform = |eff: Eff| Env::perform(eff); + + let (mut global_state, mut local_state) = ( + GlobalState { + protocol_state: state_view.clone(), + first_pass_ledger: ledger.clone(), + second_pass_ledger: { + // We stub out the second_pass_ledger initially, and then poke the + // correct value in place after the first pass is finished. + ::empty(0) + }, + fee_excess, + supply_increase, + block_global_slot: global_slot, }, - fee_excess, - supply_increase, - block_global_slot: global_slot, - }, - LocalStateEnv { - stack_frame: StackFrame::default(), - call_stack: CallStack::new(), - transaction_commitment: Fp::zero(), - full_transaction_commitment: Fp::zero(), - excess: Signed::::zero(), - supply_increase, - ledger: ::empty(0), - success: true, - account_update_index: IndexInterface::zero(), - failure_status_tbl: Vec::new(), - will_succeed: true, - }, -); - -f(init, &global_state, &local_state); -let account_updates = command.all_account_updates(); - -zkapps::non_snark::start( - &mut global_state, - &mut local_state, - zkapps::non_snark::StartData { - account_updates, - memo_hash: command.memo.hash(), - // It's always valid to set this value to true, and it will - // have no effect outside of the snark. - will_succeed: true, - }, -)?; - -let command = command.clone(); -let constraint_constants = constraint_constants.clone(); -let state_view = state_view.clone(); - -let res = ZkappCommandPartiallyApplied { - command, - previous_hash, - original_first_pass_account_states, - constraint_constants, - state_view, - global_state, - local_state, -}; + LocalStateEnv { + stack_frame: StackFrame::default(), + call_stack: CallStack::new(), + transaction_commitment: Fp::zero(), + full_transaction_commitment: Fp::zero(), + excess: Signed::::zero(), + supply_increase, + ledger: ::empty(0), + success: true, + account_update_index: IndexInterface::zero(), + failure_status_tbl: Vec::new(), + will_succeed: true, + }, + ); + + f(init, &global_state, &local_state); + let account_updates = command.all_account_updates(); + + zkapps::non_snark::start( + &mut global_state, + &mut local_state, + zkapps::non_snark::StartData { + account_updates, + memo_hash: command.memo.hash(), + // It's always valid to set this value to true, and it will + // have no effect outside of the snark. + will_succeed: true, + }, + )?; + + let command = command.clone(); + let constraint_constants = constraint_constants.clone(); + let state_view = state_view.clone(); + + let res = ZkappCommandPartiallyApplied { + command, + previous_hash, + original_first_pass_account_states, + constraint_constants, + state_view, + global_state, + local_state, + }; -Ok(res) + Ok(res) } pub fn apply_zkapp_command_first_pass( -constraint_constants: &ConstraintConstants, -global_slot: Slot, -state_view: &ProtocolStateView, -fee_excess: Option>, -supply_increase: Option>, -ledger: &mut L, -command: &ZkAppCommand, + constraint_constants: &ConstraintConstants, + global_slot: Slot, + state_view: &ProtocolStateView, + fee_excess: Option>, + supply_increase: Option>, + ledger: &mut L, + command: &ZkAppCommand, ) -> Result, String> where -L: LedgerNonSnark, + L: LedgerNonSnark, { -let mut acc = (); -let partial_stmt = apply_zkapp_command_first_pass_aux( - constraint_constants, - global_slot, - state_view, - &mut acc, - |_acc, _g, _l| {}, - fee_excess, - supply_increase, - ledger, - command, -)?; - -Ok(partial_stmt) + let mut acc = (); + let partial_stmt = apply_zkapp_command_first_pass_aux( + constraint_constants, + global_slot, + state_view, + &mut acc, + |_acc, _g, _l| {}, + fee_excess, + supply_increase, + ledger, + command, + )?; + + Ok(partial_stmt) } pub fn apply_zkapp_command_second_pass_aux( -constraint_constants: &ConstraintConstants, -init: &mut A, -f: F, -ledger: &mut L, -c: ZkappCommandPartiallyApplied, + constraint_constants: &ConstraintConstants, + init: &mut A, + f: F, + ledger: &mut L, + c: ZkappCommandPartiallyApplied, ) -> Result where -L: LedgerNonSnark, -F: Fn(&mut A, &GlobalState, &LocalStateEnv), + L: LedgerNonSnark, + F: Fn(&mut A, &GlobalState, &LocalStateEnv), { -// let perform = |eff: Eff| Env::perform(eff); - -let original_account_states: Vec<(AccountId, Option<_>)> = { - // get the original states of all the accounts in each pass. - // If an account updated in the first pass is referenced in account - // updates, then retain the value before first pass application*) + // let perform = |eff: Eff| Env::perform(eff); - let accounts_referenced = c.command.accounts_referenced(); + let original_account_states: Vec<(AccountId, Option<_>)> = { + // get the original states of all the accounts in each pass. + // If an account updated in the first pass is referenced in account + // updates, then retain the value before first pass application*) - let mut account_states = BTreeMap::>::new(); + let accounts_referenced = c.command.accounts_referenced(); - let referenced = accounts_referenced.into_iter().map(|id| { - let location = { - let loc = ledger.location_of_account(&id); - let account = loc.as_ref().and_then(|loc| ledger.get(loc)); - loc.zip(account) - }; - (id, location) - }); - - c.original_first_pass_account_states - .into_iter() - .chain(referenced) - .for_each(|(id, acc_opt)| { - use std::collections::btree_map::Entry::Vacant; + let mut account_states = BTreeMap::>::new(); - let id_with_order: AccountIdOrderable = id.into(); - if let Vacant(entry) = account_states.entry(id_with_order) { - entry.insert(acc_opt); + let referenced = accounts_referenced.into_iter().map(|id| { + let location = { + let loc = ledger.location_of_account(&id); + let account = loc.as_ref().and_then(|loc| ledger.get(loc)); + loc.zip(account) }; + (id, location) }); - account_states - .into_iter() - // Convert back the `AccountIdOrder` into `AccountId`, now that they are sorted - .map(|(id, account): (AccountIdOrderable, Option<_>)| (id.into(), account)) - .collect() -}; + c.original_first_pass_account_states + .into_iter() + .chain(referenced) + .for_each(|(id, acc_opt)| { + use std::collections::btree_map::Entry::Vacant; + + let id_with_order: AccountIdOrderable = id.into(); + if let Vacant(entry) = account_states.entry(id_with_order) { + entry.insert(acc_opt); + }; + }); + + account_states + .into_iter() + // Convert back the `AccountIdOrder` into `AccountId`, now that they are sorted + .map(|(id, account): (AccountIdOrderable, Option<_>)| (id.into(), account)) + .collect() + }; -let mut account_states_after_fee_payer = { - // To check if the accounts remain unchanged in the event the transaction - // fails. First pass updates will remain even if the transaction fails to - // apply zkapp account updates*) + let mut account_states_after_fee_payer = { + // To check if the accounts remain unchanged in the event the transaction + // fails. First pass updates will remain even if the transaction fails to + // apply zkapp account updates*) - c.command.accounts_referenced().into_iter().map(|id| { - let loc = ledger.location_of_account(&id); - let a = loc.as_ref().and_then(|loc| ledger.get(loc)); + c.command.accounts_referenced().into_iter().map(|id| { + let loc = ledger.location_of_account(&id); + let a = loc.as_ref().and_then(|loc| ledger.get(loc)); - match a { - Some(a) => (id, Some((loc.unwrap(), a))), - None => (id, None), - } - }) -}; + match a { + Some(a) => (id, Some((loc.unwrap(), a))), + None => (id, None), + } + }) + }; -let accounts = || { - original_account_states - .iter() - .map(|(id, account)| (id.clone(), account.as_ref().map(|(_loc, acc)| acc.clone()))) - .collect::>() -}; + let accounts = || { + original_account_states + .iter() + .map(|(id, account)| (id.clone(), account.as_ref().map(|(_loc, acc)| acc.clone()))) + .collect::>() + }; -// Warning(OCaml): This is an abstraction leak / hack. -// Here, we update global second pass ledger to be the input ledger, and -// then update the local ledger to be the input ledger *IF AND ONLY IF* -// there are more transaction segments to be processed in this pass. + // Warning(OCaml): This is an abstraction leak / hack. + // Here, we update global second pass ledger to be the input ledger, and + // then update the local ledger to be the input ledger *IF AND ONLY IF* + // there are more transaction segments to be processed in this pass. -// TODO(OCaml): Remove this, and uplift the logic into the call in staged ledger. + // TODO(OCaml): Remove this, and uplift the logic into the call in staged ledger. -let mut global_state = GlobalState { - second_pass_ledger: ledger.clone(), - ..c.global_state -}; + let mut global_state = GlobalState { + second_pass_ledger: ledger.clone(), + ..c.global_state + }; -let mut local_state = { - if c.local_state.stack_frame.calls.is_empty() { - // Don't mess with the local state; we've already finished the - // transaction after the fee payer. - c.local_state - } else { - // Install the ledger that should already be in the local state, but - // may not be in some situations depending on who the caller is. - LocalStateEnv { - ledger: global_state.second_pass_ledger(), - ..c.local_state + let mut local_state = { + if c.local_state.stack_frame.calls.is_empty() { + // Don't mess with the local state; we've already finished the + // transaction after the fee payer. + c.local_state + } else { + // Install the ledger that should already be in the local state, but + // may not be in some situations depending on who the caller is. + LocalStateEnv { + ledger: global_state.second_pass_ledger(), + ..c.local_state + } } - } -}; - -f(init, &global_state, &local_state); -let start = (&mut global_state, &mut local_state); - -let reversed_failure_status_tbl = step_all(constraint_constants, &f, init, start)?; - -let failure_status_tbl = reversed_failure_status_tbl - .into_iter() - .rev() - .collect::>(); + }; -let account_ids_originally_not_in_ledger = - original_account_states - .iter() - .filter_map(|(acct_id, loc_and_acct)| { - if loc_and_acct.is_none() { - Some(acct_id) - } else { - None - } - }); + f(init, &global_state, &local_state); + let start = (&mut global_state, &mut local_state); -let successfully_applied = failure_status_tbl.concat().is_empty(); + let reversed_failure_status_tbl = step_all(constraint_constants, &f, init, start)?; -// if the zkapp command fails in at least 1 account update, -// then all the account updates would be cancelled except -// the fee payer one -let failure_status_tbl = if successfully_applied { - failure_status_tbl -} else { - failure_status_tbl + let failure_status_tbl = reversed_failure_status_tbl .into_iter() - .enumerate() - .map(|(idx, fs)| { - if idx > 0 && fs.is_empty() { - vec![TransactionFailure::Cancelled] - } else { - fs - } - }) - .collect() -}; + .rev() + .collect::>(); + + let account_ids_originally_not_in_ledger = + original_account_states + .iter() + .filter_map(|(acct_id, loc_and_acct)| { + if loc_and_acct.is_none() { + Some(acct_id) + } else { + None + } + }); + + let successfully_applied = failure_status_tbl.concat().is_empty(); + + // if the zkapp command fails in at least 1 account update, + // then all the account updates would be cancelled except + // the fee payer one + let failure_status_tbl = if successfully_applied { + failure_status_tbl + } else { + failure_status_tbl + .into_iter() + .enumerate() + .map(|(idx, fs)| { + if idx > 0 && fs.is_empty() { + vec![TransactionFailure::Cancelled] + } else { + fs + } + }) + .collect() + }; -// accounts not originally in ledger, now present in ledger -let new_accounts = account_ids_originally_not_in_ledger - .filter(|acct_id| ledger.location_of_account(acct_id).is_some()) - .cloned() - .collect::>(); + // accounts not originally in ledger, now present in ledger + let new_accounts = account_ids_originally_not_in_ledger + .filter(|acct_id| ledger.location_of_account(acct_id).is_some()) + .cloned() + .collect::>(); -let new_accounts_is_empty = new_accounts.is_empty(); + let new_accounts_is_empty = new_accounts.is_empty(); -let valid_result = Ok(ZkappCommandApplied { - accounts: accounts(), - command: WithStatus { - data: c.command, - status: if successfully_applied { - TransactionStatus::Applied - } else { - TransactionStatus::Failed(failure_status_tbl) - }, - }, - new_accounts, -}); - -if successfully_applied { - valid_result -} else { - let other_account_update_accounts_unchanged = account_states_after_fee_payer - .fold_while(true, |acc, (_, loc_opt)| match loc_opt { - Some((loc, a)) => match ledger.get(&loc) { - Some(a_) if !(a == a_) => FoldWhile::Done(false), - _ => FoldWhile::Continue(acc), + let valid_result = Ok(ZkappCommandApplied { + accounts: accounts(), + command: WithStatus { + data: c.command, + status: if successfully_applied { + TransactionStatus::Applied + } else { + TransactionStatus::Failed(failure_status_tbl) }, - _ => FoldWhile::Continue(acc), - }) - .into_inner(); + }, + new_accounts, + }); - // Other zkapp_command failed, therefore, updates in those should not get applied - if new_accounts_is_empty && other_account_update_accounts_unchanged { + if successfully_applied { valid_result } else { - Err("Zkapp_command application failed but new accounts created or some of the other account_update updates applied".to_string()) + let other_account_update_accounts_unchanged = account_states_after_fee_payer + .fold_while(true, |acc, (_, loc_opt)| match loc_opt { + Some((loc, a)) => match ledger.get(&loc) { + Some(a_) if !(a == a_) => FoldWhile::Done(false), + _ => FoldWhile::Continue(acc), + }, + _ => FoldWhile::Continue(acc), + }) + .into_inner(); + + // Other zkapp_command failed, therefore, updates in those should not get applied + if new_accounts_is_empty && other_account_update_accounts_unchanged { + valid_result + } else { + Err("Zkapp_command application failed but new accounts created or some of the other account_update updates applied".to_string()) + } } } -} pub fn apply_zkapp_command_second_pass( -constraint_constants: &ConstraintConstants, -ledger: &mut L, -c: ZkappCommandPartiallyApplied, + constraint_constants: &ConstraintConstants, + ledger: &mut L, + c: ZkappCommandPartiallyApplied, ) -> Result where -L: LedgerNonSnark, + L: LedgerNonSnark, { -let x = apply_zkapp_command_second_pass_aux( - constraint_constants, - &mut (), - |_, _, _| {}, - ledger, - c, -)?; -Ok(x) + let x = apply_zkapp_command_second_pass_aux( + constraint_constants, + &mut (), + |_, _, _| {}, + ledger, + c, + )?; + Ok(x) } fn apply_zkapp_command_unchecked_aux( -constraint_constants: &ConstraintConstants, -global_slot: Slot, -state_view: &ProtocolStateView, -init: &mut A, -f: F, -fee_excess: Option>, -supply_increase: Option>, -ledger: &mut L, -command: &ZkAppCommand, + constraint_constants: &ConstraintConstants, + global_slot: Slot, + state_view: &ProtocolStateView, + init: &mut A, + f: F, + fee_excess: Option>, + supply_increase: Option>, + ledger: &mut L, + command: &ZkAppCommand, ) -> Result where -L: LedgerNonSnark, -F: Fn(&mut A, &GlobalState, &LocalStateEnv), + L: LedgerNonSnark, + F: Fn(&mut A, &GlobalState, &LocalStateEnv), { -let partial_stmt = apply_zkapp_command_first_pass_aux( - constraint_constants, - global_slot, - state_view, - init, - &f, - fee_excess, - supply_increase, - ledger, - command, -)?; - -apply_zkapp_command_second_pass_aux(constraint_constants, init, &f, ledger, partial_stmt) + let partial_stmt = apply_zkapp_command_first_pass_aux( + constraint_constants, + global_slot, + state_view, + init, + &f, + fee_excess, + supply_increase, + ledger, + command, + )?; + + apply_zkapp_command_second_pass_aux(constraint_constants, init, &f, ledger, partial_stmt) } fn apply_zkapp_command_unchecked( -constraint_constants: &ConstraintConstants, -global_slot: Slot, -state_view: &ProtocolStateView, -ledger: &mut L, -command: &ZkAppCommand, + constraint_constants: &ConstraintConstants, + global_slot: Slot, + state_view: &ProtocolStateView, + ledger: &mut L, + command: &ZkAppCommand, ) -> Result<(ZkappCommandApplied, (LocalStateEnv, Signed)), String> where -L: LedgerNonSnark, + L: LedgerNonSnark, { -let zkapp_partially_applied: ZkappCommandPartiallyApplied = apply_zkapp_command_first_pass( - constraint_constants, - global_slot, - state_view, - None, - None, - ledger, - command, -)?; - -let mut state_res = None; -let account_update_applied = apply_zkapp_command_second_pass_aux( - constraint_constants, - &mut state_res, - |acc, global_state, local_state| { - *acc = Some((local_state.clone(), global_state.fee_excess)) - }, - ledger, - zkapp_partially_applied, -)?; -let (state, amount) = state_res.unwrap(); + let zkapp_partially_applied: ZkappCommandPartiallyApplied = apply_zkapp_command_first_pass( + constraint_constants, + global_slot, + state_view, + None, + None, + ledger, + command, + )?; + + let mut state_res = None; + let account_update_applied = apply_zkapp_command_second_pass_aux( + constraint_constants, + &mut state_res, + |acc, global_state, local_state| { + *acc = Some((local_state.clone(), global_state.fee_excess)) + }, + ledger, + zkapp_partially_applied, + )?; + let (state, amount) = state_res.unwrap(); -Ok((account_update_applied, (state.clone(), amount))) + Ok((account_update_applied, (state.clone(), amount))) } diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index dc1edf570..46d155638 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1,42 +1,7 @@ -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - fmt::Display, -}; - -use itertools::FoldWhile; -use mina_core::constants::ConstraintConstants; -use mina_hasher::Fp; -use mina_macros::SerdeYojsonEnum; -use mina_p2p_messages::{ - bigint::InvalidBigInt, - binprot, - v2::{MinaBaseUserCommandStableV2, MinaTransactionTransactionStableV2}, -}; -use mina_signer::CompressedPubKey; -use poseidon::hash::{ - hash_with_kimchi, - params::{CODA_RECEIPT_UC, MINA_ZKAPP_MEMO}, - Inputs, -}; - -use crate::{ - proofs::witness::Witness, - scan_state::transaction_logic::{ - transaction_applied::{CommandApplied, Varying}, - transaction_partially_applied::FullyApplied, - zkapp_command::MaybeWithStatus, - }, - sparse_ledger::{LedgerIntf, SparseLedger}, - zkapps, - zkapps::non_snark::{LedgerNonSnark, ZkappNonSnark}, - Account, AccountId, AccountIdOrderable, AppendToInputs, BaseLedger, ControlTag, - ReceiptChainHash, Timing, TokenId, VerificationKeyWire, -}; - use self::{ local_state::{ - apply_zkapp_command_first_pass, apply_zkapp_command_second_pass, CallStack, - LocalStateEnv, StackFrame, + apply_zkapp_command_first_pass, apply_zkapp_command_second_pass, CallStack, LocalStateEnv, + StackFrame, }, protocol_state::{GlobalState, ProtocolStateView}, signed_command::{SignedCommand, SignedCommandPayload}, @@ -46,14 +11,47 @@ use self::{ }, zkapp_command::{AccessedOrNot, AccountUpdate, WithHash, ZkAppCommand}, }; - use super::{ currency::{Amount, Balance, Fee, Index, Length, Magnitude, Nonce, Signed, Slot}, fee_excess::FeeExcess, fee_rate::FeeRate, scan_state::transaction_snark::OneOrTwo, }; -use crate::zkapps::zkapp_logic::ZkAppCommandElt; +use crate::{ + proofs::witness::Witness, + scan_state::transaction_logic::{ + transaction_applied::{CommandApplied, Varying}, + transaction_partially_applied::FullyApplied, + zkapp_command::MaybeWithStatus, + }, + sparse_ledger::{LedgerIntf, SparseLedger}, + zkapps, + zkapps::{ + non_snark::{LedgerNonSnark, ZkappNonSnark}, + zkapp_logic::ZkAppCommandElt, + }, + Account, AccountId, AccountIdOrderable, AppendToInputs, BaseLedger, ControlTag, + ReceiptChainHash, Timing, TokenId, VerificationKeyWire, +}; +use itertools::FoldWhile; +use mina_core::constants::ConstraintConstants; +use mina_curves::pasta::Fp; +use mina_macros::SerdeYojsonEnum; +use mina_p2p_messages::{ + bigint::InvalidBigInt, + binprot, + v2::{MinaBaseUserCommandStableV2, MinaTransactionTransactionStableV2}, +}; +use mina_signer::CompressedPubKey; +use poseidon::hash::{ + hash_with_kimchi, + params::{CODA_RECEIPT_UC, MINA_ZKAPP_MEMO}, + Inputs, +}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + fmt::Display, +}; /// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] @@ -1034,11 +1032,11 @@ impl From<&Transaction> for MinaTransactionTransactionStableV2 { } } -pub mod transaction_applied; -pub mod transaction_witness; -pub mod protocol_state; pub mod local_state; +pub mod protocol_state; +pub mod transaction_applied; pub mod transaction_partially_applied; +pub mod transaction_witness; pub use transaction_partially_applied::{ apply_transaction_first_pass, apply_transaction_second_pass, apply_transactions, apply_user_command, set_with_location, AccountState, diff --git a/ledger/src/scan_state/transaction_logic/protocol_state.rs b/ledger/src/scan_state/transaction_logic/protocol_state.rs index b8fa75787..fc7790069 100644 --- a/ledger/src/scan_state/transaction_logic/protocol_state.rs +++ b/ledger/src/scan_state/transaction_logic/protocol_state.rs @@ -1,14 +1,13 @@ -use mina_hasher::Fp; -use mina_p2p_messages::{ - bigint::InvalidBigInt, - v2::{self, MinaStateProtocolStateValueStableV2}, -}; - use crate::{ proofs::field::FieldWitness, scan_state::currency::{Amount, Length, Signed, Slot}, sparse_ledger::LedgerIntf, }; +use mina_curves::pasta::Fp; +use mina_p2p_messages::{ + bigint::InvalidBigInt, + v2::{self, MinaStateProtocolStateValueStableV2}, +}; #[derive(Debug, Clone)] pub struct EpochLedger { diff --git a/ledger/src/scan_state/transaction_logic/transaction_applied.rs b/ledger/src/scan_state/transaction_logic/transaction_applied.rs index e83be4b9b..707599afb 100644 --- a/ledger/src/scan_state/transaction_logic/transaction_applied.rs +++ b/ledger/src/scan_state/transaction_logic/transaction_applied.rs @@ -1,5 +1,5 @@ use mina_core::constants::ConstraintConstants; -use mina_hasher::Fp; +use mina_curves::pasta::Fp; use crate::{Account, AccountId}; diff --git a/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs index b45d1b13c..6b988d443 100644 --- a/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs +++ b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs @@ -7,8 +7,7 @@ use super::{ pub struct ZkappCommandPartiallyApplied { pub command: ZkAppCommand, pub previous_hash: Fp, - pub original_first_pass_account_states: - Vec<(AccountId, Option<(L::Location, Box)>)>, + pub original_first_pass_account_states: Vec<(AccountId, Option<(L::Location, Box)>)>, pub constraint_constants: ConstraintConstants, pub state_view: ProtocolStateView, pub global_state: GlobalState, @@ -47,193 +46,192 @@ where } } - pub fn apply_transaction_first_pass( -constraint_constants: &ConstraintConstants, -global_slot: Slot, -txn_state_view: &ProtocolStateView, -ledger: &mut L, -transaction: &Transaction, + constraint_constants: &ConstraintConstants, + global_slot: Slot, + txn_state_view: &ProtocolStateView, + ledger: &mut L, + transaction: &Transaction, ) -> Result, String> where -L: LedgerNonSnark, + L: LedgerNonSnark, { -use Transaction::*; -use UserCommand::*; + use Transaction::*; + use UserCommand::*; -let previous_hash = ledger.merkle_root(); -let txn_global_slot = &global_slot; + let previous_hash = ledger.merkle_root(); + let txn_global_slot = &global_slot; -match transaction { - Command(SignedCommand(cmd)) => apply_user_command( - constraint_constants, - txn_state_view, - txn_global_slot, - ledger, - cmd, - ) - .map(|applied| { - TransactionPartiallyApplied::SignedCommand(FullyApplied { - previous_hash, - applied, - }) - }), - Command(ZkAppCommand(txn)) => apply_zkapp_command_first_pass( - constraint_constants, - global_slot, - txn_state_view, - None, - None, - ledger, - txn, - ) - .map(Box::new) - .map(TransactionPartiallyApplied::ZkappCommand), - FeeTransfer(fee_transfer) => { - apply_fee_transfer(constraint_constants, txn_global_slot, ledger, fee_transfer).map( - |applied| { - TransactionPartiallyApplied::FeeTransfer(FullyApplied { - previous_hash, - applied, - }) - }, + match transaction { + Command(SignedCommand(cmd)) => apply_user_command( + constraint_constants, + txn_state_view, + txn_global_slot, + ledger, + cmd, ) - } - Coinbase(coinbase) => { - apply_coinbase(constraint_constants, txn_global_slot, ledger, coinbase).map(|applied| { - TransactionPartiallyApplied::Coinbase(FullyApplied { + .map(|applied| { + TransactionPartiallyApplied::SignedCommand(FullyApplied { previous_hash, applied, }) - }) + }), + Command(ZkAppCommand(txn)) => apply_zkapp_command_first_pass( + constraint_constants, + global_slot, + txn_state_view, + None, + None, + ledger, + txn, + ) + .map(Box::new) + .map(TransactionPartiallyApplied::ZkappCommand), + FeeTransfer(fee_transfer) => { + apply_fee_transfer(constraint_constants, txn_global_slot, ledger, fee_transfer).map( + |applied| { + TransactionPartiallyApplied::FeeTransfer(FullyApplied { + previous_hash, + applied, + }) + }, + ) + } + Coinbase(coinbase) => { + apply_coinbase(constraint_constants, txn_global_slot, ledger, coinbase).map(|applied| { + TransactionPartiallyApplied::Coinbase(FullyApplied { + previous_hash, + applied, + }) + }) + } } } -} pub fn apply_transaction_second_pass( -constraint_constants: &ConstraintConstants, -ledger: &mut L, -partial_transaction: TransactionPartiallyApplied, + constraint_constants: &ConstraintConstants, + ledger: &mut L, + partial_transaction: TransactionPartiallyApplied, ) -> Result where -L: LedgerNonSnark, + L: LedgerNonSnark, { -use TransactionPartiallyApplied as P; - -match partial_transaction { - P::SignedCommand(FullyApplied { - previous_hash, - applied, - }) => Ok(TransactionApplied { - previous_hash, - varying: Varying::Command(CommandApplied::SignedCommand(Box::new(applied))), - }), - P::ZkappCommand(partially_applied) => { - // TODO(OCaml): either here or in second phase of apply, need to update the - // prior global state statement for the fee payer segment to add the - // second phase ledger at the end - - let previous_hash = partially_applied.previous_hash; - let applied = - apply_zkapp_command_second_pass(constraint_constants, ledger, *partially_applied)?; - - Ok(TransactionApplied { + use TransactionPartiallyApplied as P; + + match partial_transaction { + P::SignedCommand(FullyApplied { previous_hash, - varying: Varying::Command(CommandApplied::ZkappCommand(Box::new(applied))), - }) + applied, + }) => Ok(TransactionApplied { + previous_hash, + varying: Varying::Command(CommandApplied::SignedCommand(Box::new(applied))), + }), + P::ZkappCommand(partially_applied) => { + // TODO(OCaml): either here or in second phase of apply, need to update the + // prior global state statement for the fee payer segment to add the + // second phase ledger at the end + + let previous_hash = partially_applied.previous_hash; + let applied = + apply_zkapp_command_second_pass(constraint_constants, ledger, *partially_applied)?; + + Ok(TransactionApplied { + previous_hash, + varying: Varying::Command(CommandApplied::ZkappCommand(Box::new(applied))), + }) + } + P::FeeTransfer(FullyApplied { + previous_hash, + applied, + }) => Ok(TransactionApplied { + previous_hash, + varying: Varying::FeeTransfer(applied), + }), + P::Coinbase(FullyApplied { + previous_hash, + applied, + }) => Ok(TransactionApplied { + previous_hash, + varying: Varying::Coinbase(applied), + }), } - P::FeeTransfer(FullyApplied { - previous_hash, - applied, - }) => Ok(TransactionApplied { - previous_hash, - varying: Varying::FeeTransfer(applied), - }), - P::Coinbase(FullyApplied { - previous_hash, - applied, - }) => Ok(TransactionApplied { - previous_hash, - varying: Varying::Coinbase(applied), - }), -} } pub fn apply_transactions( -constraint_constants: &ConstraintConstants, -global_slot: Slot, -txn_state_view: &ProtocolStateView, -ledger: &mut L, -txns: &[Transaction], + constraint_constants: &ConstraintConstants, + global_slot: Slot, + txn_state_view: &ProtocolStateView, + ledger: &mut L, + txns: &[Transaction], ) -> Result, String> where -L: LedgerNonSnark, + L: LedgerNonSnark, { -let first_pass: Vec<_> = txns - .iter() - .map(|txn| { - apply_transaction_first_pass( - constraint_constants, - global_slot, - txn_state_view, - ledger, - txn, - ) - }) - .collect::>, _>>()?; + let first_pass: Vec<_> = txns + .iter() + .map(|txn| { + apply_transaction_first_pass( + constraint_constants, + global_slot, + txn_state_view, + ledger, + txn, + ) + }) + .collect::>, _>>()?; -first_pass - .into_iter() - .map(|partial_transaction| { - apply_transaction_second_pass(constraint_constants, ledger, partial_transaction) - }) - .collect() + first_pass + .into_iter() + .map(|partial_transaction| { + apply_transaction_second_pass(constraint_constants, ledger, partial_transaction) + }) + .collect() } struct FailureCollection { -inner: Vec>, + inner: Vec>, } /// impl FailureCollection { -fn empty() -> Self { - Self { - inner: Vec::default(), + fn empty() -> Self { + Self { + inner: Vec::default(), + } } -} -fn no_failure() -> Vec { - vec![] -} + fn no_failure() -> Vec { + vec![] + } -/// -fn single_failure() -> Self { - Self { - inner: vec![vec![TransactionFailure::UpdateNotPermittedBalance]], + /// + fn single_failure() -> Self { + Self { + inner: vec![vec![TransactionFailure::UpdateNotPermittedBalance]], + } } -} -fn update_failed() -> Vec { - vec![TransactionFailure::UpdateNotPermittedBalance] -} + fn update_failed() -> Vec { + vec![TransactionFailure::UpdateNotPermittedBalance] + } -/// -fn append_entry(list: Vec, mut s: Self) -> Self { - if s.inner.is_empty() { - Self { inner: vec![list] } - } else { - s.inner.insert(1, list); - s + /// + fn append_entry(list: Vec, mut s: Self) -> Self { + if s.inner.is_empty() { + Self { inner: vec![list] } + } else { + s.inner.insert(1, list); + s + } } -} -fn is_empty(&self) -> bool { - self.inner.iter().all(Vec::is_empty) -} + fn is_empty(&self) -> bool { + self.inner.iter().all(Vec::is_empty) + } -fn take(self) -> Vec> { - self.inner -} + fn take(self) -> Vec> { + self.inner + } } /// Structure of the failure status: @@ -248,230 +246,230 @@ fn take(self) -> Vec> { /// /// fn apply_coinbase( -constraint_constants: &ConstraintConstants, -txn_global_slot: &Slot, -ledger: &mut L, -coinbase: &Coinbase, + constraint_constants: &ConstraintConstants, + txn_global_slot: &Slot, + ledger: &mut L, + coinbase: &Coinbase, ) -> Result where -L: LedgerIntf, + L: LedgerIntf, { -let Coinbase { - receiver, - amount: coinbase_amount, - fee_transfer, -} = &coinbase; - -let ( - receiver_reward, - new_accounts1, - transferee_update, - transferee_timing_prev, - failures1, - burned_tokens1, -) = match fee_transfer { - None => ( - *coinbase_amount, - None, - None, - None, - FailureCollection::empty(), - Amount::zero(), - ), - Some( - ft @ CoinbaseFeeTransfer { - receiver_pk: transferee, - fee, - }, - ) => { - assert_ne!(transferee, receiver); - - let transferee_id = ft.receiver(); - let fee = Amount::of_fee(fee); - - let receiver_reward = coinbase_amount - .checked_sub(&fee) - .ok_or_else(|| "Coinbase fee transfer too large".to_string())?; - - let (transferee_account, action, can_receive) = - has_permission_to_receive(ledger, &transferee_id); - let new_accounts = get_new_accounts(action, transferee_id.clone()); - - let timing = update_timing_when_no_deduction(txn_global_slot, &transferee_account)?; - - let balance = { - let amount = sub_account_creation_fee(constraint_constants, action, fee)?; - add_amount(transferee_account.balance, amount)? - }; - - if can_receive.0 { - let (_, mut transferee_account, transferee_location) = - ledger.get_or_create(&transferee_id)?; - - transferee_account.balance = balance; - transferee_account.timing = timing; - - let timing = transferee_account.timing.clone(); - - ( - receiver_reward, - new_accounts, - Some((transferee_location, transferee_account)), - Some(timing), - FailureCollection::append_entry( - FailureCollection::no_failure(), - FailureCollection::empty(), - ), - Amount::zero(), - ) - } else { - ( - receiver_reward, - None, - None, - None, - FailureCollection::single_failure(), + let Coinbase { + receiver, + amount: coinbase_amount, + fee_transfer, + } = &coinbase; + + let ( + receiver_reward, + new_accounts1, + transferee_update, + transferee_timing_prev, + failures1, + burned_tokens1, + ) = match fee_transfer { + None => ( + *coinbase_amount, + None, + None, + None, + FailureCollection::empty(), + Amount::zero(), + ), + Some( + ft @ CoinbaseFeeTransfer { + receiver_pk: transferee, fee, - ) - } - } -}; + }, + ) => { + assert_ne!(transferee, receiver); -let receiver_id = AccountId::new(receiver.clone(), TokenId::default()); -let (receiver_account, action2, can_receive) = has_permission_to_receive(ledger, &receiver_id); -let new_accounts2 = get_new_accounts(action2, receiver_id.clone()); + let transferee_id = ft.receiver(); + let fee = Amount::of_fee(fee); -// Note: Updating coinbase receiver timing only if there is no fee transfer. -// This is so as to not add any extra constraints in transaction snark for checking -// "receiver" timings. This is OK because timing rules will not be violated when -// balance increases and will be checked whenever an amount is deducted from the -// account (#5973) + let receiver_reward = coinbase_amount + .checked_sub(&fee) + .ok_or_else(|| "Coinbase fee transfer too large".to_string())?; -let coinbase_receiver_timing = match transferee_timing_prev { - None => update_timing_when_no_deduction(txn_global_slot, &receiver_account)?, - Some(_) => receiver_account.timing.clone(), -}; + let (transferee_account, action, can_receive) = + has_permission_to_receive(ledger, &transferee_id); + let new_accounts = get_new_accounts(action, transferee_id.clone()); -let receiver_balance = { - let amount = sub_account_creation_fee(constraint_constants, action2, receiver_reward)?; - add_amount(receiver_account.balance, amount)? -}; + let timing = update_timing_when_no_deduction(txn_global_slot, &transferee_account)?; -let (failures, burned_tokens2) = if can_receive.0 { - let (_action2, mut receiver_account, receiver_location) = - ledger.get_or_create(&receiver_id)?; + let balance = { + let amount = sub_account_creation_fee(constraint_constants, action, fee)?; + add_amount(transferee_account.balance, amount)? + }; - receiver_account.balance = receiver_balance; - receiver_account.timing = coinbase_receiver_timing; + if can_receive.0 { + let (_, mut transferee_account, transferee_location) = + ledger.get_or_create(&transferee_id)?; - ledger.set(&receiver_location, receiver_account); + transferee_account.balance = balance; + transferee_account.timing = timing; - ( - FailureCollection::append_entry(FailureCollection::no_failure(), failures1), - Amount::zero(), - ) -} else { - ( - FailureCollection::append_entry(FailureCollection::update_failed(), failures1), - receiver_reward, - ) -}; + let timing = transferee_account.timing.clone(); -if let Some((addr, account)) = transferee_update { - ledger.set(&addr, account); -}; + ( + receiver_reward, + new_accounts, + Some((transferee_location, transferee_account)), + Some(timing), + FailureCollection::append_entry( + FailureCollection::no_failure(), + FailureCollection::empty(), + ), + Amount::zero(), + ) + } else { + ( + receiver_reward, + None, + None, + None, + FailureCollection::single_failure(), + fee, + ) + } + } + }; -let burned_tokens = burned_tokens1 - .checked_add(&burned_tokens2) - .ok_or_else(|| "burned tokens overflow".to_string())?; + let receiver_id = AccountId::new(receiver.clone(), TokenId::default()); + let (receiver_account, action2, can_receive) = has_permission_to_receive(ledger, &receiver_id); + let new_accounts2 = get_new_accounts(action2, receiver_id.clone()); -let status = if failures.is_empty() { - TransactionStatus::Applied -} else { - TransactionStatus::Failed(failures.take()) -}; + // Note: Updating coinbase receiver timing only if there is no fee transfer. + // This is so as to not add any extra constraints in transaction snark for checking + // "receiver" timings. This is OK because timing rules will not be violated when + // balance increases and will be checked whenever an amount is deducted from the + // account (#5973) + + let coinbase_receiver_timing = match transferee_timing_prev { + None => update_timing_when_no_deduction(txn_global_slot, &receiver_account)?, + Some(_) => receiver_account.timing.clone(), + }; + + let receiver_balance = { + let amount = sub_account_creation_fee(constraint_constants, action2, receiver_reward)?; + add_amount(receiver_account.balance, amount)? + }; + + let (failures, burned_tokens2) = if can_receive.0 { + let (_action2, mut receiver_account, receiver_location) = + ledger.get_or_create(&receiver_id)?; + + receiver_account.balance = receiver_balance; + receiver_account.timing = coinbase_receiver_timing; -let new_accounts: Vec<_> = [new_accounts1, new_accounts2] - .into_iter() - .flatten() - .collect(); - -Ok(transaction_applied::CoinbaseApplied { - coinbase: WithStatus { - data: coinbase.clone(), - status, - }, - new_accounts, - burned_tokens, -}) + ledger.set(&receiver_location, receiver_account); + + ( + FailureCollection::append_entry(FailureCollection::no_failure(), failures1), + Amount::zero(), + ) + } else { + ( + FailureCollection::append_entry(FailureCollection::update_failed(), failures1), + receiver_reward, + ) + }; + + if let Some((addr, account)) = transferee_update { + ledger.set(&addr, account); + }; + + let burned_tokens = burned_tokens1 + .checked_add(&burned_tokens2) + .ok_or_else(|| "burned tokens overflow".to_string())?; + + let status = if failures.is_empty() { + TransactionStatus::Applied + } else { + TransactionStatus::Failed(failures.take()) + }; + + let new_accounts: Vec<_> = [new_accounts1, new_accounts2] + .into_iter() + .flatten() + .collect(); + + Ok(transaction_applied::CoinbaseApplied { + coinbase: WithStatus { + data: coinbase.clone(), + status, + }, + new_accounts, + burned_tokens, + }) } /// fn apply_fee_transfer( -constraint_constants: &ConstraintConstants, -txn_global_slot: &Slot, -ledger: &mut L, -fee_transfer: &FeeTransfer, + constraint_constants: &ConstraintConstants, + txn_global_slot: &Slot, + ledger: &mut L, + fee_transfer: &FeeTransfer, ) -> Result where -L: LedgerIntf, + L: LedgerIntf, { -let (new_accounts, failures, burned_tokens) = process_fee_transfer( - ledger, - fee_transfer, - |action, _, balance, fee| { - let amount = { - let amount = Amount::of_fee(fee); - sub_account_creation_fee(constraint_constants, action, amount)? - }; - add_amount(balance, amount) - }, - |account| update_timing_when_no_deduction(txn_global_slot, account), -)?; - -let status = if failures.is_empty() { - TransactionStatus::Applied -} else { - TransactionStatus::Failed(failures.take()) -}; + let (new_accounts, failures, burned_tokens) = process_fee_transfer( + ledger, + fee_transfer, + |action, _, balance, fee| { + let amount = { + let amount = Amount::of_fee(fee); + sub_account_creation_fee(constraint_constants, action, amount)? + }; + add_amount(balance, amount) + }, + |account| update_timing_when_no_deduction(txn_global_slot, account), + )?; + + let status = if failures.is_empty() { + TransactionStatus::Applied + } else { + TransactionStatus::Failed(failures.take()) + }; -Ok(transaction_applied::FeeTransferApplied { - fee_transfer: WithStatus { - data: fee_transfer.clone(), - status, - }, - new_accounts, - burned_tokens, -}) + Ok(transaction_applied::FeeTransferApplied { + fee_transfer: WithStatus { + data: fee_transfer.clone(), + status, + }, + new_accounts, + burned_tokens, + }) } /// fn sub_account_creation_fee( -constraint_constants: &ConstraintConstants, -action: AccountState, -amount: Amount, + constraint_constants: &ConstraintConstants, + action: AccountState, + amount: Amount, ) -> Result { -let account_creation_fee = Amount::from_u64(constraint_constants.account_creation_fee); + let account_creation_fee = Amount::from_u64(constraint_constants.account_creation_fee); -match action { - AccountState::Added => { - if let Some(amount) = amount.checked_sub(&account_creation_fee) { - return Ok(amount); + match action { + AccountState::Added => { + if let Some(amount) = amount.checked_sub(&account_creation_fee) { + return Ok(amount); + } + Err(format!( + "Error subtracting account creation fee {:?}; transaction amount {:?} insufficient", + account_creation_fee, amount + )) } - Err(format!( - "Error subtracting account creation fee {:?}; transaction amount {:?} insufficient", - account_creation_fee, amount - )) + AccountState::Existed => Ok(amount), } - AccountState::Existed => Ok(amount), -} } fn update_timing_when_no_deduction( -txn_global_slot: &Slot, -account: &Account, + txn_global_slot: &Slot, + account: &Account, ) -> Result { -validate_timing(account, Amount::zero(), txn_global_slot) + validate_timing(account, Amount::zero(), txn_global_slot) } // /// TODO: Move this to the ledger @@ -502,10 +500,10 @@ validate_timing(account, Amount::zero(), txn_global_slot) // } fn get_new_accounts(action: AccountState, data: T) -> Option { -match action { - AccountState::Added => Some(data), - AccountState::Existed => None, -} + match action { + AccountState::Added => Some(data), + AccountState::Existed => None, + } } /// Structure of the failure status: @@ -519,167 +517,167 @@ match action { /// First fails and second succeeds: /// [[failure-of-first-fee-transfer];[]] fn process_fee_transfer( -ledger: &mut L, -fee_transfer: &FeeTransfer, -modify_balance: FunBalance, -modify_timing: FunTiming, + ledger: &mut L, + fee_transfer: &FeeTransfer, + modify_balance: FunBalance, + modify_timing: FunTiming, ) -> Result<(Vec, FailureCollection, Amount), String> where -L: LedgerIntf, -FunTiming: Fn(&Account) -> Result, -FunBalance: Fn(AccountState, &AccountId, Balance, &Fee) -> Result, + L: LedgerIntf, + FunTiming: Fn(&Account) -> Result, + FunBalance: Fn(AccountState, &AccountId, Balance, &Fee) -> Result, { -if !fee_transfer.fee_tokens().all(TokenId::is_default) { - return Err("Cannot pay fees in non-default tokens.".to_string()); -} - -match &**fee_transfer { - OneOrTwo::One(fee_transfer) => { - let account_id = fee_transfer.receiver(); - let (a, action, can_receive) = has_permission_to_receive(ledger, &account_id); - - let timing = modify_timing(&a)?; - let balance = modify_balance(action, &account_id, a.balance, &fee_transfer.fee)?; - - if can_receive.0 { - let (_, mut account, loc) = ledger.get_or_create(&account_id)?; - let new_accounts = get_new_accounts(action, account_id.clone()); - - account.balance = balance; - account.timing = timing; - - ledger.set(&loc, account); - - let new_accounts: Vec<_> = new_accounts.into_iter().collect(); - Ok((new_accounts, FailureCollection::empty(), Amount::zero())) - } else { - Ok(( - vec![], - FailureCollection::single_failure(), - Amount::of_fee(&fee_transfer.fee), - )) - } + if !fee_transfer.fee_tokens().all(TokenId::is_default) { + return Err("Cannot pay fees in non-default tokens.".to_string()); } - OneOrTwo::Two((fee_transfer1, fee_transfer2)) => { - let account_id1 = fee_transfer1.receiver(); - let (a1, action1, can_receive1) = has_permission_to_receive(ledger, &account_id1); - let account_id2 = fee_transfer2.receiver(); + match &**fee_transfer { + OneOrTwo::One(fee_transfer) => { + let account_id = fee_transfer.receiver(); + let (a, action, can_receive) = has_permission_to_receive(ledger, &account_id); - if account_id1 == account_id2 { - let fee = fee_transfer1 - .fee - .checked_add(&fee_transfer2.fee) - .ok_or_else(|| "Overflow".to_string())?; + let timing = modify_timing(&a)?; + let balance = modify_balance(action, &account_id, a.balance, &fee_transfer.fee)?; - let timing = modify_timing(&a1)?; - let balance = modify_balance(action1, &account_id1, a1.balance, &fee)?; + if can_receive.0 { + let (_, mut account, loc) = ledger.get_or_create(&account_id)?; + let new_accounts = get_new_accounts(action, account_id.clone()); - if can_receive1.0 { - let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?; - let new_accounts1 = get_new_accounts(action1, account_id1); + account.balance = balance; + account.timing = timing; - a1.balance = balance; - a1.timing = timing; + ledger.set(&loc, account); - ledger.set(&l1, a1); - - let new_accounts: Vec<_> = new_accounts1.into_iter().collect(); + let new_accounts: Vec<_> = new_accounts.into_iter().collect(); Ok((new_accounts, FailureCollection::empty(), Amount::zero())) } else { - // failure for each fee transfer single - Ok(( vec![], - FailureCollection::append_entry( - FailureCollection::update_failed(), - FailureCollection::single_failure(), - ), - Amount::of_fee(&fee), + FailureCollection::single_failure(), + Amount::of_fee(&fee_transfer.fee), )) } - } else { - let (a2, action2, can_receive2) = has_permission_to_receive(ledger, &account_id2); - - let balance1 = - modify_balance(action1, &account_id1, a1.balance, &fee_transfer1.fee)?; - - // Note: Not updating the timing field of a1 to avoid additional check - // in transactions snark (check_timing for "receiver"). This is OK - // because timing rules will not be violated when balance increases - // and will be checked whenever an amount is deducted from the account. (#5973)*) - - let timing2 = modify_timing(&a2)?; - let balance2 = - modify_balance(action2, &account_id2, a2.balance, &fee_transfer2.fee)?; - - let (new_accounts1, failures, burned_tokens1) = if can_receive1.0 { - let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?; - let new_accounts1 = get_new_accounts(action1, account_id1); - - a1.balance = balance1; - ledger.set(&l1, a1); - - ( - new_accounts1, - FailureCollection::append_entry( - FailureCollection::no_failure(), - FailureCollection::empty(), - ), - Amount::zero(), - ) - } else { - ( - None, - FailureCollection::single_failure(), - Amount::of_fee(&fee_transfer1.fee), - ) - }; - - let (new_accounts2, failures, burned_tokens2) = if can_receive2.0 { - let (_, mut a2, l2) = ledger.get_or_create(&account_id2)?; - let new_accounts2 = get_new_accounts(action2, account_id2); - - a2.balance = balance2; - a2.timing = timing2; - - ledger.set(&l2, a2); - - ( - new_accounts2, - FailureCollection::append_entry(FailureCollection::no_failure(), failures), - Amount::zero(), - ) + } + OneOrTwo::Two((fee_transfer1, fee_transfer2)) => { + let account_id1 = fee_transfer1.receiver(); + let (a1, action1, can_receive1) = has_permission_to_receive(ledger, &account_id1); + + let account_id2 = fee_transfer2.receiver(); + + if account_id1 == account_id2 { + let fee = fee_transfer1 + .fee + .checked_add(&fee_transfer2.fee) + .ok_or_else(|| "Overflow".to_string())?; + + let timing = modify_timing(&a1)?; + let balance = modify_balance(action1, &account_id1, a1.balance, &fee)?; + + if can_receive1.0 { + let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?; + let new_accounts1 = get_new_accounts(action1, account_id1); + + a1.balance = balance; + a1.timing = timing; + + ledger.set(&l1, a1); + + let new_accounts: Vec<_> = new_accounts1.into_iter().collect(); + Ok((new_accounts, FailureCollection::empty(), Amount::zero())) + } else { + // failure for each fee transfer single + + Ok(( + vec![], + FailureCollection::append_entry( + FailureCollection::update_failed(), + FailureCollection::single_failure(), + ), + Amount::of_fee(&fee), + )) + } } else { - ( - None, - FailureCollection::append_entry( - FailureCollection::update_failed(), - failures, - ), - Amount::of_fee(&fee_transfer2.fee), - ) - }; - - let burned_tokens = burned_tokens1 - .checked_add(&burned_tokens2) - .ok_or_else(|| "burned tokens overflow".to_string())?; - - let new_accounts: Vec<_> = [new_accounts1, new_accounts2] - .into_iter() - .flatten() - .collect(); - - Ok((new_accounts, failures, burned_tokens)) + let (a2, action2, can_receive2) = has_permission_to_receive(ledger, &account_id2); + + let balance1 = + modify_balance(action1, &account_id1, a1.balance, &fee_transfer1.fee)?; + + // Note: Not updating the timing field of a1 to avoid additional check + // in transactions snark (check_timing for "receiver"). This is OK + // because timing rules will not be violated when balance increases + // and will be checked whenever an amount is deducted from the account. (#5973)*) + + let timing2 = modify_timing(&a2)?; + let balance2 = + modify_balance(action2, &account_id2, a2.balance, &fee_transfer2.fee)?; + + let (new_accounts1, failures, burned_tokens1) = if can_receive1.0 { + let (_, mut a1, l1) = ledger.get_or_create(&account_id1)?; + let new_accounts1 = get_new_accounts(action1, account_id1); + + a1.balance = balance1; + ledger.set(&l1, a1); + + ( + new_accounts1, + FailureCollection::append_entry( + FailureCollection::no_failure(), + FailureCollection::empty(), + ), + Amount::zero(), + ) + } else { + ( + None, + FailureCollection::single_failure(), + Amount::of_fee(&fee_transfer1.fee), + ) + }; + + let (new_accounts2, failures, burned_tokens2) = if can_receive2.0 { + let (_, mut a2, l2) = ledger.get_or_create(&account_id2)?; + let new_accounts2 = get_new_accounts(action2, account_id2); + + a2.balance = balance2; + a2.timing = timing2; + + ledger.set(&l2, a2); + + ( + new_accounts2, + FailureCollection::append_entry(FailureCollection::no_failure(), failures), + Amount::zero(), + ) + } else { + ( + None, + FailureCollection::append_entry( + FailureCollection::update_failed(), + failures, + ), + Amount::of_fee(&fee_transfer2.fee), + ) + }; + + let burned_tokens = burned_tokens1 + .checked_add(&burned_tokens2) + .ok_or_else(|| "burned tokens overflow".to_string())?; + + let new_accounts: Vec<_> = [new_accounts1, new_accounts2] + .into_iter() + .flatten() + .collect(); + + Ok((new_accounts, failures, burned_tokens)) + } } } } -} #[derive(Copy, Clone, Debug)] pub enum AccountState { -Added, -Existed, + Added, + Existed, } #[derive(Debug)] @@ -687,390 +685,389 @@ struct HasPermissionToReceive(bool); /// fn has_permission_to_receive( -ledger: &mut L, -receiver_account_id: &AccountId, + ledger: &mut L, + receiver_account_id: &AccountId, ) -> (Box, AccountState, HasPermissionToReceive) where -L: LedgerIntf, + L: LedgerIntf, { -use crate::PermissionTo::*; -use AccountState::*; + use crate::PermissionTo::*; + use AccountState::*; -let init_account = Account::initialize(receiver_account_id); + let init_account = Account::initialize(receiver_account_id); -match ledger.location_of_account(receiver_account_id) { - None => { - // new account, check that default permissions allow receiving - let perm = init_account.has_permission_to(ControlTag::NoneGiven, Receive); - (Box::new(init_account), Added, HasPermissionToReceive(perm)) - } - Some(location) => match ledger.get(&location) { - None => panic!("Ledger location with no account"), - Some(receiver_account) => { - let perm = receiver_account.has_permission_to(ControlTag::NoneGiven, Receive); - (receiver_account, Existed, HasPermissionToReceive(perm)) + match ledger.location_of_account(receiver_account_id) { + None => { + // new account, check that default permissions allow receiving + let perm = init_account.has_permission_to(ControlTag::NoneGiven, Receive); + (Box::new(init_account), Added, HasPermissionToReceive(perm)) } - }, -} + Some(location) => match ledger.get(&location) { + None => panic!("Ledger location with no account"), + Some(receiver_account) => { + let perm = receiver_account.has_permission_to(ControlTag::NoneGiven, Receive); + (receiver_account, Existed, HasPermissionToReceive(perm)) + } + }, + } } pub fn validate_time(valid_until: &Slot, current_global_slot: &Slot) -> Result<(), String> { -if current_global_slot <= valid_until { - return Ok(()); -} + if current_global_slot <= valid_until { + return Ok(()); + } -Err(format!( - "Current global slot {:?} greater than transaction expiry slot {:?}", - current_global_slot, valid_until -)) + Err(format!( + "Current global slot {:?} greater than transaction expiry slot {:?}", + current_global_slot, valid_until + )) } pub fn is_timed(a: &Account) -> bool { -matches!(&a.timing, Timing::Timed { .. }) + matches!(&a.timing, Timing::Timed { .. }) } pub fn set_with_location( -ledger: &mut L, -location: &ExistingOrNew, -account: Box, + ledger: &mut L, + location: &ExistingOrNew, + account: Box, ) -> Result<(), String> where -L: LedgerIntf, + L: LedgerIntf, { -match location { - ExistingOrNew::Existing(location) => { - ledger.set(location, account); - Ok(()) + match location { + ExistingOrNew::Existing(location) => { + ledger.set(location, account); + Ok(()) + } + ExistingOrNew::New => ledger + .create_new_account(account.id(), *account) + .map_err(|_| "set_with_location".to_string()), } - ExistingOrNew::New => ledger - .create_new_account(account.id(), *account) - .map_err(|_| "set_with_location".to_string()), -} } pub struct Updates { -pub located_accounts: Vec<(ExistingOrNew, Box)>, -pub applied_body: signed_command_applied::Body, + pub located_accounts: Vec<(ExistingOrNew, Box)>, + pub applied_body: signed_command_applied::Body, } pub fn compute_updates( -constraint_constants: &ConstraintConstants, -receiver: AccountId, -ledger: &mut L, -current_global_slot: &Slot, -user_command: &SignedCommand, -fee_payer: &AccountId, -fee_payer_account: &Account, -fee_payer_location: &ExistingOrNew, -reject_command: &mut bool, + constraint_constants: &ConstraintConstants, + receiver: AccountId, + ledger: &mut L, + current_global_slot: &Slot, + user_command: &SignedCommand, + fee_payer: &AccountId, + fee_payer_account: &Account, + fee_payer_location: &ExistingOrNew, + reject_command: &mut bool, ) -> Result, TransactionFailure> where -L: LedgerIntf, + L: LedgerIntf, { -match &user_command.payload.body { - signed_command::Body::StakeDelegation(_) => { - let (receiver_location, _) = get_with_location(ledger, &receiver).unwrap(); - - if let ExistingOrNew::New = receiver_location { - return Err(TransactionFailure::ReceiverNotPresent); - } - if !fee_payer_account.has_permission_to_set_delegate() { - return Err(TransactionFailure::UpdateNotPermittedDelegate); - } + match &user_command.payload.body { + signed_command::Body::StakeDelegation(_) => { + let (receiver_location, _) = get_with_location(ledger, &receiver).unwrap(); - let previous_delegate = fee_payer_account.delegate.clone(); - - // Timing is always valid, but we need to record any switch from - // timed to untimed here to stay in sync with the snark. - let fee_payer_account = { - let timing = timing_error_to_user_command_status(validate_timing( - fee_payer_account, - Amount::zero(), - current_global_slot, - ))?; - - Box::new(Account { - delegate: Some(receiver.public_key.clone()), - timing, - ..fee_payer_account.clone() - }) - }; - - Ok(Updates { - located_accounts: vec![(fee_payer_location.clone(), fee_payer_account)], - applied_body: signed_command_applied::Body::StakeDelegation { previous_delegate }, - }) - } - signed_command::Body::Payment(payment) => { - let get_fee_payer_account = || { - let balance = fee_payer_account - .balance - .sub_amount(payment.amount) - .ok_or(TransactionFailure::SourceInsufficientBalance)?; - - let timing = timing_error_to_user_command_status(validate_timing( - fee_payer_account, - payment.amount, - current_global_slot, - ))?; - - Ok(Box::new(Account { - balance, - timing, - ..fee_payer_account.clone() - })) - }; - - let fee_payer_account = match get_fee_payer_account() { - Ok(fee_payer_account) => fee_payer_account, - Err(e) => { - // OCaml throw an exception when an error occurs here - // Here in Rust we set `reject_command` to differentiate the 3 cases (Ok, Err, exception) - // - // - - // Don't accept transactions with insufficient balance from the fee-payer. - // TODO(OCaml): eliminate this condition and accept transaction with failed status - *reject_command = true; - return Err(e); + if let ExistingOrNew::New = receiver_location { + return Err(TransactionFailure::ReceiverNotPresent); + } + if !fee_payer_account.has_permission_to_set_delegate() { + return Err(TransactionFailure::UpdateNotPermittedDelegate); } - }; - let (receiver_location, mut receiver_account) = if fee_payer == &receiver { - (fee_payer_location.clone(), fee_payer_account.clone()) - } else { - get_with_location(ledger, &receiver).unwrap() - }; + let previous_delegate = fee_payer_account.delegate.clone(); - if !fee_payer_account.has_permission_to_send() { - return Err(TransactionFailure::UpdateNotPermittedBalance); - } + // Timing is always valid, but we need to record any switch from + // timed to untimed here to stay in sync with the snark. + let fee_payer_account = { + let timing = timing_error_to_user_command_status(validate_timing( + fee_payer_account, + Amount::zero(), + current_global_slot, + ))?; - if !receiver_account.has_permission_to_receive() { - return Err(TransactionFailure::UpdateNotPermittedBalance); + Box::new(Account { + delegate: Some(receiver.public_key.clone()), + timing, + ..fee_payer_account.clone() + }) + }; + + Ok(Updates { + located_accounts: vec![(fee_payer_location.clone(), fee_payer_account)], + applied_body: signed_command_applied::Body::StakeDelegation { previous_delegate }, + }) } + signed_command::Body::Payment(payment) => { + let get_fee_payer_account = || { + let balance = fee_payer_account + .balance + .sub_amount(payment.amount) + .ok_or(TransactionFailure::SourceInsufficientBalance)?; + + let timing = timing_error_to_user_command_status(validate_timing( + fee_payer_account, + payment.amount, + current_global_slot, + ))?; + + Ok(Box::new(Account { + balance, + timing, + ..fee_payer_account.clone() + })) + }; - let receiver_amount = match &receiver_location { - ExistingOrNew::Existing(_) => payment.amount, - ExistingOrNew::New => { - match payment - .amount - .checked_sub(&Amount::from_u64(constraint_constants.account_creation_fee)) - { - Some(amount) => amount, - None => return Err(TransactionFailure::AmountInsufficientToCreateAccount), + let fee_payer_account = match get_fee_payer_account() { + Ok(fee_payer_account) => fee_payer_account, + Err(e) => { + // OCaml throw an exception when an error occurs here + // Here in Rust we set `reject_command` to differentiate the 3 cases (Ok, Err, exception) + // + // + + // Don't accept transactions with insufficient balance from the fee-payer. + // TODO(OCaml): eliminate this condition and accept transaction with failed status + *reject_command = true; + return Err(e); } + }; + + let (receiver_location, mut receiver_account) = if fee_payer == &receiver { + (fee_payer_location.clone(), fee_payer_account.clone()) + } else { + get_with_location(ledger, &receiver).unwrap() + }; + + if !fee_payer_account.has_permission_to_send() { + return Err(TransactionFailure::UpdateNotPermittedBalance); } - }; - let balance = match receiver_account.balance.add_amount(receiver_amount) { - Some(balance) => balance, - None => return Err(TransactionFailure::Overflow), - }; + if !receiver_account.has_permission_to_receive() { + return Err(TransactionFailure::UpdateNotPermittedBalance); + } - let new_accounts = match receiver_location { - ExistingOrNew::New => vec![receiver.clone()], - ExistingOrNew::Existing(_) => vec![], - }; + let receiver_amount = match &receiver_location { + ExistingOrNew::Existing(_) => payment.amount, + ExistingOrNew::New => { + match payment + .amount + .checked_sub(&Amount::from_u64(constraint_constants.account_creation_fee)) + { + Some(amount) => amount, + None => return Err(TransactionFailure::AmountInsufficientToCreateAccount), + } + } + }; - receiver_account.balance = balance; + let balance = match receiver_account.balance.add_amount(receiver_amount) { + Some(balance) => balance, + None => return Err(TransactionFailure::Overflow), + }; - let updated_accounts = if fee_payer == &receiver { - // [receiver_account] at this point has all the updates - vec![(receiver_location, receiver_account)] - } else { - vec![ - (receiver_location, receiver_account), - (fee_payer_location.clone(), fee_payer_account), - ] - }; + let new_accounts = match receiver_location { + ExistingOrNew::New => vec![receiver.clone()], + ExistingOrNew::Existing(_) => vec![], + }; - Ok(Updates { - located_accounts: updated_accounts, - applied_body: signed_command_applied::Body::Payments { new_accounts }, - }) + receiver_account.balance = balance; + + let updated_accounts = if fee_payer == &receiver { + // [receiver_account] at this point has all the updates + vec![(receiver_location, receiver_account)] + } else { + vec![ + (receiver_location, receiver_account), + (fee_payer_location.clone(), fee_payer_account), + ] + }; + + Ok(Updates { + located_accounts: updated_accounts, + applied_body: signed_command_applied::Body::Payments { new_accounts }, + }) + } } } -} pub fn apply_user_command_unchecked( -constraint_constants: &ConstraintConstants, -_txn_state_view: &ProtocolStateView, -txn_global_slot: &Slot, -ledger: &mut L, -user_command: &SignedCommand, + constraint_constants: &ConstraintConstants, + _txn_state_view: &ProtocolStateView, + txn_global_slot: &Slot, + ledger: &mut L, + user_command: &SignedCommand, ) -> Result where -L: LedgerIntf, + L: LedgerIntf, { -let SignedCommand { - payload: _, - signer: signer_pk, - signature: _, -} = &user_command; -let current_global_slot = txn_global_slot; - -let valid_until = user_command.valid_until(); -validate_time(&valid_until, current_global_slot)?; - -// Fee-payer information -let fee_payer = user_command.fee_payer(); -let (fee_payer_location, fee_payer_account) = - pay_fee(user_command, signer_pk, ledger, current_global_slot)?; - -if !fee_payer_account.has_permission_to_send() { - return Err(TransactionFailure::UpdateNotPermittedBalance.to_string()); -} -if !fee_payer_account.has_permission_to_increment_nonce() { - return Err(TransactionFailure::UpdateNotPermittedNonce.to_string()); -} + let SignedCommand { + payload: _, + signer: signer_pk, + signature: _, + } = &user_command; + let current_global_slot = txn_global_slot; + + let valid_until = user_command.valid_until(); + validate_time(&valid_until, current_global_slot)?; + + // Fee-payer information + let fee_payer = user_command.fee_payer(); + let (fee_payer_location, fee_payer_account) = + pay_fee(user_command, signer_pk, ledger, current_global_slot)?; + + if !fee_payer_account.has_permission_to_send() { + return Err(TransactionFailure::UpdateNotPermittedBalance.to_string()); + } + if !fee_payer_account.has_permission_to_increment_nonce() { + return Err(TransactionFailure::UpdateNotPermittedNonce.to_string()); + } -// Charge the fee. This must happen, whether or not the command itself -// succeeds, to ensure that the network is compensated for processing this -// command. -set_with_location(ledger, &fee_payer_location, fee_payer_account.clone())?; - -let receiver = user_command.receiver(); - -let mut reject_command = false; - -match compute_updates( - constraint_constants, - receiver, - ledger, - current_global_slot, - user_command, - &fee_payer, - &fee_payer_account, - &fee_payer_location, - &mut reject_command, -) { - Ok(Updates { - located_accounts, - applied_body, - }) => { - for (location, account) in located_accounts { - set_with_location(ledger, &location, account)?; - } + // Charge the fee. This must happen, whether or not the command itself + // succeeds, to ensure that the network is compensated for processing this + // command. + set_with_location(ledger, &fee_payer_location, fee_payer_account.clone())?; + + let receiver = user_command.receiver(); + + let mut reject_command = false; + + match compute_updates( + constraint_constants, + receiver, + ledger, + current_global_slot, + user_command, + &fee_payer, + &fee_payer_account, + &fee_payer_location, + &mut reject_command, + ) { + Ok(Updates { + located_accounts, + applied_body, + }) => { + for (location, account) in located_accounts { + set_with_location(ledger, &location, account)?; + } - Ok(SignedCommandApplied { + Ok(SignedCommandApplied { + common: signed_command_applied::Common { + user_command: WithStatus:: { + data: user_command.clone(), + status: TransactionStatus::Applied, + }, + }, + body: applied_body, + }) + } + Err(failure) if !reject_command => Ok(SignedCommandApplied { common: signed_command_applied::Common { user_command: WithStatus:: { data: user_command.clone(), - status: TransactionStatus::Applied, + status: TransactionStatus::Failed(vec![vec![failure]]), }, }, - body: applied_body, - }) - } - Err(failure) if !reject_command => Ok(SignedCommandApplied { - common: signed_command_applied::Common { - user_command: WithStatus:: { - data: user_command.clone(), - status: TransactionStatus::Failed(vec![vec![failure]]), - }, - }, - body: signed_command_applied::Body::Failed, - }), - Err(failure) => { - // This case occurs when an exception is throwned in OCaml - // - assert!(reject_command); - Err(failure.to_string()) + body: signed_command_applied::Body::Failed, + }), + Err(failure) => { + // This case occurs when an exception is throwned in OCaml + // + assert!(reject_command); + Err(failure.to_string()) + } } } -} pub fn apply_user_command( -constraint_constants: &ConstraintConstants, -txn_state_view: &ProtocolStateView, -txn_global_slot: &Slot, -ledger: &mut L, -user_command: &SignedCommand, + constraint_constants: &ConstraintConstants, + txn_state_view: &ProtocolStateView, + txn_global_slot: &Slot, + ledger: &mut L, + user_command: &SignedCommand, ) -> Result where -L: LedgerIntf, + L: LedgerIntf, { -apply_user_command_unchecked( - constraint_constants, - txn_state_view, - txn_global_slot, - ledger, - user_command, -) + apply_user_command_unchecked( + constraint_constants, + txn_state_view, + txn_global_slot, + ledger, + user_command, + ) } pub fn pay_fee( -user_command: &SignedCommand, -signer_pk: &CompressedPubKey, -ledger: &mut L, -current_global_slot: &Slot, + user_command: &SignedCommand, + signer_pk: &CompressedPubKey, + ledger: &mut L, + current_global_slot: &Slot, ) -> Result<(ExistingOrNew, Box), String> where -L: LedgerIntf, + L: LedgerIntf, { -let nonce = user_command.nonce(); -let fee_payer = user_command.fee_payer(); -let fee_token = user_command.fee_token(); + let nonce = user_command.nonce(); + let fee_payer = user_command.fee_payer(); + let fee_token = user_command.fee_token(); -if &fee_payer.public_key != signer_pk { - return Err("Cannot pay fees from a public key that did not sign the transaction".into()); -} + if &fee_payer.public_key != signer_pk { + return Err("Cannot pay fees from a public key that did not sign the transaction".into()); + } -if fee_token != TokenId::default() { - return Err("Cannot create transactions with fee_token different from the default".into()); -} + if fee_token != TokenId::default() { + return Err("Cannot create transactions with fee_token different from the default".into()); + } -pay_fee_impl( - &user_command.payload, - nonce, - fee_payer, - user_command.fee(), - ledger, - current_global_slot, -) + pay_fee_impl( + &user_command.payload, + nonce, + fee_payer, + user_command.fee(), + ledger, + current_global_slot, + ) } fn pay_fee_impl( -command: &SignedCommandPayload, -nonce: Nonce, -fee_payer: AccountId, -fee: Fee, -ledger: &mut L, -current_global_slot: &Slot, + command: &SignedCommandPayload, + nonce: Nonce, + fee_payer: AccountId, + fee: Fee, + ledger: &mut L, + current_global_slot: &Slot, ) -> Result<(ExistingOrNew, Box), String> where -L: LedgerIntf, + L: LedgerIntf, { -// Fee-payer information -let (location, mut account) = get_with_location(ledger, &fee_payer)?; - -if let ExistingOrNew::New = location { - return Err("The fee-payer account does not exist".to_string()); -}; - -let fee = Amount::of_fee(&fee); -let balance = sub_amount(account.balance, fee)?; - -validate_nonces(nonce, account.nonce)?; -let timing = validate_timing(&account, fee, current_global_slot)?; - -account.balance = balance; -account.nonce = account.nonce.incr(); // TODO: Not sure if OCaml wraps -account.receipt_chain_hash = cons_signed_command_payload(command, account.receipt_chain_hash); -account.timing = timing; - -Ok((location, account)) - -// in -// ( location -// , { account with -// balance -// ; nonce = Account.Nonce.succ account.nonce -// ; receipt_chain_hash = -// Receipt.Chain_hash.cons_signed_command_payload command -// account.receipt_chain_hash -// ; timing -// } ) + // Fee-payer information + let (location, mut account) = get_with_location(ledger, &fee_payer)?; + + if let ExistingOrNew::New = location { + return Err("The fee-payer account does not exist".to_string()); + }; + + let fee = Amount::of_fee(&fee); + let balance = sub_amount(account.balance, fee)?; + + validate_nonces(nonce, account.nonce)?; + let timing = validate_timing(&account, fee, current_global_slot)?; + + account.balance = balance; + account.nonce = account.nonce.incr(); // TODO: Not sure if OCaml wraps + account.receipt_chain_hash = cons_signed_command_payload(command, account.receipt_chain_hash); + account.timing = timing; + + Ok((location, account)) + + // in + // ( location + // , { account with + // balance + // ; nonce = Account.Nonce.succ account.nonce + // ; receipt_chain_hash = + // Receipt.Chain_hash.cons_signed_command_payload command + // account.receipt_chain_hash + // ; timing + // } ) } - diff --git a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs index 93948be05..66530771c 100644 --- a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs +++ b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs @@ -1,25 +1,27 @@ -use ark_ff::{PrimeField, Zero}; -use mina_hasher::{Hashable, ROInput as LegacyInput, Fp}; -use mina_signer::{CompressedPubKey, NetworkId, PubKey, Signature}; -use poseidon::hash::{hash_with_kimchi, params::CODA_RECEIPT_UC, Inputs}; - +use super::{ + signed_command::{ + self, PaymentPayload, SignedCommand, SignedCommandPayload, StakeDelegationPayload, + }, + transaction_partially_applied::set_with_location, + Coinbase, CoinbaseFeeTransfer, Memo, SingleFeeTransfer, Transaction, TransactionFailure, + UserCommand, +}; use crate::{ decompress_pk, proofs::{field::Boolean, witness::Witness}, - scan_state::currency::{Amount, Balance, Fee, Index, Magnitude, Nonce, Slot}, + scan_state::{ + currency::{Amount, Balance, Fee, Index, Magnitude, Nonce, Slot}, + scan_state::transaction_snark::OneOrTwo, + }, sparse_ledger::LedgerIntf, zkapps::zkapp_logic::ZkAppCommandElt, Account, AccountId, AppendToInputs, ReceiptChainHash, Timing, TokenId, }; - -use crate::scan_state::scan_state::transaction_snark::OneOrTwo; - -use super::{ - signed_command::{self, PaymentPayload, SignedCommand, SignedCommandPayload, StakeDelegationPayload}, - transaction_partially_applied::set_with_location, - Coinbase, CoinbaseFeeTransfer, Memo, SingleFeeTransfer, Transaction, TransactionFailure, - UserCommand, -}; +use ark_ff::{PrimeField, Zero}; +use mina_curves::pasta::Fp; +use mina_hasher::{Hashable, ROInput as LegacyInput}; +use mina_signer::{CompressedPubKey, NetworkId, PubKey, Signature}; +use poseidon::hash::{hash_with_kimchi, params::CODA_RECEIPT_UC, Inputs}; #[derive(Clone)] pub struct Common { @@ -316,9 +318,9 @@ impl TransactionUnion { let CoinbaseFeeTransfer { receiver_pk: other_pk, fee: other_amount, - } = fee_transfer.clone().unwrap_or_else(|| { - CoinbaseFeeTransfer::create(receiver.clone(), Fee::zero()) - }); + } = fee_transfer + .clone() + .unwrap_or_else(|| CoinbaseFeeTransfer::create(receiver.clone(), Fee::zero())); let signer = decompress_pk(&other_pk).unwrap(); let payload = TransactionUnionPayload { @@ -400,278 +402,277 @@ impl TransactionUnion { /// Returns the new `receipt_chain_hash` pub fn cons_signed_command_payload( -command_payload: &SignedCommandPayload, -last_receipt_chain_hash: ReceiptChainHash, + command_payload: &SignedCommandPayload, + last_receipt_chain_hash: ReceiptChainHash, ) -> ReceiptChainHash { -// Note: Not sure why they use the legacy way of hashing here + // Note: Not sure why they use the legacy way of hashing here -use poseidon::hash::legacy; + use poseidon::hash::legacy; -let ReceiptChainHash(last_receipt_chain_hash) = last_receipt_chain_hash; -let union = TransactionUnionPayload::of_user_command_payload(command_payload); + let ReceiptChainHash(last_receipt_chain_hash) = last_receipt_chain_hash; + let union = TransactionUnionPayload::of_user_command_payload(command_payload); -let mut inputs = union.to_input_legacy(); -inputs.append_field(last_receipt_chain_hash); -let hash = legacy::hash_with_kimchi(&legacy::params::CODA_RECEIPT_UC, &inputs.to_fields()); + let mut inputs = union.to_input_legacy(); + inputs.append_field(last_receipt_chain_hash); + let hash = legacy::hash_with_kimchi(&legacy::params::CODA_RECEIPT_UC, &inputs.to_fields()); -ReceiptChainHash(hash) + ReceiptChainHash(hash) } /// Returns the new `receipt_chain_hash` pub fn checked_cons_signed_command_payload( -payload: &TransactionUnionPayload, -last_receipt_chain_hash: ReceiptChainHash, -w: &mut Witness, + payload: &TransactionUnionPayload, + last_receipt_chain_hash: ReceiptChainHash, + w: &mut Witness, ) -> ReceiptChainHash { -use crate::proofs::transaction::{ - legacy_input::CheckedLegacyInput, transaction_snark::checked_legacy_hash, -}; -use poseidon::hash::legacy; + use crate::proofs::transaction::{ + legacy_input::CheckedLegacyInput, transaction_snark::checked_legacy_hash, + }; + use poseidon::hash::legacy; -let mut inputs = payload.to_checked_legacy_input_owned(w); -inputs.append_field(last_receipt_chain_hash.0); + let mut inputs = payload.to_checked_legacy_input_owned(w); + inputs.append_field(last_receipt_chain_hash.0); -let receipt_chain_hash = checked_legacy_hash(&legacy::params::CODA_RECEIPT_UC, inputs, w); + let receipt_chain_hash = checked_legacy_hash(&legacy::params::CODA_RECEIPT_UC, inputs, w); -ReceiptChainHash(receipt_chain_hash) + ReceiptChainHash(receipt_chain_hash) } /// prepend account_update index computed by Zkapp_command_logic.apply /// /// pub fn cons_zkapp_command_commitment( -index: Index, -e: ZkAppCommandElt, -receipt_hash: &ReceiptChainHash, + index: Index, + e: ZkAppCommandElt, + receipt_hash: &ReceiptChainHash, ) -> ReceiptChainHash { -let ZkAppCommandElt::ZkAppCommandCommitment(x) = e; + let ZkAppCommandElt::ZkAppCommandCommitment(x) = e; -let mut inputs = Inputs::new(); + let mut inputs = Inputs::new(); -inputs.append(&index); -inputs.append_field(x.0); -inputs.append(receipt_hash); + inputs.append(&index); + inputs.append_field(x.0); + inputs.append(receipt_hash); -ReceiptChainHash(hash_with_kimchi(&CODA_RECEIPT_UC, &inputs.to_fields())) + ReceiptChainHash(hash_with_kimchi(&CODA_RECEIPT_UC, &inputs.to_fields())) } pub fn validate_nonces(txn_nonce: Nonce, account_nonce: Nonce) -> Result<(), String> { -if account_nonce == txn_nonce { - return Ok(()); -} + if account_nonce == txn_nonce { + return Ok(()); + } -Err(format!( - "Nonce in account {:?} different from nonce in transaction {:?}", - account_nonce, txn_nonce, -)) + Err(format!( + "Nonce in account {:?} different from nonce in transaction {:?}", + account_nonce, txn_nonce, + )) } pub fn validate_timing( -account: &Account, -txn_amount: Amount, -txn_global_slot: &Slot, + account: &Account, + txn_amount: Amount, + txn_global_slot: &Slot, ) -> Result { -let (timing, _) = validate_timing_with_min_balance(account, txn_amount, txn_global_slot)?; + let (timing, _) = validate_timing_with_min_balance(account, txn_amount, txn_global_slot)?; -Ok(timing) + Ok(timing) } pub fn account_check_timing( -txn_global_slot: &Slot, -account: &Account, + txn_global_slot: &Slot, + account: &Account, ) -> (TimingValidation, Timing) { -let (invalid_timing, timing, _) = - validate_timing_with_min_balance_impl(account, Amount::from_u64(0), txn_global_slot); -// TODO: In OCaml the returned Timing is actually converted to None/Some(fields of Timing structure) -(invalid_timing, timing) + let (invalid_timing, timing, _) = + validate_timing_with_min_balance_impl(account, Amount::from_u64(0), txn_global_slot); + // TODO: In OCaml the returned Timing is actually converted to None/Some(fields of Timing structure) + (invalid_timing, timing) } fn validate_timing_with_min_balance( -account: &Account, -txn_amount: Amount, -txn_global_slot: &Slot, + account: &Account, + txn_amount: Amount, + txn_global_slot: &Slot, ) -> Result<(Timing, MinBalance), String> { -use TimingValidation::*; + use TimingValidation::*; -let (possibly_error, timing, min_balance) = - validate_timing_with_min_balance_impl(account, txn_amount, txn_global_slot); + let (possibly_error, timing, min_balance) = + validate_timing_with_min_balance_impl(account, txn_amount, txn_global_slot); -match possibly_error { - InsufficientBalance(true) => Err(format!( - "For timed account, the requested transaction for amount {:?} \ + match possibly_error { + InsufficientBalance(true) => Err(format!( + "For timed account, the requested transaction for amount {:?} \ at global slot {:?}, the balance {:?} \ is insufficient", - txn_amount, txn_global_slot, account.balance - )), - InvalidTiming(true) => Err(format!( - "For timed account {}, the requested transaction for amount {:?} \ + txn_amount, txn_global_slot, account.balance + )), + InvalidTiming(true) => Err(format!( + "For timed account {}, the requested transaction for amount {:?} \ at global slot {:?}, applying the transaction would put the \ balance below the calculated minimum balance of {:?}", - account.public_key.into_address(), - txn_amount, - txn_global_slot, - min_balance.0 - )), - InsufficientBalance(false) => { - panic!("Broken invariant in validate_timing_with_min_balance'") + account.public_key.into_address(), + txn_amount, + txn_global_slot, + min_balance.0 + )), + InsufficientBalance(false) => { + panic!("Broken invariant in validate_timing_with_min_balance'") + } + InvalidTiming(false) => Ok((timing, min_balance)), } - InvalidTiming(false) => Ok((timing, min_balance)), -} } pub fn timing_error_to_user_command_status( -timing_result: Result, + timing_result: Result, ) -> Result { -match timing_result { - Ok(timing) => Ok(timing), - Err(err_str) => { - /* - HACK: we are matching over the full error string instead - of including an extra tag string to the Err variant - */ - if err_str.contains("minimum balance") { - return Err(TransactionFailure::SourceMinimumBalanceViolation); - } + match timing_result { + Ok(timing) => Ok(timing), + Err(err_str) => { + /* + HACK: we are matching over the full error string instead + of including an extra tag string to the Err variant + */ + if err_str.contains("minimum balance") { + return Err(TransactionFailure::SourceMinimumBalanceViolation); + } - if err_str.contains("is insufficient") { - return Err(TransactionFailure::SourceInsufficientBalance); - } + if err_str.contains("is insufficient") { + return Err(TransactionFailure::SourceInsufficientBalance); + } - panic!("Unexpected timed account validation error") + panic!("Unexpected timed account validation error") + } } } -} pub enum TimingValidation { -InsufficientBalance(B), -InvalidTiming(B), + InsufficientBalance(B), + InvalidTiming(B), } #[derive(Debug)] struct MinBalance(Balance); fn validate_timing_with_min_balance_impl( -account: &Account, -txn_amount: Amount, -txn_global_slot: &Slot, + account: &Account, + txn_amount: Amount, + txn_global_slot: &Slot, ) -> (TimingValidation, Timing, MinBalance) { -use crate::Timing::*; -use TimingValidation::*; - -match &account.timing { - Untimed => { - // no time restrictions - match account.balance.sub_amount(txn_amount) { - None => ( - InsufficientBalance(true), - Untimed, - MinBalance(Balance::zero()), - ), - Some(_) => (InvalidTiming(false), Untimed, MinBalance(Balance::zero())), + use crate::Timing::*; + use TimingValidation::*; + + match &account.timing { + Untimed => { + // no time restrictions + match account.balance.sub_amount(txn_amount) { + None => ( + InsufficientBalance(true), + Untimed, + MinBalance(Balance::zero()), + ), + Some(_) => (InvalidTiming(false), Untimed, MinBalance(Balance::zero())), + } } - } - Timed { - initial_minimum_balance, - .. - } => { - let account_balance = account.balance; - - let (invalid_balance, invalid_timing, curr_min_balance) = - match account_balance.sub_amount(txn_amount) { - None => { - // NB: The [initial_minimum_balance] here is the incorrect value, - // but: - // * we don't use it anywhere in this error case; and - // * we don't want to waste time computing it if it will be unused. - (true, false, *initial_minimum_balance) - } - Some(proposed_new_balance) => { - let curr_min_balance = account.min_balance_at_slot(*txn_global_slot); - - if proposed_new_balance < curr_min_balance { - (false, true, curr_min_balance) - } else { - (false, false, curr_min_balance) + Timed { + initial_minimum_balance, + .. + } => { + let account_balance = account.balance; + + let (invalid_balance, invalid_timing, curr_min_balance) = + match account_balance.sub_amount(txn_amount) { + None => { + // NB: The [initial_minimum_balance] here is the incorrect value, + // but: + // * we don't use it anywhere in this error case; and + // * we don't want to waste time computing it if it will be unused. + (true, false, *initial_minimum_balance) } - } + Some(proposed_new_balance) => { + let curr_min_balance = account.min_balance_at_slot(*txn_global_slot); + + if proposed_new_balance < curr_min_balance { + (false, true, curr_min_balance) + } else { + (false, false, curr_min_balance) + } + } + }; + + // once the calculated minimum balance becomes zero, the account becomes untimed + let possibly_error = if invalid_balance { + InsufficientBalance(invalid_balance) + } else { + InvalidTiming(invalid_timing) }; - // once the calculated minimum balance becomes zero, the account becomes untimed - let possibly_error = if invalid_balance { - InsufficientBalance(invalid_balance) - } else { - InvalidTiming(invalid_timing) - }; - - if curr_min_balance > Balance::zero() { - ( - possibly_error, - account.timing.clone(), - MinBalance(curr_min_balance), - ) - } else { - (possibly_error, Untimed, MinBalance(Balance::zero())) + if curr_min_balance > Balance::zero() { + ( + possibly_error, + account.timing.clone(), + MinBalance(curr_min_balance), + ) + } else { + (possibly_error, Untimed, MinBalance(Balance::zero())) + } } } } -} pub fn sub_amount(balance: Balance, amount: Amount) -> Result { -balance - .sub_amount(amount) - .ok_or_else(|| "insufficient funds".to_string()) + balance + .sub_amount(amount) + .ok_or_else(|| "insufficient funds".to_string()) } pub fn add_amount(balance: Balance, amount: Amount) -> Result { -balance - .add_amount(amount) - .ok_or_else(|| "overflow".to_string()) + balance + .add_amount(amount) + .ok_or_else(|| "overflow".to_string()) } #[derive(Clone, Debug)] pub enum ExistingOrNew { -Existing(Loc), -New, + Existing(Loc), + New, } pub fn get_with_location( -ledger: &mut L, -account_id: &AccountId, + ledger: &mut L, + account_id: &AccountId, ) -> Result<(ExistingOrNew, Box), String> where -L: LedgerIntf, + L: LedgerIntf, { -match ledger.location_of_account(account_id) { - Some(location) => match ledger.get(&location) { - Some(account) => Ok((ExistingOrNew::Existing(location), account)), - None => panic!("Ledger location with no account"), - }, - None => Ok(( - ExistingOrNew::New, - Box::new(Account::create_with(account_id.clone(), Balance::zero())), - )), -} + match ledger.location_of_account(account_id) { + Some(location) => match ledger.get(&location) { + Some(account) => Ok((ExistingOrNew::Existing(location), account)), + None => panic!("Ledger location with no account"), + }, + None => Ok(( + ExistingOrNew::New, + Box::new(Account::create_with(account_id.clone(), Balance::zero())), + )), + } } pub fn get_account( -ledger: &mut L, -account_id: AccountId, + ledger: &mut L, + account_id: AccountId, ) -> (Box, ExistingOrNew) where -L: LedgerIntf, + L: LedgerIntf, { -let (loc, account) = get_with_location(ledger, &account_id).unwrap(); -(account, loc) + let (loc, account) = get_with_location(ledger, &account_id).unwrap(); + (account, loc) } pub fn set_account<'a, L>( -l: &'a mut L, -(a, loc): (Box, &ExistingOrNew), + l: &'a mut L, + (a, loc): (Box, &ExistingOrNew), ) -> &'a mut L where -L: LedgerIntf, + L: LedgerIntf, { -set_with_location(l, loc, a).unwrap(); -l + set_with_location(l, loc, a).unwrap(); + l } - diff --git a/ledger/src/scan_state/transaction_logic/valid.rs b/ledger/src/scan_state/transaction_logic/valid.rs index a1cc84820..a9848b69c 100644 --- a/ledger/src/scan_state/transaction_logic/valid.rs +++ b/ledger/src/scan_state/transaction_logic/valid.rs @@ -1,13 +1,11 @@ -use mina_hasher::Fp; -use mina_p2p_messages::v2::MinaBaseUserCommandStableV2; -use serde::{Deserialize, Serialize}; - +use super::{GenericCommand, GenericTransaction}; use crate::{ scan_state::currency::{Fee, Nonce}, AccountId, }; - -use super::{GenericCommand, GenericTransaction}; +use mina_curves::pasta::Fp; +use mina_p2p_messages::v2::MinaBaseUserCommandStableV2; +use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct VerificationKeyHash(pub Fp); diff --git a/ledger/src/scan_state/transaction_logic/verifiable.rs b/ledger/src/scan_state/transaction_logic/verifiable.rs index e77195c23..5069c09e7 100644 --- a/ledger/src/scan_state/transaction_logic/verifiable.rs +++ b/ledger/src/scan_state/transaction_logic/verifiable.rs @@ -4,8 +4,7 @@ use ark_ff::{BigInteger, PrimeField}; use mina_signer::CompressedPubKey; use super::{ - signed_command, transaction_union_payload::TransactionUnionPayload, valid, - zkapp_command, + signed_command, transaction_union_payload::TransactionUnionPayload, valid, zkapp_command, }; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command.rs b/ledger/src/scan_state/transaction_logic/zkapp_command.rs index f18b6d7ad..66764af1b 100644 --- a/ledger/src/scan_state/transaction_logic/zkapp_command.rs +++ b/ledger/src/scan_state/transaction_logic/zkapp_command.rs @@ -1,20 +1,8 @@ -use std::{collections::HashMap, sync::Arc}; - -use ark_ff::{UniformRand, Zero}; -use itertools::Itertools; -use mina_hasher::Fp; -use mina_p2p_messages::v2::MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA; -use mina_signer::{CompressedPubKey, Signature}; -use poseidon::hash::{ - hash_noinputs, hash_with_kimchi, - params::{ - MINA_ACCOUNT_UPDATE_CONS, MINA_ACCOUNT_UPDATE_NODE, MINA_ZKAPP_EVENT, MINA_ZKAPP_EVENTS, - MINA_ZKAPP_SEQ_EVENTS, NO_INPUT_MINA_ZKAPP_ACTIONS_EMPTY, NO_INPUT_MINA_ZKAPP_EVENTS_EMPTY, - }, - Inputs, +use super::{ + protocol_state::{self, ProtocolStateView}, + zkapp_statement::TransactionCommitment, + Memo, TransactionFailure, TransactionStatus, WithStatus, }; -use rand::{seq::SliceRandom, Rng}; - use crate::{ dummy, gen_compressed, gen_keypair, hash::AppendToInputs, @@ -37,12 +25,21 @@ use crate::{ ToInputs, TokenId, TokenSymbol, VerificationKey, VerificationKeyWire, VotingFor, ZkAppAccount, ZkAppUri, }; - -use super::{ - protocol_state::{self, ProtocolStateView}, - zkapp_statement::TransactionCommitment, - Memo, TransactionFailure, TransactionStatus, WithStatus, +use ark_ff::{UniformRand, Zero}; +use itertools::Itertools; +use mina_curves::pasta::Fp; +use mina_p2p_messages::v2::MinaBaseZkappCommandTStableV1WireStableV1AccountUpdatesA; +use mina_signer::{CompressedPubKey, Signature}; +use poseidon::hash::{ + hash_noinputs, hash_with_kimchi, + params::{ + MINA_ACCOUNT_UPDATE_CONS, MINA_ACCOUNT_UPDATE_NODE, MINA_ZKAPP_EVENT, MINA_ZKAPP_EVENTS, + MINA_ZKAPP_SEQ_EVENTS, NO_INPUT_MINA_ZKAPP_ACTIONS_EMPTY, NO_INPUT_MINA_ZKAPP_EVENTS_EMPTY, + }, + Inputs, }; +use rand::{seq::SliceRandom, Rng}; +use std::{collections::HashMap, sync::Arc}; #[derive(Debug, Clone, PartialEq)] pub struct Event(pub Vec); diff --git a/ledger/src/scan_state/transaction_logic/zkapp_statement.rs b/ledger/src/scan_state/transaction_logic/zkapp_statement.rs index 3343e4e24..81bb8abf1 100644 --- a/ledger/src/scan_state/transaction_logic/zkapp_statement.rs +++ b/ledger/src/scan_state/transaction_logic/zkapp_statement.rs @@ -1,10 +1,10 @@ +use super::zkapp_command::{self, AccountUpdate, CallForest, Tree}; use ark_ff::Zero; -use mina_hasher::{Fp, Hashable, ROInput}; +use mina_curves::pasta::Fp; +use mina_hasher::{Hashable, ROInput}; use mina_signer::NetworkId; use poseidon::hash::{hash_with_kimchi, params::MINA_ACCOUNT_UPDATE_CONS}; -use super::zkapp_command::{self, AccountUpdate, CallForest, Tree}; - #[derive(Copy, Clone, Debug, derive_more::Deref, derive_more::From)] pub struct TransactionCommitment(pub Fp); From 311b34e76ff42ed0c66759fdc67a0eb87f224557 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 22:03:27 +0200 Subject: [PATCH 14/16] Ledger: remove unused imports --- .../src/scan_state/transaction_logic/mod.rs | 31 +++++-------------- .../transaction_union_payload.rs | 2 +- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index 46d155638..b52b9de1a 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1,39 +1,28 @@ use self::{ - local_state::{ - apply_zkapp_command_first_pass, apply_zkapp_command_second_pass, CallStack, LocalStateEnv, - StackFrame, - }, + local_state::{apply_zkapp_command_first_pass, apply_zkapp_command_second_pass, LocalStateEnv}, protocol_state::{GlobalState, ProtocolStateView}, signed_command::{SignedCommand, SignedCommandPayload}, transaction_applied::{ signed_command_applied::{self, SignedCommandApplied}, - TransactionApplied, ZkappCommandApplied, + TransactionApplied, }, - zkapp_command::{AccessedOrNot, AccountUpdate, WithHash, ZkAppCommand}, + zkapp_command::{AccessedOrNot, ZkAppCommand}, }; use super::{ - currency::{Amount, Balance, Fee, Index, Length, Magnitude, Nonce, Signed, Slot}, + currency::{Amount, Balance, Fee, Magnitude, Nonce, Signed, Slot}, fee_excess::FeeExcess, fee_rate::FeeRate, scan_state::transaction_snark::OneOrTwo, }; use crate::{ - proofs::witness::Witness, scan_state::transaction_logic::{ transaction_applied::{CommandApplied, Varying}, - transaction_partially_applied::FullyApplied, zkapp_command::MaybeWithStatus, }, - sparse_ledger::{LedgerIntf, SparseLedger}, - zkapps, - zkapps::{ - non_snark::{LedgerNonSnark, ZkappNonSnark}, - zkapp_logic::ZkAppCommandElt, - }, - Account, AccountId, AccountIdOrderable, AppendToInputs, BaseLedger, ControlTag, - ReceiptChainHash, Timing, TokenId, VerificationKeyWire, + sparse_ledger::LedgerIntf, + zkapps::non_snark::LedgerNonSnark, + Account, AccountId, BaseLedger, ControlTag, Timing, TokenId, VerificationKeyWire, }; -use itertools::FoldWhile; use mina_core::constants::ConstraintConstants; use mina_curves::pasta::Fp; use mina_macros::SerdeYojsonEnum; @@ -43,11 +32,7 @@ use mina_p2p_messages::{ v2::{MinaBaseUserCommandStableV2, MinaTransactionTransactionStableV2}, }; use mina_signer::CompressedPubKey; -use poseidon::hash::{ - hash_with_kimchi, - params::{CODA_RECEIPT_UC, MINA_ZKAPP_MEMO}, - Inputs, -}; +use poseidon::hash::params::MINA_ZKAPP_MEMO; use std::{ collections::{BTreeMap, HashMap, HashSet}, fmt::Display, diff --git a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs index 66530771c..c68c9080e 100644 --- a/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs +++ b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs @@ -17,7 +17,7 @@ use crate::{ zkapps::zkapp_logic::ZkAppCommandElt, Account, AccountId, AppendToInputs, ReceiptChainHash, Timing, TokenId, }; -use ark_ff::{PrimeField, Zero}; +use ark_ff::PrimeField; use mina_curves::pasta::Fp; use mina_hasher::{Hashable, ROInput as LegacyInput}; use mina_signer::{CompressedPubKey, NetworkId, PubKey, Signature}; From 280d1deef3495fc2b5f81b8f808c8a5a2262abee Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 22:13:22 +0200 Subject: [PATCH 15/16] Ledger/transaction_logic: move tests to integration tests Moved the transaction_logic tests module from scan_state/transaction_logic/mod.rs (104 lines) to a new integration test file at ledger/tests/transaction_logic_tests.rs. Changes: - Removed inline tests module from mod.rs - Created transaction_logic_tests.rs in tests directory - Updated imports to work from integration test context - All 3 tests pass: test_hash_empty_event, test_cons_receipt_hash_ocaml, test_receipt_hash_update --- .../src/scan_state/transaction_logic/mod.rs | 153 +++--------------- ledger/tests/transaction_logic_tests.rs | 110 +++++++++++++ 2 files changed, 132 insertions(+), 131 deletions(-) create mode 100644 ledger/tests/transaction_logic_tests.rs diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index b52b9de1a..a4a2c203f 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -38,6 +38,28 @@ use std::{ fmt::Display, }; +pub mod local_state; +pub mod protocol_state; +pub mod signed_command; +pub mod transaction_applied; +pub mod transaction_partially_applied; +pub mod transaction_union_payload; +pub mod transaction_witness; +pub mod valid; +pub mod verifiable; +pub mod zkapp_command; +pub mod zkapp_statement; +pub use transaction_partially_applied::{ + apply_transaction_first_pass, apply_transaction_second_pass, apply_transactions, + apply_user_command, set_with_location, AccountState, +}; +pub use transaction_union_payload::{ + account_check_timing, add_amount, checked_cons_signed_command_payload, + cons_signed_command_payload, cons_zkapp_command_commitment, get_with_location, sub_amount, + timing_error_to_user_command_status, validate_nonces, validate_timing, Body, Common, + ExistingOrNew, Tag, TimingValidation, TransactionUnion, TransactionUnionPayload, +}; + /// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] pub enum TransactionFailure { @@ -237,8 +259,6 @@ where } } -pub mod valid; - /// #[derive(Debug, Clone, PartialEq)] pub struct SingleFeeTransfer { @@ -589,13 +609,6 @@ impl Memo { } } -pub mod signed_command; - -pub mod zkapp_command; -pub mod zkapp_statement; - -pub mod verifiable; - #[derive(Clone, Debug, PartialEq)] pub enum UserCommand { SignedCommand(Box), @@ -1017,24 +1030,6 @@ impl From<&Transaction> for MinaTransactionTransactionStableV2 { } } -pub mod local_state; -pub mod protocol_state; -pub mod transaction_applied; -pub mod transaction_partially_applied; -pub mod transaction_witness; -pub use transaction_partially_applied::{ - apply_transaction_first_pass, apply_transaction_second_pass, apply_transactions, - apply_user_command, set_with_location, AccountState, -}; - -pub mod transaction_union_payload; -pub use transaction_union_payload::{ - account_check_timing, add_amount, checked_cons_signed_command_payload, - cons_signed_command_payload, cons_zkapp_command_commitment, get_with_location, sub_amount, - timing_error_to_user_command_status, validate_nonces, validate_timing, Body, Common, - ExistingOrNew, Tag, TimingValidation, TransactionUnion, TransactionUnionPayload, -}; - #[cfg(any(test, feature = "fuzzing"))] pub mod for_tests { use mina_signer::Keypair; @@ -1319,107 +1314,3 @@ pub mod for_tests { } } -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use o1_utils::FieldHelpers; - - #[cfg(target_family = "wasm")] - use wasm_bindgen_test::wasm_bindgen_test as test; - - use super::{ - signed_command::{Body, Common, PaymentPayload}, - *, - }; - - fn pub_key(address: &str) -> CompressedPubKey { - mina_signer::PubKey::from_address(address) - .unwrap() - .into_compressed() - } - - #[test] - fn test_hash_empty_event() { - // Same value than OCaml - const EXPECTED: &str = - "6963060754718463299978089777716994949151371320681588566338620419071140958308"; - - let event = zkapp_command::Event::empty(); - assert_eq!(event.hash(), Fp::from_str(EXPECTED).unwrap()); - } - - /// Test using same values as here: - /// - #[test] - fn test_cons_receipt_hash_ocaml() { - let from = pub_key("B62qr71UxuyKpkSKYceCPsjw14nuaeLwWKZdMqaBMPber5AAF6nkowS"); - let to = pub_key("B62qnvGVnU7FXdy8GdkxL7yciZ8KattyCdq5J6mzo5NCxjgQPjL7BTH"); - - let common = Common { - fee: Fee::from_u64(9758327274353182341), - fee_payer_pk: from, - nonce: Nonce::from_u32(1609569868), - valid_until: Slot::from_u32(2127252111), - memo: Memo([ - 1, 32, 101, 26, 225, 104, 115, 118, 55, 102, 76, 118, 108, 78, 114, 50, 0, 115, - 110, 108, 53, 75, 109, 112, 50, 110, 88, 97, 76, 66, 76, 81, 235, 79, - ]), - }; - - let body = Body::Payment(PaymentPayload { - receiver_pk: to, - amount: Amount::from_u64(1155659205107036493), - }); - - let tx = SignedCommandPayload { common, body }; - - let prev = "4918218371695029984164006552208340844155171097348169027410983585063546229555"; - let prev_receipt_chain_hash = ReceiptChainHash(Fp::from_str(prev).unwrap()); - - let next = "19078048535981853335308913493724081578728104896524544653528728307378106007337"; - let next_receipt_chain_hash = ReceiptChainHash(Fp::from_str(next).unwrap()); - - let result = cons_signed_command_payload(&tx, prev_receipt_chain_hash); - assert_eq!(result, next_receipt_chain_hash); - } - - #[test] - fn test_receipt_hash_update() { - let from = pub_key("B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja"); - let to = pub_key("B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS"); - - let common = Common { - fee: Fee::from_u64(14500000), - fee_payer_pk: from, - nonce: Nonce::from_u32(15), - valid_until: Slot::from_u32(-1i32 as u32), - memo: Memo([ - 1, 7, 84, 104, 101, 32, 49, 48, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]), - }; - - let body = Body::Payment(PaymentPayload { - receiver_pk: to, - amount: Amount::from_u64(2354000000), - }); - - let tx = SignedCommandPayload { common, body }; - - let mut prev = - hex::decode("09ac04c9965b885acfc9c54141dbecfc63b2394a4532ea2c598d086b894bfb14") - .unwrap(); - prev.reverse(); - let prev_receipt_chain_hash = ReceiptChainHash(Fp::from_bytes(&prev).unwrap()); - - let mut next = - hex::decode("3ecaa73739df77549a2f92f7decf822562d0593373cff1e480bb24b4c87dc8f0") - .unwrap(); - next.reverse(); - let next_receipt_chain_hash = ReceiptChainHash(Fp::from_bytes(&next).unwrap()); - - let result = cons_signed_command_payload(&tx, prev_receipt_chain_hash); - assert_eq!(result, next_receipt_chain_hash); - } -} diff --git a/ledger/tests/transaction_logic_tests.rs b/ledger/tests/transaction_logic_tests.rs new file mode 100644 index 000000000..f8c4d04d5 --- /dev/null +++ b/ledger/tests/transaction_logic_tests.rs @@ -0,0 +1,110 @@ +use std::str::FromStr; + +use o1_utils::FieldHelpers; + +#[cfg(target_family = "wasm")] +use wasm_bindgen_test::wasm_bindgen_test as test; + +use mina_curves::pasta::Fp; +use mina_signer::CompressedPubKey; +use mina_tree::{ + scan_state::{ + currency::{Amount, Fee, Nonce, Slot}, + transaction_logic::{ + cons_signed_command_payload, + signed_command::{Body, Common, PaymentPayload, SignedCommandPayload}, + zkapp_command, Memo, + }, + }, + ReceiptChainHash, +}; + +fn pub_key(address: &str) -> CompressedPubKey { + mina_signer::PubKey::from_address(address) + .unwrap() + .into_compressed() +} + +#[test] +fn test_hash_empty_event() { + // Same value than OCaml + const EXPECTED: &str = + "6963060754718463299978089777716994949151371320681588566338620419071140958308"; + + let event = zkapp_command::Event::empty(); + assert_eq!(event.hash(), Fp::from_str(EXPECTED).unwrap()); +} + +/// Test using same values as here: +/// +#[test] +fn test_cons_receipt_hash_ocaml() { + let from = pub_key("B62qr71UxuyKpkSKYceCPsjw14nuaeLwWKZdMqaBMPber5AAF6nkowS"); + let to = pub_key("B62qnvGVnU7FXdy8GdkxL7yciZ8KattyCdq5J6mzo5NCxjgQPjL7BTH"); + + let common = Common { + fee: Fee::from_u64(9758327274353182341), + fee_payer_pk: from, + nonce: Nonce::from_u32(1609569868), + valid_until: Slot::from_u32(2127252111), + memo: Memo([ + 1, 32, 101, 26, 225, 104, 115, 118, 55, 102, 76, 118, 108, 78, 114, 50, 0, 115, + 110, 108, 53, 75, 109, 112, 50, 110, 88, 97, 76, 66, 76, 81, 235, 79, + ]), + }; + + let body = Body::Payment(PaymentPayload { + receiver_pk: to, + amount: Amount::from_u64(1155659205107036493), + }); + + let tx = SignedCommandPayload { common, body }; + + let prev = "4918218371695029984164006552208340844155171097348169027410983585063546229555"; + let prev_receipt_chain_hash = ReceiptChainHash(Fp::from_str(prev).unwrap()); + + let next = "19078048535981853335308913493724081578728104896524544653528728307378106007337"; + let next_receipt_chain_hash = ReceiptChainHash(Fp::from_str(next).unwrap()); + + let result = cons_signed_command_payload(&tx, prev_receipt_chain_hash); + assert_eq!(result, next_receipt_chain_hash); +} + +#[test] +fn test_receipt_hash_update() { + let from = pub_key("B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja"); + let to = pub_key("B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS"); + + let common = Common { + fee: Fee::from_u64(14500000), + fee_payer_pk: from, + nonce: Nonce::from_u32(15), + valid_until: Slot::from_u32(-1i32 as u32), + memo: Memo([ + 1, 7, 84, 104, 101, 32, 49, 48, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]), + }; + + let body = Body::Payment(PaymentPayload { + receiver_pk: to, + amount: Amount::from_u64(2354000000), + }); + + let tx = SignedCommandPayload { common, body }; + + let mut prev = + hex::decode("09ac04c9965b885acfc9c54141dbecfc63b2394a4532ea2c598d086b894bfb14") + .unwrap(); + prev.reverse(); + let prev_receipt_chain_hash = ReceiptChainHash(Fp::from_bytes(&prev).unwrap()); + + let mut next = + hex::decode("3ecaa73739df77549a2f92f7decf822562d0593373cff1e480bb24b4c87dc8f0") + .unwrap(); + next.reverse(); + let next_receipt_chain_hash = ReceiptChainHash(Fp::from_bytes(&next).unwrap()); + + let result = cons_signed_command_payload(&tx, prev_receipt_chain_hash); + assert_eq!(result, next_receipt_chain_hash); +} From 1bedb9e17dca0856f2ed8b6f2d58d27b13420425 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Tue, 7 Oct 2025 22:22:10 +0200 Subject: [PATCH 16/16] Ledger: move tests to directory tests --- ledger/src/scan_state/transaction_logic/mod.rs | 1 - ...on_logic_tests.rs => test_transaction_logic.rs} | 14 ++++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) rename ledger/tests/{transaction_logic_tests.rs => test_transaction_logic.rs} (92%) diff --git a/ledger/src/scan_state/transaction_logic/mod.rs b/ledger/src/scan_state/transaction_logic/mod.rs index a4a2c203f..7d486d3ca 100644 --- a/ledger/src/scan_state/transaction_logic/mod.rs +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -1313,4 +1313,3 @@ pub mod for_tests { ledger.get_or_create_account(id, account).unwrap(); } } - diff --git a/ledger/tests/transaction_logic_tests.rs b/ledger/tests/test_transaction_logic.rs similarity index 92% rename from ledger/tests/transaction_logic_tests.rs rename to ledger/tests/test_transaction_logic.rs index f8c4d04d5..5b4350d96 100644 --- a/ledger/tests/transaction_logic_tests.rs +++ b/ledger/tests/test_transaction_logic.rs @@ -48,8 +48,8 @@ fn test_cons_receipt_hash_ocaml() { nonce: Nonce::from_u32(1609569868), valid_until: Slot::from_u32(2127252111), memo: Memo([ - 1, 32, 101, 26, 225, 104, 115, 118, 55, 102, 76, 118, 108, 78, 114, 50, 0, 115, - 110, 108, 53, 75, 109, 112, 50, 110, 88, 97, 76, 66, 76, 81, 235, 79, + 1, 32, 101, 26, 225, 104, 115, 118, 55, 102, 76, 118, 108, 78, 114, 50, 0, 115, 110, + 108, 53, 75, 109, 112, 50, 110, 88, 97, 76, 66, 76, 81, 235, 79, ]), }; @@ -81,8 +81,8 @@ fn test_receipt_hash_update() { nonce: Nonce::from_u32(15), valid_until: Slot::from_u32(-1i32 as u32), memo: Memo([ - 1, 7, 84, 104, 101, 32, 49, 48, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 7, 84, 104, 101, 32, 49, 48, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, ]), }; @@ -94,14 +94,12 @@ fn test_receipt_hash_update() { let tx = SignedCommandPayload { common, body }; let mut prev = - hex::decode("09ac04c9965b885acfc9c54141dbecfc63b2394a4532ea2c598d086b894bfb14") - .unwrap(); + hex::decode("09ac04c9965b885acfc9c54141dbecfc63b2394a4532ea2c598d086b894bfb14").unwrap(); prev.reverse(); let prev_receipt_chain_hash = ReceiptChainHash(Fp::from_bytes(&prev).unwrap()); let mut next = - hex::decode("3ecaa73739df77549a2f92f7decf822562d0593373cff1e480bb24b4c87dc8f0") - .unwrap(); + hex::decode("3ecaa73739df77549a2f92f7decf822562d0593373cff1e480bb24b4c87dc8f0").unwrap(); next.reverse(); let next_receipt_chain_hash = ReceiptChainHash(Fp::from_bytes(&next).unwrap());