diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b7eb3a52..5f393f11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `mina-node-native` ([#1549](https://github.com/o1-labs/mina-rust/pull/1549)) - **CI**: add a step in tests to run the unit/integration tests of the package `mina-node-native` ([#1549](https://github.com/o1-labs/mina-rust/pull/1549)) +- **Tests**: add account creation test cases for payment and coinbase + transactions, verifying correct handling of account creation fees during + the first pass of transaction application + ([#1581](https://github.com/o1-labs/mina-rust/pull/1581)) - **tools**: remove stack allocation from tools ([#1576](https://github.com/o1-labs/mina-rust/pull/1576)) diff --git a/ledger/tests/test_transaction_logic_first_pass.rs b/ledger/tests/test_transaction_logic_first_pass.rs index a28355148..9d5b63bdc 100644 --- a/ledger/tests/test_transaction_logic_first_pass.rs +++ b/ledger/tests/test_transaction_logic_first_pass.rs @@ -4,6 +4,7 @@ //! //! Tests the first pass of two-phase transaction application, covering: //! - Successful payment transactions +//! - Payment creating receiver account //! - Insufficient balance errors //! - Invalid nonce errors //! - Nonexistent fee payer errors @@ -439,3 +440,126 @@ fn test_apply_payment_nonexistent_fee_payer() { "Alice's account should still not exist after transaction error" ); } + +/// Test payment that creates a new receiver account. +/// +/// When the receiver account doesn't exist, a new account is created +/// automatically. The account creation fee is deducted from the payment amount, +/// not from the sender's balance. +/// +/// Ledger state: Sender's balance decreased by amount + fee, receiver account +/// created with balance = amount - account_creation_fee. +#[test] +fn test_apply_payment_creates_receiver_account() { + let db = Database::create(15); + let mut ledger = Mask::new_root(db); + + let alice_pk = mina_signer::PubKey::from_address( + "B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja", + ) + .unwrap() + .into_compressed(); + let bob_pk = mina_signer::PubKey::from_address( + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS", + ) + .unwrap() + .into_compressed(); + + // Create only Alice's account + let alice_id = AccountId::new(alice_pk.clone(), Default::default()); + let alice_account = Account::create_with(alice_id.clone(), Balance::from_u64(5_000_000_000)); + ledger + .get_or_create_account(alice_id.clone(), alice_account) + .unwrap(); + + let bob_id = AccountId::new(bob_pk.clone(), Default::default()); + + // Verify Bob's account does not exist before the transaction + assert!( + ledger.location_of_account(&bob_id).is_none(), + "Bob's account should not exist before transaction" + ); + + // Record initial state + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_before = ledger.get(alice_location).unwrap(); + let initial_alice_balance = alice_before.balance; + let initial_alice_nonce = alice_before.nonce; + let initial_alice_receipt_hash = alice_before.receipt_chain_hash; + + let amount = 2_000_000_000; // 2 MINA + let fee = 10_000_000; // 0.01 MINA + let nonce = 0; + let payment = create_payment(&alice_pk, &bob_pk, amount, fee, nonce); + + let constraint_constants = &test_constraint_constants(); + let account_creation_fee = constraint_constants.account_creation_fee; // 1 MINA + + let state_view = ProtocolStateView { + snarked_ledger_hash: Fp::zero(), + blockchain_length: Length::from_u32(0), + min_window_density: Length::from_u32(0), + total_currency: Amount::zero(), + global_slot_since_genesis: Slot::from_u32(0), + staking_epoch_data: dummy_epoch_data(), + next_epoch_data: dummy_epoch_data(), + }; + let result = apply_transaction_first_pass( + constraint_constants, + Slot::from_u32(0), + &state_view, + &mut ledger, + &Transaction::Command(UserCommand::SignedCommand(Box::new(payment))), + ); + + assert!(result.is_ok()); + + // Verify Alice's balance decreased by fee + payment amount + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_after = ledger.get(alice_location).unwrap(); + let expected_alice_balance = initial_alice_balance + .sub_amount(Amount::from_u64(fee)) + .unwrap() + .sub_amount(Amount::from_u64(amount)) + .unwrap(); + assert_eq!( + alice_after.balance, expected_alice_balance, + "Alice's balance should decrease by fee + payment amount" + ); + + // Verify Alice's nonce incremented + assert_eq!( + alice_after.nonce, + initial_alice_nonce.incr(), + "Alice's nonce should be incremented" + ); + + // Verify Alice's receipt chain hash updated + assert_ne!( + alice_after.receipt_chain_hash, initial_alice_receipt_hash, + "Alice's receipt chain hash should be updated" + ); + + // Verify Bob's account was created + let bob_location = ledger.location_of_account(&bob_id); + assert!( + bob_location.is_some(), + "Bob's account should now exist after transaction" + ); + + // Verify Bob's balance is payment amount minus account creation fee + let bob_location = bob_location.unwrap(); + let bob_after = ledger.get(bob_location).unwrap(); + let expected_bob_balance = Balance::from_u64(amount - account_creation_fee); + assert_eq!( + bob_after.balance, expected_bob_balance, + "Bob's balance should be payment amount minus account creation fee" + ); + + // Verify Bob's nonce is 0 (new account) + assert_eq!( + bob_after.nonce, + Nonce::zero(), + "Bob's nonce should be 0 for new account" + ); +} diff --git a/ledger/tests/test_transaction_logic_first_pass_coinbase.rs b/ledger/tests/test_transaction_logic_first_pass_coinbase.rs index c2754517b..11d881f51 100644 --- a/ledger/tests/test_transaction_logic_first_pass_coinbase.rs +++ b/ledger/tests/test_transaction_logic_first_pass_coinbase.rs @@ -1,11 +1,12 @@ //! Tests for apply_transaction_first_pass with coinbase transactions //! -//! Run with: cargo test --test test_transaction_logic_first_pass_coinbase +//! Run with: cargo test --test test_transaction_logic_first_pass_coinbase --release //! //! Tests the first pass of two-phase transaction application for coinbase //! rewards, covering: //! - Successful coinbase without fee transfer //! - Successful coinbase with fee transfer to different account +//! - Coinbase with fee transfer to nonexistent account (creates account) //! - Coinbase with fee transfer to same account (fee transfer should be //! removed) //! - Coinbase creating a new account @@ -228,6 +229,120 @@ fn test_apply_coinbase_with_fee_transfer() { ); } +/// Test coinbase with fee transfer to a nonexistent account. +/// +/// The coinbase receiver exists, but the fee transfer receiver doesn't exist. +/// The fee transfer should create the receiver account, deducting the account +/// creation fee from the fee transfer amount. +/// +/// Ledger state: +/// - Coinbase receiver gets coinbase_amount - fee_transfer_amount +/// - Fee transfer receiver account created with fee_transfer_amount - +/// account_creation_fee +#[test] +fn test_apply_coinbase_with_fee_transfer_creates_account() { + let mut ledger = create_test_ledger(); + + let alice_pk = mina_signer::PubKey::from_address( + "B62qmnY6m4c6bdgSPnQGZriSaj9vuSjsfh6qkveGTsFX3yGA5ywRaja", + ) + .unwrap() + .into_compressed(); + let bob_pk = mina_signer::PubKey::from_address( + "B62qjVQLxt9nYMWGn45mkgwYfcz8e8jvjNCBo11VKJb7vxDNwv5QLPS", + ) + .unwrap() + .into_compressed(); + + let alice_id = AccountId::new(alice_pk.clone(), Default::default()); + let bob_id = AccountId::new(bob_pk.clone(), Default::default()); + + // Verify Bob's account does not exist before the transaction + assert!( + ledger.location_of_account(&bob_id).is_none(), + "Bob's account should not exist before transaction" + ); + + // Record Alice's initial state + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_before = ledger.get(alice_location).unwrap(); + let initial_alice_balance = alice_before.balance; + + // Create a coinbase of 720 MINA to Alice with a 10 MINA fee transfer to Bob + // (who doesn't exist yet) + let coinbase_amount = Amount::from_u64(720_000_000_000); + let fee_transfer_amount = Fee::from_u64(10_000_000_000); + let fee_transfer = CoinbaseFeeTransfer::create(bob_pk.clone(), fee_transfer_amount); + let coinbase = Coinbase::create(coinbase_amount, alice_pk.clone(), Some(fee_transfer)).unwrap(); + + let constraint_constants = &test_constraint_constants(); + let state_view = ProtocolStateView { + snarked_ledger_hash: Fp::zero(), + blockchain_length: Length::from_u32(0), + min_window_density: Length::from_u32(0), + total_currency: Amount::zero(), + global_slot_since_genesis: Slot::from_u32(0), + staking_epoch_data: dummy_epoch_data(), + next_epoch_data: dummy_epoch_data(), + }; + let result = apply_transaction_first_pass( + constraint_constants, + Slot::from_u32(0), + &state_view, + &mut ledger, + &Transaction::Coinbase(coinbase), + ); + + assert!(result.is_ok()); + + // Verify Bob's account was created + let bob_location = ledger.location_of_account(&bob_id); + assert!( + bob_location.is_some(), + "Bob's account should exist after transaction" + ); + + // Verify ledger state changes + let alice_location = ledger.location_of_account(&alice_id).unwrap(); + let alice_after = ledger.get(alice_location).unwrap(); + let bob_account = ledger.get(bob_location.unwrap()).unwrap(); + + // Verify Alice's balance increased by (coinbase amount - fee transfer amount) + let coinbase_after_fee_transfer = coinbase_amount + .checked_sub(&Amount::of_fee(&fee_transfer_amount)) + .unwrap(); + let expected_alice_balance = initial_alice_balance + .add_amount(coinbase_after_fee_transfer) + .unwrap(); + assert_eq!( + alice_after.balance, expected_alice_balance, + "Alice's balance should increase by coinbase minus fee transfer" + ); + + // Verify Bob's balance equals fee transfer amount minus account creation fee + let account_creation_fee = constraint_constants.account_creation_fee; + let expected_bob_balance = Balance::from_u64( + Amount::of_fee(&fee_transfer_amount) + .as_u64() + .saturating_sub(account_creation_fee), + ); + assert_eq!( + bob_account.balance, expected_bob_balance, + "Bob's balance should equal fee transfer minus account creation fee" + ); + + // Verify nonces + assert_eq!( + alice_after.nonce, alice_before.nonce, + "Alice's nonce should remain unchanged" + ); + assert_eq!( + bob_account.nonce, + Nonce::zero(), + "Bob's nonce should be 0 for new account" + ); +} + /// Test coinbase with fee transfer to the same account. /// /// When the coinbase receiver and fee transfer receiver are the same, the fee