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 diff --git a/ledger/src/scan_state/transaction_logic.rs b/ledger/src/scan_state/transaction_logic.rs deleted file mode 100644 index c61f635e1..000000000 --- a/ledger/src/scan_state/transaction_logic.rs +++ /dev/null @@ -1,8218 +0,0 @@ -use std::{ - collections::{BTreeMap, HashMap, HashSet}, - fmt::Display, -}; - -use ark_ff::Zero; -use itertools::{FoldWhile, Itertools}; -use mina_core::constants::ConstraintConstants; -use mina_hasher::{Fp, Hashable, ROInput}; -use mina_macros::SerdeYojsonEnum; -use mina_p2p_messages::{ - bigint::InvalidBigInt, - binprot, - v2::{MinaBaseUserCommandStableV2, MinaTransactionTransactionStableV2}, -}; -use mina_signer::{CompressedPubKey, NetworkId}; -use poseidon::hash::{ - hash_noinputs, 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::{CallStack, LocalStateEnv, StackFrame}, - protocol_state::{GlobalState, ProtocolStateView}, - signed_command::{SignedCommand, SignedCommandPayload}, - transaction_applied::{ - signed_command_applied::{self, SignedCommandApplied}, - TransactionApplied, ZkappCommandApplied, - }, - transaction_union_payload::TransactionUnionPayload, - zkapp_command::{AccessedOrNot, AccountUpdate, WithHash, ZkAppCommand}, -}; - -use super::{ - currency::{Amount, Balance, Fee, Index, Length, Magnitude, Nonce, Signed, Slot, SlotSpan}, - fee_excess::FeeExcess, - fee_rate::FeeRate, - scan_state::transaction_snark::OneOrTwo, -}; -use crate::zkapps::zkapp_logic::ZkAppCommandElt; - -/// -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] -pub enum TransactionFailure { - Predicate, - SourceNotPresent, - ReceiverNotPresent, - AmountInsufficientToCreateAccount, - CannotPayCreationFeeInToken, - SourceInsufficientBalance, - SourceMinimumBalanceViolation, - ReceiverAlreadyExists, - TokenOwnerNotCaller, - Overflow, - GlobalExcessOverflow, - LocalExcessOverflow, - LocalSupplyIncreaseOverflow, - GlobalSupplyIncreaseOverflow, - SignedCommandOnZkappAccount, - ZkappAccountNotPresent, - UpdateNotPermittedBalance, - UpdateNotPermittedAccess, - UpdateNotPermittedTiming, - UpdateNotPermittedDelegate, - UpdateNotPermittedAppState, - UpdateNotPermittedVerificationKey, - UpdateNotPermittedActionState, - UpdateNotPermittedZkappUri, - UpdateNotPermittedTokenSymbol, - UpdateNotPermittedPermissions, - UpdateNotPermittedNonce, - UpdateNotPermittedVotingFor, - ZkappCommandReplayCheckFailed, - FeePayerNonceMustIncrease, - FeePayerMustBeSigned, - AccountBalancePreconditionUnsatisfied, - AccountNoncePreconditionUnsatisfied, - AccountReceiptChainHashPreconditionUnsatisfied, - AccountDelegatePreconditionUnsatisfied, - AccountActionStatePreconditionUnsatisfied, - AccountAppStatePreconditionUnsatisfied(u64), - AccountProvedStatePreconditionUnsatisfied, - AccountIsNewPreconditionUnsatisfied, - ProtocolStatePreconditionUnsatisfied, - UnexpectedVerificationKeyHash, - ValidWhilePreconditionUnsatisfied, - IncorrectNonce, - InvalidFeeExcess, - Cancelled, -} - -impl Display for TransactionFailure { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let message = match self { - Self::Predicate => "Predicate", - Self::SourceNotPresent => "Source_not_present", - Self::ReceiverNotPresent => "Receiver_not_present", - Self::AmountInsufficientToCreateAccount => "Amount_insufficient_to_create_account", - Self::CannotPayCreationFeeInToken => "Cannot_pay_creation_fee_in_token", - Self::SourceInsufficientBalance => "Source_insufficient_balance", - Self::SourceMinimumBalanceViolation => "Source_minimum_balance_violation", - Self::ReceiverAlreadyExists => "Receiver_already_exists", - Self::TokenOwnerNotCaller => "Token_owner_not_caller", - Self::Overflow => "Overflow", - Self::GlobalExcessOverflow => "Global_excess_overflow", - Self::LocalExcessOverflow => "Local_excess_overflow", - Self::LocalSupplyIncreaseOverflow => "Local_supply_increase_overflow", - Self::GlobalSupplyIncreaseOverflow => "Global_supply_increase_overflow", - Self::SignedCommandOnZkappAccount => "Signed_command_on_zkapp_account", - Self::ZkappAccountNotPresent => "Zkapp_account_not_present", - Self::UpdateNotPermittedBalance => "Update_not_permitted_balance", - Self::UpdateNotPermittedAccess => "Update_not_permitted_access", - Self::UpdateNotPermittedTiming => "Update_not_permitted_timing", - Self::UpdateNotPermittedDelegate => "update_not_permitted_delegate", - Self::UpdateNotPermittedAppState => "Update_not_permitted_app_state", - Self::UpdateNotPermittedVerificationKey => "Update_not_permitted_verification_key", - Self::UpdateNotPermittedActionState => "Update_not_permitted_action_state", - Self::UpdateNotPermittedZkappUri => "Update_not_permitted_zkapp_uri", - Self::UpdateNotPermittedTokenSymbol => "Update_not_permitted_token_symbol", - Self::UpdateNotPermittedPermissions => "Update_not_permitted_permissions", - Self::UpdateNotPermittedNonce => "Update_not_permitted_nonce", - Self::UpdateNotPermittedVotingFor => "Update_not_permitted_voting_for", - Self::ZkappCommandReplayCheckFailed => "Zkapp_command_replay_check_failed", - Self::FeePayerNonceMustIncrease => "Fee_payer_nonce_must_increase", - Self::FeePayerMustBeSigned => "Fee_payer_must_be_signed", - Self::AccountBalancePreconditionUnsatisfied => { - "Account_balance_precondition_unsatisfied" - } - Self::AccountNoncePreconditionUnsatisfied => "Account_nonce_precondition_unsatisfied", - Self::AccountReceiptChainHashPreconditionUnsatisfied => { - "Account_receipt_chain_hash_precondition_unsatisfied" - } - Self::AccountDelegatePreconditionUnsatisfied => { - "Account_delegate_precondition_unsatisfied" - } - Self::AccountActionStatePreconditionUnsatisfied => { - "Account_action_state_precondition_unsatisfied" - } - Self::AccountAppStatePreconditionUnsatisfied(i) => { - return write!(f, "Account_app_state_{}_precondition_unsatisfied", i); - } - Self::AccountProvedStatePreconditionUnsatisfied => { - "Account_proved_state_precondition_unsatisfied" - } - Self::AccountIsNewPreconditionUnsatisfied => "Account_is_new_precondition_unsatisfied", - Self::ProtocolStatePreconditionUnsatisfied => "Protocol_state_precondition_unsatisfied", - Self::IncorrectNonce => "Incorrect_nonce", - Self::InvalidFeeExcess => "Invalid_fee_excess", - Self::Cancelled => "Cancelled", - Self::UnexpectedVerificationKeyHash => "Unexpected_verification_key_hash", - Self::ValidWhilePreconditionUnsatisfied => "Valid_while_precondition_unsatisfied", - }; - - write!(f, "{}", message) - } -} - -/// -#[derive(SerdeYojsonEnum, Debug, Clone, PartialEq, Eq)] -pub enum TransactionStatus { - Applied, - Failed(Vec>), -} - -impl TransactionStatus { - pub fn is_applied(&self) -> bool { - matches!(self, Self::Applied) - } - pub fn is_failed(&self) -> bool { - matches!(self, Self::Failed(_)) - } -} - -/// -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] -pub struct WithStatus { - pub data: T, - pub status: TransactionStatus, -} - -impl WithStatus { - pub fn applied(data: T) -> Self { - Self { - data, - status: TransactionStatus::Applied, - } - } - - pub fn failed(data: T, failures: Vec>) -> Self { - Self { - data, - status: TransactionStatus::Failed(failures), - } - } - - pub fn map(&self, fun: F) -> WithStatus - where - F: Fn(&T) -> R, - { - WithStatus { - data: fun(&self.data), - status: self.status.clone(), - } - } - - pub fn into_map(self, fun: F) -> WithStatus - where - F: Fn(T) -> R, - { - WithStatus { - data: fun(self.data), - status: self.status, - } - } -} - -pub trait GenericCommand { - fn fee(&self) -> Fee; - fn forget(&self) -> UserCommand; -} - -pub trait GenericTransaction: Sized { - fn is_fee_transfer(&self) -> bool; - fn is_coinbase(&self) -> bool; - fn is_command(&self) -> bool; -} - -impl GenericCommand for WithStatus -where - T: GenericCommand, -{ - fn fee(&self) -> Fee { - self.data.fee() - } - - fn forget(&self) -> UserCommand { - self.data.forget() - } -} - -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()), - } - } - } -} - -/// -#[derive(Debug, Clone, PartialEq)] -pub struct SingleFeeTransfer { - pub receiver_pk: CompressedPubKey, - pub fee: Fee, - pub fee_token: TokenId, -} - -impl SingleFeeTransfer { - pub fn receiver(&self) -> AccountId { - AccountId { - public_key: self.receiver_pk.clone(), - token_id: self.fee_token.clone(), - } - } - - pub fn create(receiver_pk: CompressedPubKey, fee: Fee, fee_token: TokenId) -> Self { - Self { - receiver_pk, - fee, - fee_token, - } - } -} - -/// -#[derive(Debug, Clone, PartialEq)] -pub struct FeeTransfer(pub(super) OneOrTwo); - -impl std::ops::Deref for FeeTransfer { - type Target = OneOrTwo; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl FeeTransfer { - pub fn fee_tokens(&self) -> impl Iterator { - self.0.iter().map(|fee_transfer| &fee_transfer.fee_token) - } - - pub fn receiver_pks(&self) -> impl Iterator { - self.0.iter().map(|fee_transfer| &fee_transfer.receiver_pk) - } - - pub fn receivers(&self) -> impl Iterator + '_ { - self.0.iter().map(|fee_transfer| AccountId { - public_key: fee_transfer.receiver_pk.clone(), - token_id: fee_transfer.fee_token.clone(), - }) - } - - /// - pub fn fee_excess(&self) -> Result { - let one_or_two = self.0.map(|SingleFeeTransfer { fee, fee_token, .. }| { - (fee_token.clone(), Signed::::of_unsigned(*fee).negate()) - }); - FeeExcess::of_one_or_two(one_or_two) - } - - /// - pub fn of_singles(singles: OneOrTwo) -> Result { - match singles { - OneOrTwo::One(a) => Ok(Self(OneOrTwo::One(a))), - OneOrTwo::Two((one, two)) => { - if one.fee_token == two.fee_token { - Ok(Self(OneOrTwo::Two((one, two)))) - } else { - // Necessary invariant for the transaction snark: we should never have - // fee excesses in multiple tokens simultaneously. - Err(format!( - "Cannot combine single fee transfers with incompatible tokens: {:?} <> {:?}", - one, two - )) - } - } - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct CoinbaseFeeTransfer { - pub receiver_pk: CompressedPubKey, - pub fee: Fee, -} - -impl CoinbaseFeeTransfer { - pub fn create(receiver_pk: CompressedPubKey, fee: Fee) -> Self { - Self { receiver_pk, fee } - } - - pub fn receiver(&self) -> AccountId { - AccountId { - public_key: self.receiver_pk.clone(), - token_id: TokenId::default(), - } - } -} - -/// -#[derive(Debug, Clone, PartialEq)] -pub struct Coinbase { - pub receiver: CompressedPubKey, - pub amount: Amount, - pub fee_transfer: Option, -} - -impl Coinbase { - fn is_valid(&self) -> bool { - match &self.fee_transfer { - None => true, - Some(CoinbaseFeeTransfer { fee, .. }) => Amount::of_fee(fee) <= self.amount, - } - } - - pub fn create( - amount: Amount, - receiver: CompressedPubKey, - fee_transfer: Option, - ) -> Result { - let mut this = Self { - receiver: receiver.clone(), - amount, - fee_transfer, - }; - - if this.is_valid() { - let adjusted_fee_transfer = this.fee_transfer.as_ref().and_then(|ft| { - if receiver != ft.receiver_pk { - Some(ft.clone()) - } else { - None - } - }); - this.fee_transfer = adjusted_fee_transfer; - Ok(this) - } else { - Err("Coinbase.create: invalid coinbase".to_string()) - } - } - - /// - fn expected_supply_increase(&self) -> Result { - let Self { - amount, - fee_transfer, - .. - } = self; - - match fee_transfer { - None => Ok(*amount), - Some(CoinbaseFeeTransfer { fee, .. }) => amount - .checked_sub(&Amount::of_fee(fee)) - // The substraction result is ignored here - .map(|_| *amount) - .ok_or_else(|| "Coinbase underflow".to_string()), - } - } - - pub fn fee_excess(&self) -> Result { - self.expected_supply_increase().map(|_| FeeExcess::empty()) - } - - /// - pub fn receiver(&self) -> AccountId { - AccountId::new(self.receiver.clone(), TokenId::default()) - } - - /// - pub fn account_access_statuses( - &self, - status: &TransactionStatus, - ) -> Vec<(AccountId, zkapp_command::AccessedOrNot)> { - let access_status = match status { - TransactionStatus::Applied => zkapp_command::AccessedOrNot::Accessed, - TransactionStatus::Failed(_) => zkapp_command::AccessedOrNot::NotAccessed, - }; - - let mut ids = Vec::with_capacity(2); - - if let Some(fee_transfer) = self.fee_transfer.as_ref() { - ids.push((fee_transfer.receiver(), access_status.clone())); - }; - - ids.push((self.receiver(), access_status)); - - ids - } - - /// - pub fn accounts_referenced(&self) -> Vec { - self.account_access_statuses(&TransactionStatus::Applied) - .into_iter() - .map(|(id, _status)| id) - .collect() - } -} - -/// 0th byte is a tag to distinguish digests from other data -/// 1st byte is length, always 32 for digests -/// bytes 2 to 33 are data, 0-right-padded if length is less than 32 -/// -#[derive(Clone, PartialEq)] -pub struct Memo(pub [u8; 34]); - -impl std::fmt::Debug for Memo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use crate::staged_ledger::hash::OCamlString; - - // Display like OCaml - // Example: "\000 \014WQ\192&\229C\178\232\171.\176`\153\218\161\209\229\223Gw\143w\135\250\171E\205\241/\227\168" - - f.write_fmt(format_args!("\"{}\"", self.0.to_ocaml_str())) - } -} - -impl std::str::FromStr for Memo { - type Err = (); - - fn from_str(s: &str) -> Result { - let length = std::cmp::min(s.len(), Self::DIGEST_LENGTH) as u8; - let mut memo: [u8; Self::MEMO_LENGTH] = std::array::from_fn(|i| (i == 0) as u8); - memo[Self::TAG_INDEX] = Self::BYTES_TAG; - memo[Self::LENGTH_INDEX] = length; - let padded = format!("{s:\0<32}"); - memo[2..].copy_from_slice( - &padded.as_bytes()[..std::cmp::min(padded.len(), Self::DIGEST_LENGTH)], - ); - Ok(Memo(memo)) - } -} - -impl std::fmt::Display for Memo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.0[0] != Self::BYTES_TAG { - return Err(std::fmt::Error); - } - - let length = self.0[1] as usize; - let memo_slice = &self.0[2..2 + length]; - let memo_str = String::from_utf8_lossy(memo_slice).to_string(); - let trimmed = memo_str.trim_end_matches('\0').to_string(); - - write!(f, "{trimmed}") - } -} - -impl Memo { - const TAG_INDEX: usize = 0; - const LENGTH_INDEX: usize = 1; - - const DIGEST_TAG: u8 = 0x00; - const BYTES_TAG: u8 = 0x01; - - const DIGEST_LENGTH: usize = 32; // Blake2.digest_size_in_bytes - const DIGEST_LENGTH_BYTE: u8 = Self::DIGEST_LENGTH as u8; - - /// +2 for tag and length bytes - const MEMO_LENGTH: usize = Self::DIGEST_LENGTH + 2; - - const MAX_INPUT_LENGTH: usize = Self::DIGEST_LENGTH; - - const MAX_DIGESTIBLE_STRING_LENGTH: usize = 1000; - - pub fn to_bits(&self) -> [bool; std::mem::size_of::() * 8] { - use crate::proofs::transaction::legacy_input::BitsIterator; - - const NBYTES: usize = 34; - const NBITS: usize = NBYTES * 8; - assert_eq!(std::mem::size_of::(), NBYTES); - - let mut iter = BitsIterator { - index: 0, - number: self.0, - } - .take(NBITS); - std::array::from_fn(|_| iter.next().unwrap()) - } - - pub fn hash(&self) -> Fp { - use poseidon::hash::{hash_with_kimchi, legacy}; - - // For some reason we are mixing legacy inputs and "new" hashing - let mut inputs = legacy::Inputs::new(); - inputs.append_bytes(&self.0); - hash_with_kimchi(&MINA_ZKAPP_MEMO, &inputs.to_fields()) - } - - pub fn as_slice(&self) -> &[u8] { - self.0.as_slice() - } - - /// - pub fn dummy() -> Self { - // TODO - Self([0; 34]) - } - - pub fn empty() -> Self { - let mut array = [0; 34]; - array[0] = 1; - Self(array) - } - - /// Example: - /// "\000 \014WQ\192&\229C\178\232\171.\176`\153\218\161\209\229\223Gw\143w\135\250\171E\205\241/\227\168" - #[cfg(test)] - pub fn from_ocaml_str(s: &str) -> Self { - use crate::staged_ledger::hash::OCamlString; - - Self(<[u8; 34]>::from_ocaml_str(s)) - } - - pub fn with_number(number: usize) -> Self { - let s = format!("{:034}", number); - assert_eq!(s.len(), 34); - Self(s.into_bytes().try_into().unwrap()) - } - - /// - fn create_by_digesting_string_exn(s: &str) -> Self { - if s.len() > Self::MAX_DIGESTIBLE_STRING_LENGTH { - panic!("Too_long_digestible_string"); - } - - let mut memo = [0; 34]; - memo[Self::TAG_INDEX] = Self::DIGEST_TAG; - memo[Self::LENGTH_INDEX] = Self::DIGEST_LENGTH_BYTE; - - use blake2::{ - digest::{Update, VariableOutput}, - Blake2bVar, - }; - let mut hasher = Blake2bVar::new(32).expect("Invalid Blake2bVar output size"); - hasher.update(s.as_bytes()); - hasher.finalize_variable(&mut memo[2..]).unwrap(); - - Self(memo) - } - - /// - pub fn gen() -> Self { - use rand::distributions::{Alphanumeric, DistString}; - let random_string = Alphanumeric.sample_string(&mut rand::thread_rng(), 50); - - Self::create_by_digesting_string_exn(&random_string) - } -} - -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 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_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 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) - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum UserCommand { - SignedCommand(Box), - ZkAppCommand(Box), -} - -impl From<&UserCommand> for MinaBaseUserCommandStableV2 { - fn from(user_command: &UserCommand) -> Self { - match user_command { - UserCommand::SignedCommand(signed_command) => { - MinaBaseUserCommandStableV2::SignedCommand((&(*(signed_command.clone()))).into()) - } - UserCommand::ZkAppCommand(zkapp_command) => { - MinaBaseUserCommandStableV2::ZkappCommand((&(*(zkapp_command.clone()))).into()) - } - } - } -} - -impl TryFrom<&MinaBaseUserCommandStableV2> for UserCommand { - type Error = InvalidBigInt; - - fn try_from(user_command: &MinaBaseUserCommandStableV2) -> Result { - match user_command { - MinaBaseUserCommandStableV2::SignedCommand(signed_command) => Ok( - UserCommand::SignedCommand(Box::new(signed_command.try_into()?)), - ), - MinaBaseUserCommandStableV2::ZkappCommand(zkapp_command) => Ok( - UserCommand::ZkAppCommand(Box::new(zkapp_command.try_into()?)), - ), - } - } -} - -impl binprot::BinProtWrite for UserCommand { - fn binprot_write(&self, w: &mut W) -> std::io::Result<()> { - let p2p: MinaBaseUserCommandStableV2 = self.into(); - p2p.binprot_write(w) - } -} - -impl binprot::BinProtRead for UserCommand { - fn binprot_read(r: &mut R) -> Result { - let p2p = MinaBaseUserCommandStableV2::binprot_read(r)?; - match UserCommand::try_from(&p2p) { - Ok(cmd) => Ok(cmd), - Err(e) => Err(binprot::Error::CustomError(Box::new(e))), - } - } -} - -impl UserCommand { - /// - pub fn account_access_statuses( - &self, - status: &TransactionStatus, - ) -> Vec<(AccountId, AccessedOrNot)> { - match self { - UserCommand::SignedCommand(cmd) => cmd.account_access_statuses(status).to_vec(), - UserCommand::ZkAppCommand(cmd) => cmd.account_access_statuses(status), - } - } - - /// - pub fn accounts_referenced(&self) -> Vec { - self.account_access_statuses(&TransactionStatus::Applied) - .into_iter() - .map(|(id, _status)| id) - .collect() - } - - pub fn fee_payer(&self) -> AccountId { - match self { - UserCommand::SignedCommand(cmd) => cmd.fee_payer(), - UserCommand::ZkAppCommand(cmd) => cmd.fee_payer(), - } - } - - pub fn valid_until(&self) -> Slot { - match self { - UserCommand::SignedCommand(cmd) => cmd.valid_until(), - UserCommand::ZkAppCommand(cmd) => { - let ZkAppCommand { fee_payer, .. } = &**cmd; - fee_payer.body.valid_until.unwrap_or_else(Slot::max) - } - } - } - - pub fn applicable_at_nonce(&self) -> Nonce { - match self { - UserCommand::SignedCommand(cmd) => cmd.nonce(), - UserCommand::ZkAppCommand(cmd) => cmd.applicable_at_nonce(), - } - } - - pub fn expected_target_nonce(&self) -> Nonce { - self.applicable_at_nonce().succ() - } - - /// - pub fn fee(&self) -> Fee { - match self { - UserCommand::SignedCommand(cmd) => cmd.fee(), - UserCommand::ZkAppCommand(cmd) => cmd.fee(), - } - } - - pub fn weight(&self) -> u64 { - match self { - UserCommand::SignedCommand(cmd) => cmd.weight(), - UserCommand::ZkAppCommand(cmd) => cmd.weight(), - } - } - - /// Fee per weight unit - pub fn fee_per_wu(&self) -> FeeRate { - FeeRate::make_exn(self.fee(), self.weight()) - } - - pub fn fee_token(&self) -> TokenId { - match self { - UserCommand::SignedCommand(cmd) => cmd.fee_token(), - UserCommand::ZkAppCommand(cmd) => cmd.fee_token(), - } - } - - pub fn extract_vks(&self) -> Vec<(AccountId, VerificationKeyWire)> { - match self { - UserCommand::SignedCommand(_) => vec![], - UserCommand::ZkAppCommand(zkapp) => zkapp.extract_vks(), - } - } - - /// - pub fn to_valid_unsafe(self) -> valid::UserCommand { - match self { - UserCommand::SignedCommand(cmd) => valid::UserCommand::SignedCommand(cmd), - UserCommand::ZkAppCommand(cmd) => { - valid::UserCommand::ZkAppCommand(Box::new(zkapp_command::valid::ZkAppCommand { - zkapp_command: *cmd, - })) - } - } - } - - /// - pub fn to_verifiable( - &self, - status: &TransactionStatus, - find_vk: F, - ) -> Result - where - F: Fn(Fp, &AccountId) -> Result, - { - use verifiable::UserCommand::{SignedCommand, ZkAppCommand}; - match self { - UserCommand::SignedCommand(cmd) => Ok(SignedCommand(cmd.clone())), - UserCommand::ZkAppCommand(zkapp) => Ok(ZkAppCommand(Box::new( - zkapp_command::verifiable::create(zkapp, status.is_failed(), find_vk)?, - ))), - } - } - - pub fn load_vks_from_ledger( - account_ids: HashSet, - ledger: &crate::Mask, - ) -> HashMap { - let ids: Vec<_> = account_ids.iter().cloned().collect(); - let locations: Vec<_> = ledger - .location_of_account_batch(&ids) - .into_iter() - .filter_map(|(_, addr)| addr) - .collect(); - ledger - .get_batch(&locations) - .into_iter() - .filter_map(|(_, account)| { - let account = account.unwrap(); - let zkapp = account.zkapp.as_ref()?; - let vk = zkapp.verification_key.clone()?; - Some((account.id(), vk)) - }) - .collect() - } - - pub fn load_vks_from_ledger_accounts( - accounts: &BTreeMap, - ) -> HashMap { - accounts - .iter() - .filter_map(|(_, account)| { - let zkapp = account.zkapp.as_ref()?; - let vk = zkapp.verification_key.clone()?; - Some((account.id(), vk)) - }) - .collect() - } - - pub fn to_all_verifiable( - ts: Vec>, - load_vk_cache: F, - ) -> Result>, String> - where - S: zkapp_command::ToVerifiableStrategy, - F: Fn(HashSet) -> S::Cache, - { - let accounts_referenced: HashSet = ts - .iter() - .flat_map(|cmd| match cmd.cmd() { - UserCommand::SignedCommand(_) => Vec::new(), - UserCommand::ZkAppCommand(cmd) => cmd.accounts_referenced(), - }) - .collect(); - let mut vk_cache = load_vk_cache(accounts_referenced); - - ts.into_iter() - .map(|cmd| { - let is_failed = cmd.is_failed(); - let MaybeWithStatus { cmd, status } = cmd; - match cmd { - UserCommand::SignedCommand(c) => Ok(MaybeWithStatus { - cmd: verifiable::UserCommand::SignedCommand(c), - status, - }), - UserCommand::ZkAppCommand(c) => { - let zkapp_verifiable = S::create_all(&c, is_failed, &mut vk_cache)?; - Ok(MaybeWithStatus { - cmd: verifiable::UserCommand::ZkAppCommand(Box::new(zkapp_verifiable)), - status, - }) - } - } - }) - .collect() - } - - fn has_insufficient_fee(&self) -> bool { - /// `minimum_user_command_fee` - const MINIMUM_USER_COMMAND_FEE: Fee = Fee::from_u64(1000000); - self.fee() < MINIMUM_USER_COMMAND_FEE - } - - fn has_zero_vesting_period(&self) -> bool { - match self { - UserCommand::SignedCommand(_cmd) => false, - UserCommand::ZkAppCommand(cmd) => cmd.has_zero_vesting_period(), - } - } - - fn is_incompatible_version(&self) -> bool { - match self { - UserCommand::SignedCommand(_cmd) => false, - UserCommand::ZkAppCommand(cmd) => cmd.is_incompatible_version(), - } - } - - fn is_disabled(&self) -> bool { - match self { - UserCommand::SignedCommand(_cmd) => false, - UserCommand::ZkAppCommand(_cmd) => false, // Mina_compile_config.zkapps_disabled - } - } - - fn valid_size(&self) -> Result<(), String> { - match self { - UserCommand::SignedCommand(_cmd) => Ok(()), - UserCommand::ZkAppCommand(cmd) => cmd.valid_size(), - } - } - - pub fn check_well_formedness(&self) -> Result<(), Vec> { - let mut errors: Vec<_> = [ - ( - Self::has_insufficient_fee as fn(_) -> _, - WellFormednessError::InsufficientFee, - ), - ( - Self::has_zero_vesting_period, - WellFormednessError::ZeroVestingPeriod, - ), - ( - Self::is_incompatible_version, - WellFormednessError::IncompatibleVersion, - ), - ( - Self::is_disabled, - WellFormednessError::TransactionTypeDisabled, - ), - ] - .iter() - .filter_map(|(fun, e)| if fun(self) { Some(e.clone()) } else { None }) - .collect(); - - if let Err(e) = self.valid_size() { - errors.push(WellFormednessError::ZkappTooBig(e)); - } - - if errors.is_empty() { - Ok(()) - } else { - Err(errors) - } - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq, thiserror::Error)] -pub enum WellFormednessError { - #[error("Insufficient Fee")] - InsufficientFee, - #[error("Zero vesting period")] - ZeroVestingPeriod, - #[error("Zkapp too big: {0}")] - ZkappTooBig(String), - #[error("Transaction type disabled")] - TransactionTypeDisabled, - #[error("Incompatible version")] - IncompatibleVersion, -} - -impl GenericCommand for UserCommand { - fn fee(&self) -> Fee { - match self { - UserCommand::SignedCommand(cmd) => cmd.fee(), - UserCommand::ZkAppCommand(cmd) => cmd.fee(), - } - } - - fn forget(&self) -> UserCommand { - self.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(Clone, Debug, derive_more::From)] -pub enum Transaction { - Command(UserCommand), - FeeTransfer(FeeTransfer), - Coinbase(Coinbase), -} - -impl Transaction { - pub fn is_zkapp(&self) -> bool { - matches!(self, Self::Command(UserCommand::ZkAppCommand(_))) - } - - pub fn fee_excess(&self) -> Result { - use Transaction::*; - use UserCommand::*; - - match self { - Command(SignedCommand(cmd)) => Ok(cmd.fee_excess()), - Command(ZkAppCommand(cmd)) => Ok(cmd.fee_excess()), - FeeTransfer(ft) => ft.fee_excess(), - Coinbase(cb) => cb.fee_excess(), - } - } - - /// - pub fn public_keys(&self) -> Vec { - use Transaction::*; - use UserCommand::*; - - let to_pks = |ids: Vec| ids.into_iter().map(|id| id.public_key).collect(); - - match self { - Command(SignedCommand(cmd)) => to_pks(cmd.accounts_referenced()), - Command(ZkAppCommand(cmd)) => to_pks(cmd.accounts_referenced()), - FeeTransfer(ft) => ft.receiver_pks().cloned().collect(), - Coinbase(cb) => to_pks(cb.accounts_referenced()), - } - } - - /// - pub fn account_access_statuses( - &self, - status: &TransactionStatus, - ) -> Vec<(AccountId, zkapp_command::AccessedOrNot)> { - use Transaction::*; - use UserCommand::*; - - match self { - Command(SignedCommand(cmd)) => cmd.account_access_statuses(status).to_vec(), - Command(ZkAppCommand(cmd)) => cmd.account_access_statuses(status), - FeeTransfer(ft) => ft - .receivers() - .map(|account_id| (account_id, AccessedOrNot::Accessed)) - .collect(), - Coinbase(cb) => cb.account_access_statuses(status), - } - } - - /// - pub fn accounts_referenced(&self) -> Vec { - self.account_access_statuses(&TransactionStatus::Applied) - .into_iter() - .map(|(id, _status)| id) - .collect() - } -} - -impl From<&Transaction> for MinaTransactionTransactionStableV2 { - fn from(value: &Transaction) -> Self { - match value { - Transaction::Command(v) => Self::Command(Box::new(v.into())), - Transaction::FeeTransfer(v) => Self::FeeTransfer(v.into()), - Transaction::Coinbase(v) => Self::Coinbase(v.into()), - } - } -} - -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_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 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 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 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_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 -} - -#[cfg(any(test, feature = "fuzzing"))] -pub mod for_tests { - use mina_signer::Keypair; - use rand::Rng; - - use crate::{ - gen_keypair, scan_state::parallel_scan::ceil_log2, AuthRequired, Mask, Permissions, - VerificationKey, ZkAppAccount, TXN_VERSION_CURRENT, - }; - - use super::*; - - const MIN_INIT_BALANCE: u64 = 8000000000; - const MAX_INIT_BALANCE: u64 = 8000000000000; - const NUM_ACCOUNTS: u64 = 10; - const NUM_TRANSACTIONS: u64 = 10; - const DEPTH: u64 = ceil_log2(NUM_ACCOUNTS + NUM_TRANSACTIONS); - - /// Use this for tests only - /// Hashmaps are not deterministic - #[derive(Debug, PartialEq, Eq)] - pub struct HashableKeypair(pub Keypair); - - impl std::hash::Hash for HashableKeypair { - fn hash(&self, state: &mut H) { - let compressed = self.0.public.into_compressed(); - HashableCompressedPubKey(compressed).hash(state); - } - } - - /// Use this for tests only - /// Hashmaps are not deterministic - #[derive(Clone, Debug, Eq, derive_more::From)] - pub struct HashableCompressedPubKey(pub CompressedPubKey); - - impl PartialEq for HashableCompressedPubKey { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } - } - - impl std::hash::Hash for HashableCompressedPubKey { - fn hash(&self, state: &mut H) { - self.0.x.hash(state); - self.0.is_odd.hash(state); - } - } - - impl PartialOrd for HashableCompressedPubKey { - fn partial_cmp(&self, other: &Self) -> Option { - match self.0.x.partial_cmp(&other.0.x) { - Some(core::cmp::Ordering::Equal) => {} - ord => return ord, - }; - self.0.is_odd.partial_cmp(&other.0.is_odd) - } - } - - /// - #[derive(Debug)] - pub struct InitLedger(pub Vec<(Keypair, u64)>); - - /// - #[derive(Debug)] - pub struct TransactionSpec { - pub fee: Fee, - pub sender: (Keypair, Nonce), - pub receiver: CompressedPubKey, - pub amount: Amount, - } - - /// - #[derive(Debug)] - pub struct TestSpec { - pub init_ledger: InitLedger, - pub specs: Vec, - } - - impl InitLedger { - pub fn init(&self, zkapp: Option, ledger: &mut impl LedgerIntf) { - let zkapp = zkapp.unwrap_or(true); - - self.0.iter().for_each(|(kp, amount)| { - let (_tag, mut account, loc) = ledger - .get_or_create(&AccountId::new( - kp.public.into_compressed(), - TokenId::default(), - )) - .unwrap(); - - use AuthRequired::Either; - let permissions = Permissions { - edit_state: Either, - access: AuthRequired::None, - send: Either, - receive: AuthRequired::None, - set_delegate: Either, - set_permissions: Either, - set_verification_key: crate::SetVerificationKey { - auth: Either, - txn_version: TXN_VERSION_CURRENT, - }, - set_zkapp_uri: Either, - edit_action_state: Either, - set_token_symbol: Either, - increment_nonce: Either, - set_voting_for: Either, - set_timing: Either, - }; - - let zkapp = if zkapp { - let zkapp = ZkAppAccount { - verification_key: Some(VerificationKeyWire::new( - crate::dummy::trivial_verification_key(), - )), - ..Default::default() - }; - - Some(zkapp.into()) - } else { - None - }; - - account.balance = Balance::from_u64(*amount); - account.permissions = permissions; - account.zkapp = zkapp; - - ledger.set(&loc, account); - }); - } - - pub fn gen() -> Self { - let mut rng = rand::thread_rng(); - - let mut tbl = HashSet::with_capacity(256); - - let init = (0..NUM_ACCOUNTS) - .map(|_| { - let kp = loop { - let keypair = gen_keypair(); - let compressed = keypair.public.into_compressed(); - if !tbl.contains(&HashableCompressedPubKey(compressed)) { - break keypair; - } - }; - - let amount = rng.gen_range(MIN_INIT_BALANCE..MAX_INIT_BALANCE); - tbl.insert(HashableCompressedPubKey(kp.public.into_compressed())); - (kp, amount) - }) - .collect(); - - Self(init) - } - } - - impl TransactionSpec { - pub fn gen(init_ledger: &InitLedger, nonces: &mut HashMap) -> Self { - let mut rng = rand::thread_rng(); - - let pk = |(kp, _): (Keypair, u64)| kp.public.into_compressed(); - - let receiver_is_new: bool = rng.gen(); - - let mut gen_index = || rng.gen_range(0..init_ledger.0.len().checked_sub(1).unwrap()); - - let receiver_index = if receiver_is_new { - None - } else { - Some(gen_index()) - }; - - let receiver = match receiver_index { - None => gen_keypair().public.into_compressed(), - Some(i) => pk(init_ledger.0[i].clone()), - }; - - let sender = { - let i = match receiver_index { - None => gen_index(), - Some(j) => loop { - let i = gen_index(); - if i != j { - break i; - } - }, - }; - init_ledger.0[i].0.clone() - }; - - let nonce = nonces - .get(&HashableKeypair(sender.clone())) - .cloned() - .unwrap(); - - let amount = Amount::from_u64(rng.gen_range(1_000_000..100_000_000)); - let fee = Fee::from_u64(rng.gen_range(1_000_000..100_000_000)); - - let old = nonces.get_mut(&HashableKeypair(sender.clone())).unwrap(); - *old = old.incr(); - - Self { - fee, - sender: (sender, nonce), - receiver, - amount, - } - } - } - - impl TestSpec { - fn mk_gen(num_transactions: Option) -> TestSpec { - let num_transactions = num_transactions.unwrap_or(NUM_TRANSACTIONS); - - let init_ledger = InitLedger::gen(); - - let mut map = init_ledger - .0 - .iter() - .map(|(kp, _)| (HashableKeypair(kp.clone()), Nonce::zero())) - .collect(); - - let specs = (0..num_transactions) - .map(|_| TransactionSpec::gen(&init_ledger, &mut map)) - .collect(); - - Self { init_ledger, specs } - } - - pub fn gen() -> Self { - Self::mk_gen(Some(NUM_TRANSACTIONS)) - } - } - - #[derive(Debug)] - pub struct UpdateStatesSpec { - pub fee: Fee, - pub sender: (Keypair, Nonce), - pub fee_payer: Option<(Keypair, Nonce)>, - pub receivers: Vec<(CompressedPubKey, Amount)>, - pub amount: Amount, - pub zkapp_account_keypairs: Vec, - pub memo: Memo, - pub new_zkapp_account: bool, - pub snapp_update: zkapp_command::Update, - // Authorization for the update being performed - pub current_auth: AuthRequired, - pub actions: Vec>, - pub events: Vec>, - pub call_data: Fp, - pub preconditions: Option, - } - - pub fn trivial_zkapp_account( - permissions: Option>, - vk: VerificationKey, - pk: CompressedPubKey, - ) -> Account { - let id = AccountId::new(pk, TokenId::default()); - let mut account = Account::create_with(id, Balance::from_u64(1_000_000_000_000_000)); - account.permissions = permissions.unwrap_or_else(Permissions::user_default); - account.zkapp = Some( - ZkAppAccount { - verification_key: Some(VerificationKeyWire::new(vk)), - ..Default::default() - } - .into(), - ); - account - } - - pub fn create_trivial_zkapp_account( - permissions: Option>, - vk: VerificationKey, - ledger: &mut Mask, - pk: CompressedPubKey, - ) { - let id = AccountId::new(pk.clone(), TokenId::default()); - let account = trivial_zkapp_account(permissions, vk, pk); - assert!(BaseLedger::location_of_account(ledger, &id).is_none()); - ledger.get_or_create_account(id, account).unwrap(); - } -} - -#[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/src/scan_state/transaction_logic/local_state.rs b/ledger/src/scan_state/transaction_logic/local_state.rs new file mode 100644 index 000000000..dacb13774 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/local_state.rs @@ -0,0 +1,960 @@ +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}, + 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 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 { + 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 new file mode 100644 index 000000000..7d486d3ca --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/mod.rs @@ -0,0 +1,1315 @@ +use self::{ + 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, + }, + zkapp_command::{AccessedOrNot, ZkAppCommand}, +}; +use super::{ + currency::{Amount, Balance, Fee, Magnitude, Nonce, Signed, Slot}, + fee_excess::FeeExcess, + fee_rate::FeeRate, + scan_state::transaction_snark::OneOrTwo, +}; +use crate::{ + scan_state::transaction_logic::{ + transaction_applied::{CommandApplied, Varying}, + zkapp_command::MaybeWithStatus, + }, + sparse_ledger::LedgerIntf, + zkapps::non_snark::LedgerNonSnark, + Account, AccountId, BaseLedger, ControlTag, Timing, TokenId, VerificationKeyWire, +}; +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::params::MINA_ZKAPP_MEMO; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + 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 { + Predicate, + SourceNotPresent, + ReceiverNotPresent, + AmountInsufficientToCreateAccount, + CannotPayCreationFeeInToken, + SourceInsufficientBalance, + SourceMinimumBalanceViolation, + ReceiverAlreadyExists, + TokenOwnerNotCaller, + Overflow, + GlobalExcessOverflow, + LocalExcessOverflow, + LocalSupplyIncreaseOverflow, + GlobalSupplyIncreaseOverflow, + SignedCommandOnZkappAccount, + ZkappAccountNotPresent, + UpdateNotPermittedBalance, + UpdateNotPermittedAccess, + UpdateNotPermittedTiming, + UpdateNotPermittedDelegate, + UpdateNotPermittedAppState, + UpdateNotPermittedVerificationKey, + UpdateNotPermittedActionState, + UpdateNotPermittedZkappUri, + UpdateNotPermittedTokenSymbol, + UpdateNotPermittedPermissions, + UpdateNotPermittedNonce, + UpdateNotPermittedVotingFor, + ZkappCommandReplayCheckFailed, + FeePayerNonceMustIncrease, + FeePayerMustBeSigned, + AccountBalancePreconditionUnsatisfied, + AccountNoncePreconditionUnsatisfied, + AccountReceiptChainHashPreconditionUnsatisfied, + AccountDelegatePreconditionUnsatisfied, + AccountActionStatePreconditionUnsatisfied, + AccountAppStatePreconditionUnsatisfied(u64), + AccountProvedStatePreconditionUnsatisfied, + AccountIsNewPreconditionUnsatisfied, + ProtocolStatePreconditionUnsatisfied, + UnexpectedVerificationKeyHash, + ValidWhilePreconditionUnsatisfied, + IncorrectNonce, + InvalidFeeExcess, + Cancelled, +} + +impl Display for TransactionFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = match self { + Self::Predicate => "Predicate", + Self::SourceNotPresent => "Source_not_present", + Self::ReceiverNotPresent => "Receiver_not_present", + Self::AmountInsufficientToCreateAccount => "Amount_insufficient_to_create_account", + Self::CannotPayCreationFeeInToken => "Cannot_pay_creation_fee_in_token", + Self::SourceInsufficientBalance => "Source_insufficient_balance", + Self::SourceMinimumBalanceViolation => "Source_minimum_balance_violation", + Self::ReceiverAlreadyExists => "Receiver_already_exists", + Self::TokenOwnerNotCaller => "Token_owner_not_caller", + Self::Overflow => "Overflow", + Self::GlobalExcessOverflow => "Global_excess_overflow", + Self::LocalExcessOverflow => "Local_excess_overflow", + Self::LocalSupplyIncreaseOverflow => "Local_supply_increase_overflow", + Self::GlobalSupplyIncreaseOverflow => "Global_supply_increase_overflow", + Self::SignedCommandOnZkappAccount => "Signed_command_on_zkapp_account", + Self::ZkappAccountNotPresent => "Zkapp_account_not_present", + Self::UpdateNotPermittedBalance => "Update_not_permitted_balance", + Self::UpdateNotPermittedAccess => "Update_not_permitted_access", + Self::UpdateNotPermittedTiming => "Update_not_permitted_timing", + Self::UpdateNotPermittedDelegate => "update_not_permitted_delegate", + Self::UpdateNotPermittedAppState => "Update_not_permitted_app_state", + Self::UpdateNotPermittedVerificationKey => "Update_not_permitted_verification_key", + Self::UpdateNotPermittedActionState => "Update_not_permitted_action_state", + Self::UpdateNotPermittedZkappUri => "Update_not_permitted_zkapp_uri", + Self::UpdateNotPermittedTokenSymbol => "Update_not_permitted_token_symbol", + Self::UpdateNotPermittedPermissions => "Update_not_permitted_permissions", + Self::UpdateNotPermittedNonce => "Update_not_permitted_nonce", + Self::UpdateNotPermittedVotingFor => "Update_not_permitted_voting_for", + Self::ZkappCommandReplayCheckFailed => "Zkapp_command_replay_check_failed", + Self::FeePayerNonceMustIncrease => "Fee_payer_nonce_must_increase", + Self::FeePayerMustBeSigned => "Fee_payer_must_be_signed", + Self::AccountBalancePreconditionUnsatisfied => { + "Account_balance_precondition_unsatisfied" + } + Self::AccountNoncePreconditionUnsatisfied => "Account_nonce_precondition_unsatisfied", + Self::AccountReceiptChainHashPreconditionUnsatisfied => { + "Account_receipt_chain_hash_precondition_unsatisfied" + } + Self::AccountDelegatePreconditionUnsatisfied => { + "Account_delegate_precondition_unsatisfied" + } + Self::AccountActionStatePreconditionUnsatisfied => { + "Account_action_state_precondition_unsatisfied" + } + Self::AccountAppStatePreconditionUnsatisfied(i) => { + return write!(f, "Account_app_state_{}_precondition_unsatisfied", i); + } + Self::AccountProvedStatePreconditionUnsatisfied => { + "Account_proved_state_precondition_unsatisfied" + } + Self::AccountIsNewPreconditionUnsatisfied => "Account_is_new_precondition_unsatisfied", + Self::ProtocolStatePreconditionUnsatisfied => "Protocol_state_precondition_unsatisfied", + Self::IncorrectNonce => "Incorrect_nonce", + Self::InvalidFeeExcess => "Invalid_fee_excess", + Self::Cancelled => "Cancelled", + Self::UnexpectedVerificationKeyHash => "Unexpected_verification_key_hash", + Self::ValidWhilePreconditionUnsatisfied => "Valid_while_precondition_unsatisfied", + }; + + write!(f, "{}", message) + } +} + +/// +#[derive(SerdeYojsonEnum, Debug, Clone, PartialEq, Eq)] +pub enum TransactionStatus { + Applied, + Failed(Vec>), +} + +impl TransactionStatus { + pub fn is_applied(&self) -> bool { + matches!(self, Self::Applied) + } + pub fn is_failed(&self) -> bool { + matches!(self, Self::Failed(_)) + } +} + +/// +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)] +pub struct WithStatus { + pub data: T, + pub status: TransactionStatus, +} + +impl WithStatus { + pub fn applied(data: T) -> Self { + Self { + data, + status: TransactionStatus::Applied, + } + } + + pub fn failed(data: T, failures: Vec>) -> Self { + Self { + data, + status: TransactionStatus::Failed(failures), + } + } + + pub fn map(&self, fun: F) -> WithStatus + where + F: Fn(&T) -> R, + { + WithStatus { + data: fun(&self.data), + status: self.status.clone(), + } + } + + pub fn into_map(self, fun: F) -> WithStatus + where + F: Fn(T) -> R, + { + WithStatus { + data: fun(self.data), + status: self.status, + } + } +} + +pub trait GenericCommand { + fn fee(&self) -> Fee; + fn forget(&self) -> UserCommand; +} + +pub trait GenericTransaction: Sized { + fn is_fee_transfer(&self) -> bool; + fn is_coinbase(&self) -> bool; + fn is_command(&self) -> bool; +} + +impl GenericCommand for WithStatus +where + T: GenericCommand, +{ + fn fee(&self) -> Fee { + self.data.fee() + } + + fn forget(&self) -> UserCommand { + self.data.forget() + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct SingleFeeTransfer { + pub receiver_pk: CompressedPubKey, + pub fee: Fee, + pub fee_token: TokenId, +} + +impl SingleFeeTransfer { + pub fn receiver(&self) -> AccountId { + AccountId { + public_key: self.receiver_pk.clone(), + token_id: self.fee_token.clone(), + } + } + + pub fn create(receiver_pk: CompressedPubKey, fee: Fee, fee_token: TokenId) -> Self { + Self { + receiver_pk, + fee, + fee_token, + } + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct FeeTransfer(pub(super) OneOrTwo); + +impl std::ops::Deref for FeeTransfer { + type Target = OneOrTwo; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FeeTransfer { + pub fn fee_tokens(&self) -> impl Iterator { + self.0.iter().map(|fee_transfer| &fee_transfer.fee_token) + } + + pub fn receiver_pks(&self) -> impl Iterator { + self.0.iter().map(|fee_transfer| &fee_transfer.receiver_pk) + } + + pub fn receivers(&self) -> impl Iterator + '_ { + self.0.iter().map(|fee_transfer| AccountId { + public_key: fee_transfer.receiver_pk.clone(), + token_id: fee_transfer.fee_token.clone(), + }) + } + + /// + pub fn fee_excess(&self) -> Result { + let one_or_two = self.0.map(|SingleFeeTransfer { fee, fee_token, .. }| { + (fee_token.clone(), Signed::::of_unsigned(*fee).negate()) + }); + FeeExcess::of_one_or_two(one_or_two) + } + + /// + pub fn of_singles(singles: OneOrTwo) -> Result { + match singles { + OneOrTwo::One(a) => Ok(Self(OneOrTwo::One(a))), + OneOrTwo::Two((one, two)) => { + if one.fee_token == two.fee_token { + Ok(Self(OneOrTwo::Two((one, two)))) + } else { + // Necessary invariant for the transaction snark: we should never have + // fee excesses in multiple tokens simultaneously. + Err(format!( + "Cannot combine single fee transfers with incompatible tokens: {:?} <> {:?}", + one, two + )) + } + } + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CoinbaseFeeTransfer { + pub receiver_pk: CompressedPubKey, + pub fee: Fee, +} + +impl CoinbaseFeeTransfer { + pub fn create(receiver_pk: CompressedPubKey, fee: Fee) -> Self { + Self { receiver_pk, fee } + } + + pub fn receiver(&self) -> AccountId { + AccountId { + public_key: self.receiver_pk.clone(), + token_id: TokenId::default(), + } + } +} + +/// +#[derive(Debug, Clone, PartialEq)] +pub struct Coinbase { + pub receiver: CompressedPubKey, + pub amount: Amount, + pub fee_transfer: Option, +} + +impl Coinbase { + fn is_valid(&self) -> bool { + match &self.fee_transfer { + None => true, + Some(CoinbaseFeeTransfer { fee, .. }) => Amount::of_fee(fee) <= self.amount, + } + } + + pub fn create( + amount: Amount, + receiver: CompressedPubKey, + fee_transfer: Option, + ) -> Result { + let mut this = Self { + receiver: receiver.clone(), + amount, + fee_transfer, + }; + + if this.is_valid() { + let adjusted_fee_transfer = this.fee_transfer.as_ref().and_then(|ft| { + if receiver != ft.receiver_pk { + Some(ft.clone()) + } else { + None + } + }); + this.fee_transfer = adjusted_fee_transfer; + Ok(this) + } else { + Err("Coinbase.create: invalid coinbase".to_string()) + } + } + + /// + fn expected_supply_increase(&self) -> Result { + let Self { + amount, + fee_transfer, + .. + } = self; + + match fee_transfer { + None => Ok(*amount), + Some(CoinbaseFeeTransfer { fee, .. }) => amount + .checked_sub(&Amount::of_fee(fee)) + // The substraction result is ignored here + .map(|_| *amount) + .ok_or_else(|| "Coinbase underflow".to_string()), + } + } + + pub fn fee_excess(&self) -> Result { + self.expected_supply_increase().map(|_| FeeExcess::empty()) + } + + /// + pub fn receiver(&self) -> AccountId { + AccountId::new(self.receiver.clone(), TokenId::default()) + } + + /// + pub fn account_access_statuses( + &self, + status: &TransactionStatus, + ) -> Vec<(AccountId, zkapp_command::AccessedOrNot)> { + let access_status = match status { + TransactionStatus::Applied => zkapp_command::AccessedOrNot::Accessed, + TransactionStatus::Failed(_) => zkapp_command::AccessedOrNot::NotAccessed, + }; + + let mut ids = Vec::with_capacity(2); + + if let Some(fee_transfer) = self.fee_transfer.as_ref() { + ids.push((fee_transfer.receiver(), access_status.clone())); + }; + + ids.push((self.receiver(), access_status)); + + ids + } + + /// + pub fn accounts_referenced(&self) -> Vec { + self.account_access_statuses(&TransactionStatus::Applied) + .into_iter() + .map(|(id, _status)| id) + .collect() + } +} + +/// 0th byte is a tag to distinguish digests from other data +/// 1st byte is length, always 32 for digests +/// bytes 2 to 33 are data, 0-right-padded if length is less than 32 +/// +#[derive(Clone, PartialEq)] +pub struct Memo(pub [u8; 34]); + +impl std::fmt::Debug for Memo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::staged_ledger::hash::OCamlString; + + // Display like OCaml + // Example: "\000 \014WQ\192&\229C\178\232\171.\176`\153\218\161\209\229\223Gw\143w\135\250\171E\205\241/\227\168" + + f.write_fmt(format_args!("\"{}\"", self.0.to_ocaml_str())) + } +} + +impl std::str::FromStr for Memo { + type Err = (); + + fn from_str(s: &str) -> Result { + let length = std::cmp::min(s.len(), Self::DIGEST_LENGTH) as u8; + let mut memo: [u8; Self::MEMO_LENGTH] = std::array::from_fn(|i| (i == 0) as u8); + memo[Self::TAG_INDEX] = Self::BYTES_TAG; + memo[Self::LENGTH_INDEX] = length; + let padded = format!("{s:\0<32}"); + memo[2..].copy_from_slice( + &padded.as_bytes()[..std::cmp::min(padded.len(), Self::DIGEST_LENGTH)], + ); + Ok(Memo(memo)) + } +} + +impl std::fmt::Display for Memo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0[0] != Self::BYTES_TAG { + return Err(std::fmt::Error); + } + + let length = self.0[1] as usize; + let memo_slice = &self.0[2..2 + length]; + let memo_str = String::from_utf8_lossy(memo_slice).to_string(); + let trimmed = memo_str.trim_end_matches('\0').to_string(); + + write!(f, "{trimmed}") + } +} + +impl Memo { + const TAG_INDEX: usize = 0; + const LENGTH_INDEX: usize = 1; + + const DIGEST_TAG: u8 = 0x00; + const BYTES_TAG: u8 = 0x01; + + const DIGEST_LENGTH: usize = 32; // Blake2.digest_size_in_bytes + const DIGEST_LENGTH_BYTE: u8 = Self::DIGEST_LENGTH as u8; + + /// +2 for tag and length bytes + const MEMO_LENGTH: usize = Self::DIGEST_LENGTH + 2; + + const MAX_INPUT_LENGTH: usize = Self::DIGEST_LENGTH; + + const MAX_DIGESTIBLE_STRING_LENGTH: usize = 1000; + + pub fn to_bits(&self) -> [bool; std::mem::size_of::() * 8] { + use crate::proofs::transaction::legacy_input::BitsIterator; + + const NBYTES: usize = 34; + const NBITS: usize = NBYTES * 8; + assert_eq!(std::mem::size_of::(), NBYTES); + + let mut iter = BitsIterator { + index: 0, + number: self.0, + } + .take(NBITS); + std::array::from_fn(|_| iter.next().unwrap()) + } + + pub fn hash(&self) -> Fp { + use poseidon::hash::{hash_with_kimchi, legacy}; + + // For some reason we are mixing legacy inputs and "new" hashing + let mut inputs = legacy::Inputs::new(); + inputs.append_bytes(&self.0); + hash_with_kimchi(&MINA_ZKAPP_MEMO, &inputs.to_fields()) + } + + pub fn as_slice(&self) -> &[u8] { + self.0.as_slice() + } + + /// + pub fn dummy() -> Self { + // TODO + Self([0; 34]) + } + + pub fn empty() -> Self { + let mut array = [0; 34]; + array[0] = 1; + Self(array) + } + + /// Example: + /// "\000 \014WQ\192&\229C\178\232\171.\176`\153\218\161\209\229\223Gw\143w\135\250\171E\205\241/\227\168" + #[cfg(test)] + pub fn from_ocaml_str(s: &str) -> Self { + use crate::staged_ledger::hash::OCamlString; + + Self(<[u8; 34]>::from_ocaml_str(s)) + } + + pub fn with_number(number: usize) -> Self { + let s = format!("{:034}", number); + assert_eq!(s.len(), 34); + Self(s.into_bytes().try_into().unwrap()) + } + + /// + fn create_by_digesting_string_exn(s: &str) -> Self { + if s.len() > Self::MAX_DIGESTIBLE_STRING_LENGTH { + panic!("Too_long_digestible_string"); + } + + let mut memo = [0; 34]; + memo[Self::TAG_INDEX] = Self::DIGEST_TAG; + memo[Self::LENGTH_INDEX] = Self::DIGEST_LENGTH_BYTE; + + use blake2::{ + digest::{Update, VariableOutput}, + Blake2bVar, + }; + let mut hasher = Blake2bVar::new(32).expect("Invalid Blake2bVar output size"); + hasher.update(s.as_bytes()); + hasher.finalize_variable(&mut memo[2..]).unwrap(); + + Self(memo) + } + + /// + pub fn gen() -> Self { + use rand::distributions::{Alphanumeric, DistString}; + let random_string = Alphanumeric.sample_string(&mut rand::thread_rng(), 50); + + Self::create_by_digesting_string_exn(&random_string) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum UserCommand { + SignedCommand(Box), + ZkAppCommand(Box), +} + +impl From<&UserCommand> for MinaBaseUserCommandStableV2 { + fn from(user_command: &UserCommand) -> Self { + match user_command { + UserCommand::SignedCommand(signed_command) => { + MinaBaseUserCommandStableV2::SignedCommand((&(*(signed_command.clone()))).into()) + } + UserCommand::ZkAppCommand(zkapp_command) => { + MinaBaseUserCommandStableV2::ZkappCommand((&(*(zkapp_command.clone()))).into()) + } + } + } +} + +impl TryFrom<&MinaBaseUserCommandStableV2> for UserCommand { + type Error = InvalidBigInt; + + fn try_from(user_command: &MinaBaseUserCommandStableV2) -> Result { + match user_command { + MinaBaseUserCommandStableV2::SignedCommand(signed_command) => Ok( + UserCommand::SignedCommand(Box::new(signed_command.try_into()?)), + ), + MinaBaseUserCommandStableV2::ZkappCommand(zkapp_command) => Ok( + UserCommand::ZkAppCommand(Box::new(zkapp_command.try_into()?)), + ), + } + } +} + +impl binprot::BinProtWrite for UserCommand { + fn binprot_write(&self, w: &mut W) -> std::io::Result<()> { + let p2p: MinaBaseUserCommandStableV2 = self.into(); + p2p.binprot_write(w) + } +} + +impl binprot::BinProtRead for UserCommand { + fn binprot_read(r: &mut R) -> Result { + let p2p = MinaBaseUserCommandStableV2::binprot_read(r)?; + match UserCommand::try_from(&p2p) { + Ok(cmd) => Ok(cmd), + Err(e) => Err(binprot::Error::CustomError(Box::new(e))), + } + } +} + +impl UserCommand { + /// + pub fn account_access_statuses( + &self, + status: &TransactionStatus, + ) -> Vec<(AccountId, AccessedOrNot)> { + match self { + UserCommand::SignedCommand(cmd) => cmd.account_access_statuses(status).to_vec(), + UserCommand::ZkAppCommand(cmd) => cmd.account_access_statuses(status), + } + } + + /// + pub fn accounts_referenced(&self) -> Vec { + self.account_access_statuses(&TransactionStatus::Applied) + .into_iter() + .map(|(id, _status)| id) + .collect() + } + + pub fn fee_payer(&self) -> AccountId { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee_payer(), + UserCommand::ZkAppCommand(cmd) => cmd.fee_payer(), + } + } + + pub fn valid_until(&self) -> Slot { + match self { + UserCommand::SignedCommand(cmd) => cmd.valid_until(), + UserCommand::ZkAppCommand(cmd) => { + let ZkAppCommand { fee_payer, .. } = &**cmd; + fee_payer.body.valid_until.unwrap_or_else(Slot::max) + } + } + } + + pub fn applicable_at_nonce(&self) -> Nonce { + match self { + UserCommand::SignedCommand(cmd) => cmd.nonce(), + UserCommand::ZkAppCommand(cmd) => cmd.applicable_at_nonce(), + } + } + + pub fn expected_target_nonce(&self) -> Nonce { + self.applicable_at_nonce().succ() + } + + /// + pub fn fee(&self) -> Fee { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee(), + UserCommand::ZkAppCommand(cmd) => cmd.fee(), + } + } + + pub fn weight(&self) -> u64 { + match self { + UserCommand::SignedCommand(cmd) => cmd.weight(), + UserCommand::ZkAppCommand(cmd) => cmd.weight(), + } + } + + /// Fee per weight unit + pub fn fee_per_wu(&self) -> FeeRate { + FeeRate::make_exn(self.fee(), self.weight()) + } + + pub fn fee_token(&self) -> TokenId { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee_token(), + UserCommand::ZkAppCommand(cmd) => cmd.fee_token(), + } + } + + pub fn extract_vks(&self) -> Vec<(AccountId, VerificationKeyWire)> { + match self { + UserCommand::SignedCommand(_) => vec![], + UserCommand::ZkAppCommand(zkapp) => zkapp.extract_vks(), + } + } + + /// + pub fn to_valid_unsafe(self) -> valid::UserCommand { + match self { + UserCommand::SignedCommand(cmd) => valid::UserCommand::SignedCommand(cmd), + UserCommand::ZkAppCommand(cmd) => { + valid::UserCommand::ZkAppCommand(Box::new(zkapp_command::valid::ZkAppCommand { + zkapp_command: *cmd, + })) + } + } + } + + /// + pub fn to_verifiable( + &self, + status: &TransactionStatus, + find_vk: F, + ) -> Result + where + F: Fn(Fp, &AccountId) -> Result, + { + use verifiable::UserCommand::{SignedCommand, ZkAppCommand}; + match self { + UserCommand::SignedCommand(cmd) => Ok(SignedCommand(cmd.clone())), + UserCommand::ZkAppCommand(zkapp) => Ok(ZkAppCommand(Box::new( + zkapp_command::verifiable::create(zkapp, status.is_failed(), find_vk)?, + ))), + } + } + + pub fn load_vks_from_ledger( + account_ids: HashSet, + ledger: &crate::Mask, + ) -> HashMap { + let ids: Vec<_> = account_ids.iter().cloned().collect(); + let locations: Vec<_> = ledger + .location_of_account_batch(&ids) + .into_iter() + .filter_map(|(_, addr)| addr) + .collect(); + ledger + .get_batch(&locations) + .into_iter() + .filter_map(|(_, account)| { + let account = account.unwrap(); + let zkapp = account.zkapp.as_ref()?; + let vk = zkapp.verification_key.clone()?; + Some((account.id(), vk)) + }) + .collect() + } + + pub fn load_vks_from_ledger_accounts( + accounts: &BTreeMap, + ) -> HashMap { + accounts + .iter() + .filter_map(|(_, account)| { + let zkapp = account.zkapp.as_ref()?; + let vk = zkapp.verification_key.clone()?; + Some((account.id(), vk)) + }) + .collect() + } + + pub fn to_all_verifiable( + ts: Vec>, + load_vk_cache: F, + ) -> Result>, String> + where + S: zkapp_command::ToVerifiableStrategy, + F: Fn(HashSet) -> S::Cache, + { + let accounts_referenced: HashSet = ts + .iter() + .flat_map(|cmd| match cmd.cmd() { + UserCommand::SignedCommand(_) => Vec::new(), + UserCommand::ZkAppCommand(cmd) => cmd.accounts_referenced(), + }) + .collect(); + let mut vk_cache = load_vk_cache(accounts_referenced); + + ts.into_iter() + .map(|cmd| { + let is_failed = cmd.is_failed(); + let MaybeWithStatus { cmd, status } = cmd; + match cmd { + UserCommand::SignedCommand(c) => Ok(MaybeWithStatus { + cmd: verifiable::UserCommand::SignedCommand(c), + status, + }), + UserCommand::ZkAppCommand(c) => { + let zkapp_verifiable = S::create_all(&c, is_failed, &mut vk_cache)?; + Ok(MaybeWithStatus { + cmd: verifiable::UserCommand::ZkAppCommand(Box::new(zkapp_verifiable)), + status, + }) + } + } + }) + .collect() + } + + fn has_insufficient_fee(&self) -> bool { + /// `minimum_user_command_fee` + const MINIMUM_USER_COMMAND_FEE: Fee = Fee::from_u64(1000000); + self.fee() < MINIMUM_USER_COMMAND_FEE + } + + fn has_zero_vesting_period(&self) -> bool { + match self { + UserCommand::SignedCommand(_cmd) => false, + UserCommand::ZkAppCommand(cmd) => cmd.has_zero_vesting_period(), + } + } + + fn is_incompatible_version(&self) -> bool { + match self { + UserCommand::SignedCommand(_cmd) => false, + UserCommand::ZkAppCommand(cmd) => cmd.is_incompatible_version(), + } + } + + fn is_disabled(&self) -> bool { + match self { + UserCommand::SignedCommand(_cmd) => false, + UserCommand::ZkAppCommand(_cmd) => false, // Mina_compile_config.zkapps_disabled + } + } + + fn valid_size(&self) -> Result<(), String> { + match self { + UserCommand::SignedCommand(_cmd) => Ok(()), + UserCommand::ZkAppCommand(cmd) => cmd.valid_size(), + } + } + + pub fn check_well_formedness(&self) -> Result<(), Vec> { + let mut errors: Vec<_> = [ + ( + Self::has_insufficient_fee as fn(_) -> _, + WellFormednessError::InsufficientFee, + ), + ( + Self::has_zero_vesting_period, + WellFormednessError::ZeroVestingPeriod, + ), + ( + Self::is_incompatible_version, + WellFormednessError::IncompatibleVersion, + ), + ( + Self::is_disabled, + WellFormednessError::TransactionTypeDisabled, + ), + ] + .iter() + .filter_map(|(fun, e)| if fun(self) { Some(e.clone()) } else { None }) + .collect(); + + if let Err(e) = self.valid_size() { + errors.push(WellFormednessError::ZkappTooBig(e)); + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq, thiserror::Error)] +pub enum WellFormednessError { + #[error("Insufficient Fee")] + InsufficientFee, + #[error("Zero vesting period")] + ZeroVestingPeriod, + #[error("Zkapp too big: {0}")] + ZkappTooBig(String), + #[error("Transaction type disabled")] + TransactionTypeDisabled, + #[error("Incompatible version")] + IncompatibleVersion, +} + +impl GenericCommand for UserCommand { + fn fee(&self) -> Fee { + match self { + UserCommand::SignedCommand(cmd) => cmd.fee(), + UserCommand::ZkAppCommand(cmd) => cmd.fee(), + } + } + + fn forget(&self) -> UserCommand { + self.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(Clone, Debug, derive_more::From)] +pub enum Transaction { + Command(UserCommand), + FeeTransfer(FeeTransfer), + Coinbase(Coinbase), +} + +impl Transaction { + pub fn is_zkapp(&self) -> bool { + matches!(self, Self::Command(UserCommand::ZkAppCommand(_))) + } + + pub fn fee_excess(&self) -> Result { + use Transaction::*; + use UserCommand::*; + + match self { + Command(SignedCommand(cmd)) => Ok(cmd.fee_excess()), + Command(ZkAppCommand(cmd)) => Ok(cmd.fee_excess()), + FeeTransfer(ft) => ft.fee_excess(), + Coinbase(cb) => cb.fee_excess(), + } + } + + /// + pub fn public_keys(&self) -> Vec { + use Transaction::*; + use UserCommand::*; + + let to_pks = |ids: Vec| ids.into_iter().map(|id| id.public_key).collect(); + + match self { + Command(SignedCommand(cmd)) => to_pks(cmd.accounts_referenced()), + Command(ZkAppCommand(cmd)) => to_pks(cmd.accounts_referenced()), + FeeTransfer(ft) => ft.receiver_pks().cloned().collect(), + Coinbase(cb) => to_pks(cb.accounts_referenced()), + } + } + + /// + pub fn account_access_statuses( + &self, + status: &TransactionStatus, + ) -> Vec<(AccountId, zkapp_command::AccessedOrNot)> { + use Transaction::*; + use UserCommand::*; + + match self { + Command(SignedCommand(cmd)) => cmd.account_access_statuses(status).to_vec(), + Command(ZkAppCommand(cmd)) => cmd.account_access_statuses(status), + FeeTransfer(ft) => ft + .receivers() + .map(|account_id| (account_id, AccessedOrNot::Accessed)) + .collect(), + Coinbase(cb) => cb.account_access_statuses(status), + } + } + + /// + pub fn accounts_referenced(&self) -> Vec { + self.account_access_statuses(&TransactionStatus::Applied) + .into_iter() + .map(|(id, _status)| id) + .collect() + } +} + +impl From<&Transaction> for MinaTransactionTransactionStableV2 { + fn from(value: &Transaction) -> Self { + match value { + Transaction::Command(v) => Self::Command(Box::new(v.into())), + Transaction::FeeTransfer(v) => Self::FeeTransfer(v.into()), + Transaction::Coinbase(v) => Self::Coinbase(v.into()), + } + } +} + +#[cfg(any(test, feature = "fuzzing"))] +pub mod for_tests { + use mina_signer::Keypair; + use rand::Rng; + + use crate::{ + gen_keypair, scan_state::parallel_scan::ceil_log2, AuthRequired, Mask, Permissions, + VerificationKey, ZkAppAccount, TXN_VERSION_CURRENT, + }; + + use super::*; + + const MIN_INIT_BALANCE: u64 = 8000000000; + const MAX_INIT_BALANCE: u64 = 8000000000000; + const NUM_ACCOUNTS: u64 = 10; + const NUM_TRANSACTIONS: u64 = 10; + const DEPTH: u64 = ceil_log2(NUM_ACCOUNTS + NUM_TRANSACTIONS); + + /// Use this for tests only + /// Hashmaps are not deterministic + #[derive(Debug, PartialEq, Eq)] + pub struct HashableKeypair(pub Keypair); + + impl std::hash::Hash for HashableKeypair { + fn hash(&self, state: &mut H) { + let compressed = self.0.public.into_compressed(); + HashableCompressedPubKey(compressed).hash(state); + } + } + + /// Use this for tests only + /// Hashmaps are not deterministic + #[derive(Clone, Debug, Eq, derive_more::From)] + pub struct HashableCompressedPubKey(pub CompressedPubKey); + + impl PartialEq for HashableCompressedPubKey { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } + } + + impl std::hash::Hash for HashableCompressedPubKey { + fn hash(&self, state: &mut H) { + self.0.x.hash(state); + self.0.is_odd.hash(state); + } + } + + impl PartialOrd for HashableCompressedPubKey { + fn partial_cmp(&self, other: &Self) -> Option { + match self.0.x.partial_cmp(&other.0.x) { + Some(core::cmp::Ordering::Equal) => {} + ord => return ord, + }; + self.0.is_odd.partial_cmp(&other.0.is_odd) + } + } + + /// + #[derive(Debug)] + pub struct InitLedger(pub Vec<(Keypair, u64)>); + + /// + #[derive(Debug)] + pub struct TransactionSpec { + pub fee: Fee, + pub sender: (Keypair, Nonce), + pub receiver: CompressedPubKey, + pub amount: Amount, + } + + /// + #[derive(Debug)] + pub struct TestSpec { + pub init_ledger: InitLedger, + pub specs: Vec, + } + + impl InitLedger { + pub fn init(&self, zkapp: Option, ledger: &mut impl LedgerIntf) { + let zkapp = zkapp.unwrap_or(true); + + self.0.iter().for_each(|(kp, amount)| { + let (_tag, mut account, loc) = ledger + .get_or_create(&AccountId::new( + kp.public.into_compressed(), + TokenId::default(), + )) + .unwrap(); + + use AuthRequired::Either; + let permissions = Permissions { + edit_state: Either, + access: AuthRequired::None, + send: Either, + receive: AuthRequired::None, + set_delegate: Either, + set_permissions: Either, + set_verification_key: crate::SetVerificationKey { + auth: Either, + txn_version: TXN_VERSION_CURRENT, + }, + set_zkapp_uri: Either, + edit_action_state: Either, + set_token_symbol: Either, + increment_nonce: Either, + set_voting_for: Either, + set_timing: Either, + }; + + let zkapp = if zkapp { + let zkapp = ZkAppAccount { + verification_key: Some(VerificationKeyWire::new( + crate::dummy::trivial_verification_key(), + )), + ..Default::default() + }; + + Some(zkapp.into()) + } else { + None + }; + + account.balance = Balance::from_u64(*amount); + account.permissions = permissions; + account.zkapp = zkapp; + + ledger.set(&loc, account); + }); + } + + pub fn gen() -> Self { + let mut rng = rand::thread_rng(); + + let mut tbl = HashSet::with_capacity(256); + + let init = (0..NUM_ACCOUNTS) + .map(|_| { + let kp = loop { + let keypair = gen_keypair(); + let compressed = keypair.public.into_compressed(); + if !tbl.contains(&HashableCompressedPubKey(compressed)) { + break keypair; + } + }; + + let amount = rng.gen_range(MIN_INIT_BALANCE..MAX_INIT_BALANCE); + tbl.insert(HashableCompressedPubKey(kp.public.into_compressed())); + (kp, amount) + }) + .collect(); + + Self(init) + } + } + + impl TransactionSpec { + pub fn gen(init_ledger: &InitLedger, nonces: &mut HashMap) -> Self { + let mut rng = rand::thread_rng(); + + let pk = |(kp, _): (Keypair, u64)| kp.public.into_compressed(); + + let receiver_is_new: bool = rng.gen(); + + let mut gen_index = || rng.gen_range(0..init_ledger.0.len().checked_sub(1).unwrap()); + + let receiver_index = if receiver_is_new { + None + } else { + Some(gen_index()) + }; + + let receiver = match receiver_index { + None => gen_keypair().public.into_compressed(), + Some(i) => pk(init_ledger.0[i].clone()), + }; + + let sender = { + let i = match receiver_index { + None => gen_index(), + Some(j) => loop { + let i = gen_index(); + if i != j { + break i; + } + }, + }; + init_ledger.0[i].0.clone() + }; + + let nonce = nonces + .get(&HashableKeypair(sender.clone())) + .cloned() + .unwrap(); + + let amount = Amount::from_u64(rng.gen_range(1_000_000..100_000_000)); + let fee = Fee::from_u64(rng.gen_range(1_000_000..100_000_000)); + + let old = nonces.get_mut(&HashableKeypair(sender.clone())).unwrap(); + *old = old.incr(); + + Self { + fee, + sender: (sender, nonce), + receiver, + amount, + } + } + } + + impl TestSpec { + fn mk_gen(num_transactions: Option) -> TestSpec { + let num_transactions = num_transactions.unwrap_or(NUM_TRANSACTIONS); + + let init_ledger = InitLedger::gen(); + + let mut map = init_ledger + .0 + .iter() + .map(|(kp, _)| (HashableKeypair(kp.clone()), Nonce::zero())) + .collect(); + + let specs = (0..num_transactions) + .map(|_| TransactionSpec::gen(&init_ledger, &mut map)) + .collect(); + + Self { init_ledger, specs } + } + + pub fn gen() -> Self { + Self::mk_gen(Some(NUM_TRANSACTIONS)) + } + } + + #[derive(Debug)] + pub struct UpdateStatesSpec { + pub fee: Fee, + pub sender: (Keypair, Nonce), + pub fee_payer: Option<(Keypair, Nonce)>, + pub receivers: Vec<(CompressedPubKey, Amount)>, + pub amount: Amount, + pub zkapp_account_keypairs: Vec, + pub memo: Memo, + pub new_zkapp_account: bool, + pub snapp_update: zkapp_command::Update, + // Authorization for the update being performed + pub current_auth: AuthRequired, + pub actions: Vec>, + pub events: Vec>, + pub call_data: Fp, + pub preconditions: Option, + } + + pub fn trivial_zkapp_account( + permissions: Option>, + vk: VerificationKey, + pk: CompressedPubKey, + ) -> Account { + let id = AccountId::new(pk, TokenId::default()); + let mut account = Account::create_with(id, Balance::from_u64(1_000_000_000_000_000)); + account.permissions = permissions.unwrap_or_else(Permissions::user_default); + account.zkapp = Some( + ZkAppAccount { + verification_key: Some(VerificationKeyWire::new(vk)), + ..Default::default() + } + .into(), + ); + account + } + + pub fn create_trivial_zkapp_account( + permissions: Option>, + vk: VerificationKey, + ledger: &mut Mask, + pk: CompressedPubKey, + ) { + let id = AccountId::new(pk.clone(), TokenId::default()); + let account = trivial_zkapp_account(permissions, vk, pk); + assert!(BaseLedger::location_of_account(ledger, &id).is_none()); + ledger.get_or_create_account(id, account).unwrap(); + } +} 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..fc7790069 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/protocol_state.rs @@ -0,0 +1,160 @@ +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 { + 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 + } +} 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()) + } +} 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..707599afb --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/transaction_applied.rs @@ -0,0 +1,186 @@ +use mina_core::constants::ConstraintConstants; +use mina_curves::pasta::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()) + } +} 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..6b988d443 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/transaction_partially_applied.rs @@ -0,0 +1,1073 @@ +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 + // } ) +} 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..c68c9080e --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/transaction_union_payload.rs @@ -0,0 +1,678 @@ +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::transaction_snark::OneOrTwo, + }, + sparse_ledger::LedgerIntf, + zkapps::zkapp_logic::ZkAppCommandElt, + Account, AccountId, AppendToInputs, ReceiptChainHash, Timing, TokenId, +}; +use ark_ff::PrimeField; +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 { + 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 +} 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, +} 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..a9848b69c --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/valid.rs @@ -0,0 +1,95 @@ +use super::{GenericCommand, GenericTransaction}; +use crate::{ + scan_state::currency::{Fee, Nonce}, + AccountId, +}; +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); + +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()), + } + } +} 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..5069c09e7 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/verifiable.rs @@ -0,0 +1,51 @@ +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) + } +} 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..66764af1b --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command.rs @@ -0,0 +1,3297 @@ +use super::{ + protocol_state::{self, ProtocolStateView}, + zkapp_statement::TransactionCommitment, + Memo, TransactionFailure, TransactionStatus, WithStatus, +}; +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 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); + +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 + } +} 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..81bb8abf1 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_statement.rs @@ -0,0 +1,94 @@ +use super::zkapp_command::{self, AccountUpdate, CallForest, Tree}; +use ark_ff::Zero; +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}; + +#[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))) + } +} 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, diff --git a/ledger/tests/test_transaction_logic.rs b/ledger/tests/test_transaction_logic.rs new file mode 100644 index 000000000..5b4350d96 --- /dev/null +++ b/ledger/tests/test_transaction_logic.rs @@ -0,0 +1,108 @@ +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); +}