From 994a266bd17451d2decdcb17cb4d42f1f4977c93 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 11:16:42 +0200 Subject: [PATCH 1/4] CHANGELOG: add description for patch 1525 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b6785185..d32da12bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ledger/scan_state/transaction_logic**: move submodule `for_tests` into a new file `zkapp_command/for_tests.rs` ([#1527](https://github.com/o1-labs/mina-rust/pull/1527)). +- **ledger/scan_state/transaction_logic**: update OCaml references in `mod.rs` + ([#1525](https://github.com/o1-labs/mina-rust/pull/1525)) ## v0.17.0 From 6d5e391a2d7b630f618512dc141b479d1d430eec Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 12:14:51 +0200 Subject: [PATCH 2/4] CHANGELOG: add description for patch 1527 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d32da12bf..791f86e0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1527](https://github.com/o1-labs/mina-rust/pull/1527)). - **ledger/scan_state/transaction_logic**: update OCaml references in `mod.rs` ([#1525](https://github.com/o1-labs/mina-rust/pull/1525)) +- **ledger/scan_state/transaction_logic**: move submodule `for_tests` into a + new file `zkapp_command/for_tests.rs` + ([#1527](https://github.com/o1-labs/mina-rust/pull/1527)). ## v0.17.0 From 1e1433a372816b3f0213501bb8a8c27b14c021f2 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 12:05:05 +0200 Subject: [PATCH 3/4] Ledger/scan_state/tx-logic: split zkapp_command into submodules --- .../zkapp_command/from_applied_sequence.rs | 32 ++ .../zkapp_command/from_unapplied_sequence.rs | 32 ++ .../mod.rs} | 294 ++---------------- .../transaction_logic/zkapp_command/valid.rs | 36 +++ .../zkapp_command/verifiable.rs | 157 ++++++++++ .../zkapp_command/zkapp_weight.rs | 18 ++ 6 files changed, 295 insertions(+), 274 deletions(-) create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/from_applied_sequence.rs create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/from_unapplied_sequence.rs rename ledger/src/scan_state/transaction_logic/{zkapp_command.rs => zkapp_command/mod.rs} (91%) create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/valid.rs create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/verifiable.rs create mode 100644 ledger/src/scan_state/transaction_logic/zkapp_command/zkapp_weight.rs diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/from_applied_sequence.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/from_applied_sequence.rs new file mode 100644 index 000000000..3f00b3f58 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/from_applied_sequence.rs @@ -0,0 +1,32 @@ +use mina_curves::pasta::Fp; +use std::collections::HashMap; + +use super::{AccountId, ToVerifiableCache, ToVerifiableStrategy, VerificationKeyWire}; + +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; +} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/from_unapplied_sequence.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/from_unapplied_sequence.rs new file mode 100644 index 000000000..aed8f65ab --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/from_unapplied_sequence.rs @@ -0,0 +1,32 @@ +use mina_curves::pasta::Fp; +use std::collections::HashMap; + +use super::{AccountId, ToVerifiableCache, ToVerifiableStrategy, VerificationKeyWire}; + +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; +} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs similarity index 91% rename from ledger/src/scan_state/transaction_logic/zkapp_command.rs rename to ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs index 66764af1b..66a9b6b78 100644 --- a/ledger/src/scan_state/transaction_logic/zkapp_command.rs +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/mod.rs @@ -19,7 +19,6 @@ use crate::{ 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, @@ -39,7 +38,13 @@ use poseidon::hash::{ Inputs, }; use rand::{seq::SliceRandom, Rng}; -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; + +pub mod from_applied_sequence; +pub mod from_unapplied_sequence; +pub mod valid; +pub mod verifiable; +pub mod zkapp_weight; #[derive(Debug, Clone, PartialEq)] pub struct Event(pub Vec); @@ -48,9 +53,11 @@ 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() @@ -86,23 +93,30 @@ 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())) } @@ -111,15 +125,19 @@ impl MakeEvents for Events { /// 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())) } @@ -2944,196 +2962,6 @@ impl ZkAppCommand { } } -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, @@ -3213,85 +3041,3 @@ pub trait ToVerifiableStrategy { 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_command/valid.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/valid.rs new file mode 100644 index 000000000..85c53993b --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/valid.rs @@ -0,0 +1,36 @@ +use mina_curves::pasta::Fp; + +use super::{ + verifiable::{self, create}, + AccountId, TransactionStatus, VerificationKeyWire, +}; + +#[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) +} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/verifiable.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/verifiable.rs new file mode 100644 index 000000000..7f46db0f1 --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/verifiable.rs @@ -0,0 +1,157 @@ +use mina_curves::pasta::Fp; +use mina_p2p_messages::v2::MinaBaseZkappCommandVerifiableStableV1; +use std::collections::HashMap; + +use super::{ + AccountId, AccountUpdate, AuthorizationKind, CallForest, Control, FeePayer, Memo, SetOrKeep, + VerificationKeyWire, +}; +use crate::sparse_ledger::LedgerIntf; + +#[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(), + }) +} diff --git a/ledger/src/scan_state/transaction_logic/zkapp_command/zkapp_weight.rs b/ledger/src/scan_state/transaction_logic/zkapp_command/zkapp_weight.rs new file mode 100644 index 000000000..050c5620f --- /dev/null +++ b/ledger/src/scan_state/transaction_logic/zkapp_command/zkapp_weight.rs @@ -0,0 +1,18 @@ +/// +use super::{AccountUpdate, CallForest, FeePayer, Memo}; + +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(_: &Memo) -> u64 { + 0 +} From 17f0fc8ba4adc7d335645cf0adf16f5ea22379c9 Mon Sep 17 00:00:00 2001 From: Danny Willems Date: Fri, 10 Oct 2025 12:18:57 +0200 Subject: [PATCH 4/4] CHANGELOG: add description for patch 1528 --- CHANGELOG.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 791f86e0e..316411228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,11 +69,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ledger/scan_state/transaction_logic**: move submodule `for_tests` into a new file `zkapp_command/for_tests.rs` ([#1527](https://github.com/o1-labs/mina-rust/pull/1527)). -- **ledger/scan_state/transaction_logic**: update OCaml references in `mod.rs` - ([#1525](https://github.com/o1-labs/mina-rust/pull/1525)) -- **ledger/scan_state/transaction_logic**: move submodule `for_tests` into a - new file `zkapp_command/for_tests.rs` - ([#1527](https://github.com/o1-labs/mina-rust/pull/1527)). +- **Ledger/scan-state/transaction-logic**: split + `ledger::scan_state::transaction_logic::zkapp_command` into submodules in a + new directory `zkapp_command` + ([#1528](https://github.com/o1-labs/mina-rust/pull/1528/)) ## v0.17.0