mina_tree/scan_state/transaction_logic/
for_tests.rs

1use super::{
2    zkapp_command, Account, AccountId, Amount, Balance, Fee, Memo, Nonce, TokenId,
3    VerificationKeyWire,
4};
5use crate::{
6    gen_keypair,
7    scan_state::{currency::Magnitude, parallel_scan::ceil_log2},
8    sparse_ledger::LedgerIntf,
9    AuthRequired, BaseLedger, Mask, Permissions, VerificationKey, ZkAppAccount,
10    TXN_VERSION_CURRENT,
11};
12use mina_curves::pasta::Fp;
13use mina_signer::{CompressedPubKey, Keypair};
14use rand::Rng;
15use std::collections::{HashMap, HashSet};
16
17const MIN_INIT_BALANCE: u64 = 8000000000;
18const MAX_INIT_BALANCE: u64 = 8000000000000;
19const NUM_ACCOUNTS: u64 = 10;
20const NUM_TRANSACTIONS: u64 = 10;
21const DEPTH: u64 = ceil_log2(NUM_ACCOUNTS + NUM_TRANSACTIONS);
22
23/// Use this for tests only
24/// Hashmaps are not deterministic
25#[derive(Debug, PartialEq, Eq)]
26pub struct HashableKeypair(pub Keypair);
27
28impl std::hash::Hash for HashableKeypair {
29    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
30        let compressed = self.0.public.into_compressed();
31        HashableCompressedPubKey(compressed).hash(state);
32    }
33}
34
35/// Use this for tests only
36/// Hashmaps are not deterministic
37#[derive(Clone, Debug, Eq, derive_more::From)]
38pub struct HashableCompressedPubKey(pub CompressedPubKey);
39
40impl PartialEq for HashableCompressedPubKey {
41    fn eq(&self, other: &Self) -> bool {
42        self.0 == other.0
43    }
44}
45
46impl std::hash::Hash for HashableCompressedPubKey {
47    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
48        self.0.x.hash(state);
49        self.0.is_odd.hash(state);
50    }
51}
52
53impl PartialOrd for HashableCompressedPubKey {
54    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
55        match self.0.x.partial_cmp(&other.0.x) {
56            Some(core::cmp::Ordering::Equal) => {}
57            ord => return ord,
58        };
59        self.0.is_odd.partial_cmp(&other.0.is_odd)
60    }
61}
62
63/// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2285-2285
64/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
65/// Last verified: 2025-10-10
66#[derive(Debug)]
67pub struct InitLedger(pub Vec<(Keypair, u64)>);
68
69/// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2351-2356
70/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
71/// Last verified: 2025-10-10
72#[derive(Debug)]
73pub struct TransactionSpec {
74    pub fee: Fee,
75    pub sender: (Keypair, Nonce),
76    pub receiver: CompressedPubKey,
77    pub amount: Amount,
78}
79
80/// OCaml reference: src/lib/transaction_logic/mina_transaction_logic.ml L:2407
81/// Commit: 5da42ccd72e791f164d4d200cf1ce300262873b3
82/// Last verified: 2025-10-10
83#[derive(Debug)]
84pub struct TestSpec {
85    pub init_ledger: InitLedger,
86    pub specs: Vec<TransactionSpec>,
87}
88
89impl InitLedger {
90    pub fn init(&self, zkapp: Option<bool>, ledger: &mut impl LedgerIntf) {
91        let zkapp = zkapp.unwrap_or(true);
92
93        self.0.iter().for_each(|(kp, amount)| {
94            let (_tag, mut account, loc) = ledger
95                .get_or_create(&AccountId::new(
96                    kp.public.into_compressed(),
97                    TokenId::default(),
98                ))
99                .unwrap();
100
101            use AuthRequired::Either;
102            let permissions = Permissions {
103                edit_state: Either,
104                access: AuthRequired::None,
105                send: Either,
106                receive: AuthRequired::None,
107                set_delegate: Either,
108                set_permissions: Either,
109                set_verification_key: crate::SetVerificationKey {
110                    auth: Either,
111                    txn_version: TXN_VERSION_CURRENT,
112                },
113                set_zkapp_uri: Either,
114                edit_action_state: Either,
115                set_token_symbol: Either,
116                increment_nonce: Either,
117                set_voting_for: Either,
118                set_timing: Either,
119            };
120
121            let zkapp = if zkapp {
122                let zkapp = ZkAppAccount {
123                    verification_key: Some(VerificationKeyWire::new(
124                        crate::dummy::trivial_verification_key(),
125                    )),
126                    ..Default::default()
127                };
128
129                Some(zkapp.into())
130            } else {
131                None
132            };
133
134            account.balance = Balance::from_u64(*amount);
135            account.permissions = permissions;
136            account.zkapp = zkapp;
137
138            ledger.set(&loc, account);
139        });
140    }
141
142    pub fn gen() -> Self {
143        let mut rng = rand::thread_rng();
144
145        let mut tbl = HashSet::with_capacity(256);
146
147        let init = (0..NUM_ACCOUNTS)
148            .map(|_| {
149                let kp = loop {
150                    let keypair = gen_keypair();
151                    let compressed = keypair.public.into_compressed();
152                    if !tbl.contains(&HashableCompressedPubKey(compressed)) {
153                        break keypair;
154                    }
155                };
156
157                let amount = rng.gen_range(MIN_INIT_BALANCE..MAX_INIT_BALANCE);
158                tbl.insert(HashableCompressedPubKey(kp.public.into_compressed()));
159                (kp, amount)
160            })
161            .collect();
162
163        Self(init)
164    }
165}
166
167impl TransactionSpec {
168    pub fn gen(init_ledger: &InitLedger, nonces: &mut HashMap<HashableKeypair, Nonce>) -> Self {
169        let mut rng = rand::thread_rng();
170
171        let pk = |(kp, _): (Keypair, u64)| kp.public.into_compressed();
172
173        let receiver_is_new: bool = rng.gen();
174
175        let mut gen_index = || rng.gen_range(0..init_ledger.0.len().checked_sub(1).unwrap());
176
177        let receiver_index = if receiver_is_new {
178            None
179        } else {
180            Some(gen_index())
181        };
182
183        let receiver = match receiver_index {
184            None => gen_keypair().public.into_compressed(),
185            Some(i) => pk(init_ledger.0[i].clone()),
186        };
187
188        let sender = {
189            let i = match receiver_index {
190                None => gen_index(),
191                Some(j) => loop {
192                    let i = gen_index();
193                    if i != j {
194                        break i;
195                    }
196                },
197            };
198            init_ledger.0[i].0.clone()
199        };
200
201        let nonce = nonces
202            .get(&HashableKeypair(sender.clone()))
203            .cloned()
204            .unwrap();
205
206        let amount = Amount::from_u64(rng.gen_range(1_000_000..100_000_000));
207        let fee = Fee::from_u64(rng.gen_range(1_000_000..100_000_000));
208
209        let old = nonces.get_mut(&HashableKeypair(sender.clone())).unwrap();
210        *old = old.incr();
211
212        Self {
213            fee,
214            sender: (sender, nonce),
215            receiver,
216            amount,
217        }
218    }
219}
220
221impl TestSpec {
222    fn mk_gen(num_transactions: Option<u64>) -> TestSpec {
223        let num_transactions = num_transactions.unwrap_or(NUM_TRANSACTIONS);
224
225        let init_ledger = InitLedger::gen();
226
227        let mut map = init_ledger
228            .0
229            .iter()
230            .map(|(kp, _)| (HashableKeypair(kp.clone()), Nonce::zero()))
231            .collect();
232
233        let specs = (0..num_transactions)
234            .map(|_| TransactionSpec::gen(&init_ledger, &mut map))
235            .collect();
236
237        Self { init_ledger, specs }
238    }
239
240    pub fn gen() -> Self {
241        Self::mk_gen(Some(NUM_TRANSACTIONS))
242    }
243}
244
245#[derive(Debug)]
246pub struct UpdateStatesSpec {
247    pub fee: Fee,
248    pub sender: (Keypair, Nonce),
249    pub fee_payer: Option<(Keypair, Nonce)>,
250    pub receivers: Vec<(CompressedPubKey, Amount)>,
251    pub amount: Amount,
252    pub zkapp_account_keypairs: Vec<Keypair>,
253    pub memo: Memo,
254    pub new_zkapp_account: bool,
255    pub snapp_update: zkapp_command::Update,
256    // Authorization for the update being performed
257    pub current_auth: AuthRequired,
258    pub actions: Vec<Vec<Fp>>,
259    pub events: Vec<Vec<Fp>>,
260    pub call_data: Fp,
261    pub preconditions: Option<zkapp_command::Preconditions>,
262}
263
264pub fn trivial_zkapp_account(
265    permissions: Option<Permissions<AuthRequired>>,
266    vk: VerificationKey,
267    pk: CompressedPubKey,
268) -> Account {
269    let id = AccountId::new(pk, TokenId::default());
270    let mut account = Account::create_with(id, Balance::from_u64(1_000_000_000_000_000));
271    account.permissions = permissions.unwrap_or_else(Permissions::user_default);
272    account.zkapp = Some(
273        ZkAppAccount {
274            verification_key: Some(VerificationKeyWire::new(vk)),
275            ..Default::default()
276        }
277        .into(),
278    );
279    account
280}
281
282pub fn create_trivial_zkapp_account(
283    permissions: Option<Permissions<AuthRequired>>,
284    vk: VerificationKey,
285    ledger: &mut Mask,
286    pk: CompressedPubKey,
287) {
288    let id = AccountId::new(pk.clone(), TokenId::default());
289    let account = trivial_zkapp_account(permissions, vk, pk);
290    assert!(BaseLedger::location_of_account(ledger, &id).is_none());
291    ledger.get_or_create_account(id, account).unwrap();
292}