Skip to content

Commit 9a15e57

Browse files
committed
Ledger: add tests for each type of transaction
1 parent b2f2fb7 commit 9a15e57

File tree

4 files changed

+1616
-0
lines changed

4 files changed

+1616
-0
lines changed
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
//! Tests for apply_transaction_first_pass with coinbase transactions
2+
//!
3+
//! Run with: cargo test --test test_transaction_logic_first_pass_coinbase
4+
//!
5+
//! Tests the first pass of two-phase transaction application for coinbase
6+
//! rewards, covering:
7+
//! - Successful coinbase without fee transfer
8+
//! - Successful coinbase with fee transfer to different account
9+
//! - Coinbase with fee transfer to same account (fee transfer should be
10+
//! removed)
11+
//! - Coinbase creating a new account
12+
13+
use ark_ff::Zero;
14+
use mina_core::constants::ConstraintConstants;
15+
use mina_curves::pasta::Fp;
16+
use mina_tree::{
17+
scan_state::{
18+
currency::{Amount, Balance, Fee, Length, Magnitude, Nonce, Slot},
19+
transaction_logic::{
20+
protocol_state::{EpochData, EpochLedger, ProtocolStateView},
21+
transaction_partially_applied::apply_transaction_first_pass,
22+
Coinbase, CoinbaseFeeTransfer, Transaction,
23+
},
24+
},
25+
Account, AccountId, BaseLedger, Database, Mask,
26+
};
27+
28+
fn dummy_epoch_data() -> EpochData<Fp> {
29+
EpochData {
30+
ledger: EpochLedger {
31+
hash: Fp::zero(),
32+
total_currency: Amount::zero(),
33+
},
34+
seed: Fp::zero(),
35+
start_checkpoint: Fp::zero(),
36+
lock_checkpoint: Fp::zero(),
37+
epoch_length: Length::from_u32(0),
38+
}
39+
}
40+
41+
fn test_constraint_constants() -> ConstraintConstants {
42+
ConstraintConstants {
43+
sub_windows_per_window: 11,
44+
ledger_depth: 15,
45+
work_delay: 2,
46+
block_window_duration_ms: 180_000,
47+
transaction_capacity_log_2: 7,
48+
pending_coinbase_depth: 5,
49+
coinbase_amount: 720_000_000_000,
50+
supercharged_coinbase_factor: 2,
51+
account_creation_fee: 1_000_000_000,
52+
fork: None,
53+
}
54+
}
55+
56+
fn create_test_ledger() -> Mask {
57+
let db = Database::create(15);
58+
let mut ledger = Mask::new_root(db);
59+
let alice = mina_signer::PubKey::from_address(
60+
"B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja",
61+
)
62+
.unwrap()
63+
.into_compressed();
64+
65+
// Create Alice's account with balance
66+
let alice_id = AccountId::new(alice, Default::default());
67+
let alice_account = Account::create_with(alice_id.clone(), Balance::from_u64(1_000_000_000));
68+
ledger
69+
.get_or_create_account(alice_id, alice_account)
70+
.unwrap();
71+
72+
ledger
73+
}
74+
75+
#[test]
76+
fn test_apply_coinbase_without_fee_transfer() {
77+
let mut ledger = create_test_ledger();
78+
79+
let alice_pk = mina_signer::PubKey::from_address(
80+
"B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja",
81+
)
82+
.unwrap()
83+
.into_compressed();
84+
85+
let alice_id = AccountId::new(alice_pk.clone(), Default::default());
86+
87+
// Record initial state
88+
let alice_location = ledger.location_of_account(&alice_id).unwrap();
89+
let alice_before = ledger.get(alice_location).unwrap();
90+
let initial_alice_balance = alice_before.balance;
91+
92+
// Create a coinbase of 720 MINA to Alice with no fee transfer
93+
let coinbase_amount = Amount::from_u64(720_000_000_000);
94+
let coinbase = Coinbase::create(coinbase_amount, alice_pk.clone(), None).unwrap();
95+
96+
let constraint_constants = &test_constraint_constants();
97+
let state_view = ProtocolStateView {
98+
snarked_ledger_hash: Fp::zero(),
99+
blockchain_length: Length::from_u32(0),
100+
min_window_density: Length::from_u32(0),
101+
total_currency: Amount::zero(),
102+
global_slot_since_genesis: Slot::from_u32(0),
103+
staking_epoch_data: dummy_epoch_data(),
104+
next_epoch_data: dummy_epoch_data(),
105+
};
106+
let result = apply_transaction_first_pass(
107+
constraint_constants,
108+
Slot::from_u32(0),
109+
&state_view,
110+
&mut ledger,
111+
&Transaction::Coinbase(coinbase),
112+
);
113+
114+
assert!(result.is_ok());
115+
116+
// Verify ledger state changes
117+
let alice_location = ledger.location_of_account(&alice_id).unwrap();
118+
let alice_after = ledger.get(alice_location).unwrap();
119+
120+
// Verify Alice's balance increased by coinbase amount
121+
let expected_alice_balance = initial_alice_balance.add_amount(coinbase_amount).unwrap();
122+
assert_eq!(
123+
alice_after.balance, expected_alice_balance,
124+
"Alice's balance should increase by coinbase amount"
125+
);
126+
127+
// Verify Alice's nonce unchanged (coinbase doesn't affect nonces)
128+
assert_eq!(
129+
alice_after.nonce, alice_before.nonce,
130+
"Alice's nonce should remain unchanged"
131+
);
132+
}
133+
134+
#[test]
135+
fn test_apply_coinbase_with_fee_transfer() {
136+
let mut ledger = create_test_ledger();
137+
138+
let alice_pk = mina_signer::PubKey::from_address(
139+
"B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja",
140+
)
141+
.unwrap()
142+
.into_compressed();
143+
let bob_pk = mina_signer::PubKey::from_address(
144+
"B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS",
145+
)
146+
.unwrap()
147+
.into_compressed();
148+
149+
let alice_id = AccountId::new(alice_pk.clone(), Default::default());
150+
151+
// Create Bob's account
152+
let bob_id = AccountId::new(bob_pk.clone(), Default::default());
153+
let bob_account = Account::create_with(bob_id.clone(), Balance::from_u64(500_000_000));
154+
ledger
155+
.get_or_create_account(bob_id.clone(), bob_account)
156+
.unwrap();
157+
158+
// Record initial state
159+
let alice_location = ledger.location_of_account(&alice_id).unwrap();
160+
let alice_before = ledger.get(alice_location).unwrap();
161+
let bob_location = ledger.location_of_account(&bob_id).unwrap();
162+
let bob_before = ledger.get(bob_location).unwrap();
163+
let initial_alice_balance = alice_before.balance;
164+
let initial_bob_balance = bob_before.balance;
165+
166+
// Create a coinbase of 720 MINA to Alice with a 10 MINA fee transfer to Bob
167+
let coinbase_amount = Amount::from_u64(720_000_000_000);
168+
let fee_transfer_amount = Fee::from_u64(10_000_000_000);
169+
let fee_transfer = CoinbaseFeeTransfer::create(bob_pk.clone(), fee_transfer_amount);
170+
let coinbase = Coinbase::create(coinbase_amount, alice_pk.clone(), Some(fee_transfer)).unwrap();
171+
172+
let constraint_constants = &test_constraint_constants();
173+
let state_view = ProtocolStateView {
174+
snarked_ledger_hash: Fp::zero(),
175+
blockchain_length: Length::from_u32(0),
176+
min_window_density: Length::from_u32(0),
177+
total_currency: Amount::zero(),
178+
global_slot_since_genesis: Slot::from_u32(0),
179+
staking_epoch_data: dummy_epoch_data(),
180+
next_epoch_data: dummy_epoch_data(),
181+
};
182+
let result = apply_transaction_first_pass(
183+
constraint_constants,
184+
Slot::from_u32(0),
185+
&state_view,
186+
&mut ledger,
187+
&Transaction::Coinbase(coinbase),
188+
);
189+
190+
assert!(result.is_ok());
191+
192+
// Verify ledger state changes
193+
let alice_location = ledger.location_of_account(&alice_id).unwrap();
194+
let alice_after = ledger.get(alice_location).unwrap();
195+
let bob_location = ledger.location_of_account(&bob_id).unwrap();
196+
let bob_after = ledger.get(bob_location).unwrap();
197+
198+
// Verify Alice's balance increased by (coinbase amount - fee transfer amount)
199+
// The fee transfer is deducted from the coinbase reward
200+
let coinbase_after_fee_transfer = coinbase_amount
201+
.checked_sub(&Amount::of_fee(&fee_transfer_amount))
202+
.unwrap();
203+
let expected_alice_balance = initial_alice_balance
204+
.add_amount(coinbase_after_fee_transfer)
205+
.unwrap();
206+
assert_eq!(
207+
alice_after.balance, expected_alice_balance,
208+
"Alice's balance should increase by coinbase minus fee transfer"
209+
);
210+
211+
// Verify Bob's balance increased by fee transfer amount
212+
let expected_bob_balance = initial_bob_balance
213+
.add_amount(Amount::of_fee(&fee_transfer_amount))
214+
.unwrap();
215+
assert_eq!(
216+
bob_after.balance, expected_bob_balance,
217+
"Bob's balance should increase by fee transfer amount"
218+
);
219+
220+
// Verify nonces unchanged
221+
assert_eq!(
222+
alice_after.nonce, alice_before.nonce,
223+
"Alice's nonce should remain unchanged"
224+
);
225+
assert_eq!(
226+
bob_after.nonce, bob_before.nonce,
227+
"Bob's nonce should remain unchanged"
228+
);
229+
}
230+
231+
/// Test coinbase with fee transfer to the same account.
232+
///
233+
/// When the coinbase receiver and fee transfer receiver are the same, the fee
234+
/// transfer should be removed during coinbase creation, and the receiver
235+
/// should only receive the coinbase amount (not coinbase + fee transfer).
236+
///
237+
/// Ledger state: Receiver gets only coinbase amount.
238+
#[test]
239+
fn test_apply_coinbase_with_fee_transfer_to_same_account() {
240+
let mut ledger = create_test_ledger();
241+
242+
let alice_pk = mina_signer::PubKey::from_address(
243+
"B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja",
244+
)
245+
.unwrap()
246+
.into_compressed();
247+
248+
let alice_id = AccountId::new(alice_pk.clone(), Default::default());
249+
250+
// Record initial state
251+
let alice_location = ledger.location_of_account(&alice_id).unwrap();
252+
let alice_before = ledger.get(alice_location).unwrap();
253+
let initial_alice_balance = alice_before.balance;
254+
255+
// Create a coinbase of 720 MINA to Alice with a 10 MINA fee transfer also
256+
// to Alice. The fee transfer should be removed.
257+
let coinbase_amount = Amount::from_u64(720_000_000_000);
258+
let fee_transfer_amount = Fee::from_u64(10_000_000_000);
259+
let fee_transfer = CoinbaseFeeTransfer::create(alice_pk.clone(), fee_transfer_amount);
260+
let coinbase = Coinbase::create(coinbase_amount, alice_pk.clone(), Some(fee_transfer)).unwrap();
261+
262+
// Verify that the fee transfer was removed during creation
263+
assert!(
264+
coinbase.fee_transfer.is_none(),
265+
"Fee transfer should be None when receiver equals fee transfer receiver"
266+
);
267+
268+
let constraint_constants = &test_constraint_constants();
269+
let state_view = ProtocolStateView {
270+
snarked_ledger_hash: Fp::zero(),
271+
blockchain_length: Length::from_u32(0),
272+
min_window_density: Length::from_u32(0),
273+
total_currency: Amount::zero(),
274+
global_slot_since_genesis: Slot::from_u32(0),
275+
staking_epoch_data: dummy_epoch_data(),
276+
next_epoch_data: dummy_epoch_data(),
277+
};
278+
let result = apply_transaction_first_pass(
279+
constraint_constants,
280+
Slot::from_u32(0),
281+
&state_view,
282+
&mut ledger,
283+
&Transaction::Coinbase(coinbase),
284+
);
285+
286+
assert!(result.is_ok());
287+
288+
// Verify ledger state changes
289+
let alice_location = ledger.location_of_account(&alice_id).unwrap();
290+
let alice_after = ledger.get(alice_location).unwrap();
291+
292+
// Verify Alice's balance increased by ONLY coinbase amount (not coinbase +
293+
// fee transfer)
294+
let expected_alice_balance = initial_alice_balance.add_amount(coinbase_amount).unwrap();
295+
assert_eq!(
296+
alice_after.balance, expected_alice_balance,
297+
"Alice's balance should increase by only coinbase amount"
298+
);
299+
300+
// Verify Alice's nonce unchanged
301+
assert_eq!(
302+
alice_after.nonce, alice_before.nonce,
303+
"Alice's nonce should remain unchanged"
304+
);
305+
}
306+
307+
/// Test coinbase to a nonexistent account.
308+
///
309+
/// The receiver account does not exist, so the coinbase should create it with
310+
/// the coinbase amount as balance.
311+
///
312+
/// Ledger state: New account created with the coinbase amount as balance.
313+
#[test]
314+
fn test_apply_coinbase_creates_account() {
315+
let db = Database::create(15);
316+
let mut ledger = Mask::new_root(db);
317+
318+
let bob_pk = mina_signer::PubKey::from_address(
319+
"B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS",
320+
)
321+
.unwrap()
322+
.into_compressed();
323+
324+
let bob_id = AccountId::new(bob_pk.clone(), Default::default());
325+
326+
// Verify Bob's account does not exist before the transaction
327+
assert!(
328+
ledger.location_of_account(&bob_id).is_none(),
329+
"Bob's account should not exist before transaction"
330+
);
331+
332+
// Create a coinbase of 720 MINA to Bob (who doesn't exist yet)
333+
let coinbase_amount = Amount::from_u64(720_000_000_000);
334+
let coinbase = Coinbase::create(coinbase_amount, bob_pk.clone(), None).unwrap();
335+
336+
let constraint_constants = &test_constraint_constants();
337+
let state_view = ProtocolStateView {
338+
snarked_ledger_hash: Fp::zero(),
339+
blockchain_length: Length::from_u32(0),
340+
min_window_density: Length::from_u32(0),
341+
total_currency: Amount::zero(),
342+
global_slot_since_genesis: Slot::from_u32(0),
343+
staking_epoch_data: dummy_epoch_data(),
344+
next_epoch_data: dummy_epoch_data(),
345+
};
346+
let result = apply_transaction_first_pass(
347+
constraint_constants,
348+
Slot::from_u32(0),
349+
&state_view,
350+
&mut ledger,
351+
&Transaction::Coinbase(coinbase),
352+
);
353+
354+
assert!(result.is_ok());
355+
356+
// Verify Bob's account was created
357+
let bob_location = ledger.location_of_account(&bob_id);
358+
assert!(
359+
bob_location.is_some(),
360+
"Bob's account should exist after transaction"
361+
);
362+
363+
// Verify Bob's balance equals the coinbase amount minus account creation fee
364+
let bob_account = ledger.get(bob_location.unwrap()).unwrap();
365+
let account_creation_fee = constraint_constants.account_creation_fee;
366+
let expected_balance = Balance::from_u64(
367+
coinbase_amount
368+
.as_u64()
369+
.saturating_sub(account_creation_fee),
370+
);
371+
assert_eq!(
372+
bob_account.balance, expected_balance,
373+
"Bob's balance should equal coinbase minus account creation fee"
374+
);
375+
376+
// Verify Bob's nonce is 0 (new account)
377+
assert_eq!(
378+
bob_account.nonce,
379+
Nonce::zero(),
380+
"Bob's nonce should be 0 for new account"
381+
);
382+
}

0 commit comments

Comments
 (0)