Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.12.2 (unreleased)

- Added `create_mint_note` and `create_burn_note` helper functions for creating standardized MINT and BURN notes ([#2061](https://github.com/0xMiden/miden-base/pull/2061)).
- Skip value slot normalization for new account's deltas ([#2075](https://github.com/0xMiden/miden-base/pull/2075)).

## 0.12.1 (2025-11-06)

Expand Down
11 changes: 10 additions & 1 deletion crates/miden-lib/asm/kernels/transaction/lib/account_delta.masm
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,16 @@ proc.update_value_slot_delta
exec.word::test_eq not
# => [was_changed, INIT_VALUE', CURRENT_VALUE, slot_idx, RATE, RATE, PERM]

# only include in delta if the slot's value has changed
# set was_changed to true if the account is new
# this means a storage slot initialized with an empty word is included in the commitment for
# new accounts
# more generally, we want the delta for a new account to include all its newly added values,
# regardless of the exact value, because the initial delta for an account must represent its
# full state
exec.memory::get_init_nonce eq.0 or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR: but we use the check init_nonce == 0 in a few places now and I wonder if we should add something like memory::is_account_new procedure.

# => [was_changed, INIT_VALUE', CURRENT_VALUE, slot_idx, RATE, RATE, PERM]

# only include in delta if the slot's value has changed or the account is new
if.true
# drop init value
dropw
Expand Down
2 changes: 1 addition & 1 deletion crates/miden-lib/src/transaction/kernel_procedures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub const KERNEL_PROCEDURES: [Word; 52] = [
// account_has_non_fungible_asset
word!("0xfaad11de0c026551df15231790c2364cc598e891444bf826da01b524b1a8ca8f"),
// account_compute_delta_commitment
word!("0x88a66fd1da5df01c98d02c7d76cd567de9a17c6641c045fcd9b05881e990826a"),
word!("0x42269a8b9160ae865b8da796ef02c8730ee857f95b219c1398dcc3ec17010f13"),
// account_get_num_procedures
word!("0x53b5ec38b7841948762c258010e6e07ad93963bcaac2d83813f8edb6710dc720"),
// account_get_procedure_root
Expand Down
38 changes: 37 additions & 1 deletion crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use miden_objects::testing::constants::{
};
use miden_objects::testing::storage::{STORAGE_INDEX_0, STORAGE_INDEX_2};
use miden_objects::transaction::TransactionScript;
use miden_objects::{EMPTY_WORD, Felt, LexicographicWord, Word, ZERO};
use miden_objects::{EMPTY_WORD, Felt, FieldElement, LexicographicWord, Word, ZERO};
use miden_tx::LocalTransactionProver;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
Expand Down Expand Up @@ -869,6 +869,42 @@ async fn proven_tx_storage_maps_matches_executed_tx_for_new_account() -> anyhow:
Ok(())
}

/// Tests that creating a new account with a slot whose value is empty is correctly included in the
/// delta and not normalized away.
#[tokio::test]
async fn delta_for_new_account_retains_empty_value_storage_slots() -> anyhow::Result<()> {
Comment on lines +872 to +875
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this on next by:

$ git restore -s origin/pgackst-fix-new-account-delta crates/miden-testing/src/kernel_tests/tx/test_account_delta.rs
$ cargo test delta_for_new_account_retains_empty_value_storage_slots

and it fails with the error reported in #2072.

👍🏼

let slot_value2 = Word::from([1, 2, 3, 4u32]);
let mut account = AccountBuilder::new(rand::random())
.account_type(AccountType::RegularAccountUpdatableCode)
.storage_mode(AccountStorageMode::Network)
.with_component(MockAccountComponent::with_slots(vec![
StorageSlot::empty_value(),
StorageSlot::Value(slot_value2),
]))
.with_auth_component(Auth::IncrNonce)
.build()?;

let tx = TransactionContextBuilder::new(account.clone()).build()?.execute().await?;

let proven_tx = LocalTransactionProver::default().prove_dummy(tx.clone())?;

let AccountUpdateDetails::Delta(delta) = proven_tx.account_update().details() else {
panic!("expected delta");
};

assert_eq!(delta.storage().values().len(), 2);
assert_eq!(delta.storage().values().get(&0).unwrap(), &Word::empty());
assert_eq!(delta.storage().values().get(&1).unwrap(), &slot_value2);

let recreated_account = Account::try_from(delta)?;
// The recreated account should match the original account with the nonce incremented (and the
// seed removed).
account.increment_nonce(Felt::ONE)?;
assert_eq!(recreated_account, account);

Ok(())
}

/// Tests that adding a fungible asset with amount zero to the account vault works and does not
/// result in an account delta entry.
#[tokio::test]
Expand Down
11 changes: 2 additions & 9 deletions crates/miden-tx/src/host/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ where
/// Extracts information from the process state about the storage slot being updated and
/// records the latest value of this storage slot.
///
/// Expected stack state: `[event, slot_index, NEW_SLOT_VALUE, CURRENT_SLOT_VALUE, ...]`
/// Expected stack state: `[event, slot_index, NEW_SLOT_VALUE]`
pub fn on_account_storage_after_set_item(
&mut self,
process: &ProcessState,
Expand All @@ -654,14 +654,7 @@ where
// get the value to which the slot is being updated
let new_slot_value = process.get_stack_word_be(2);

// get the current value for the slot
let current_slot_value = process.get_stack_word_be(6);

self.account_delta.storage().set_item(
slot_index.as_int() as u8,
current_slot_value,
new_slot_value,
);
self.account_delta.storage().set_item(slot_index.as_int() as u8, new_slot_value);

Ok(())
}
Expand Down
45 changes: 28 additions & 17 deletions crates/miden-tx/src/host/storage_delta_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use miden_objects::account::{
/// that slot.
#[derive(Debug, Clone)]
pub struct StorageDeltaTracker {
/// Flag indicating whether this delta is for a new account.
is_account_new: bool,
/// The _initial_ storage header of the native account against which the transaction is
/// executed. This is only used to look up the initial values of storage _value_ slots, while
/// the map slots are unused.
Expand Down Expand Up @@ -48,6 +50,7 @@ impl StorageDeltaTracker {
};

let mut storage_delta_tracker = Self {
is_account_new: account.is_new(),
storage_header: initial_storage_header,
init_maps: BTreeMap::new(),
delta: AccountStorageDelta::new(),
Expand All @@ -58,9 +61,9 @@ impl StorageDeltaTracker {
(0..u8::MAX).zip(account.storage().header().slots()).for_each(
|(slot_idx, (slot_type, value))| match slot_type {
StorageSlotType::Value => {
// Note that we can insert the value unconditionally as the delta will be
// normalized before the commitment is computed.
storage_delta_tracker.set_item(slot_idx, Word::empty(), *value);
// For new accounts, all values should be added to the delta, even empty
// words, so that the final delta includes the storage slot.
storage_delta_tracker.set_item(slot_idx, *value);
},
StorageSlotType::Map => {
let storage_map = account
Expand Down Expand Up @@ -89,11 +92,8 @@ impl StorageDeltaTracker {
// --------------------------------------------------------------------------------------------

/// Updates a value slot.
pub fn set_item(&mut self, slot_index: u8, prev_value: Word, new_value: Word) {
// Don't update the delta if the new value matches the old one.
if prev_value != new_value {
self.delta.set_item(slot_index, new_value);
}
pub fn set_item(&mut self, slot_index: u8, new_value: Word) {
self.delta.set_item(slot_index, new_value);
}

/// Updates a map slot.
Expand Down Expand Up @@ -127,17 +127,28 @@ impl StorageDeltaTracker {
/// - removing entries for map slot updates where for a given key, the new value is equal to the
/// initial value at the beginning of transaction execution.
fn normalize(self) -> AccountStorageDelta {
let Self { storage_header, init_maps, delta } = self;
let Self {
is_account_new,
storage_header,
init_maps,
delta,
} = self;
let (mut value_slots, mut map_slots) = delta.into_parts();

// Keep only the values whose new value is different from the initial value.
value_slots.retain(|slot_idx, new_value| {
// SAFETY: The header in the initial storage is the one from the account against which
// the transaction is executed, so accessing that slot index should be fine.
let (_, initial_value) =
storage_header.slot(*slot_idx as usize).expect("index should be in bounds");
new_value != initial_value
});
// Skip normalization of value slots for new accounts. Since the initial value for
// normalization defaults to Word::empty, this prevents slots being removed that are validly
// created with an empty value.
if !is_account_new {
// Keep only the values whose new value is different from the initial value.
value_slots.retain(|slot_idx, new_value| {
// SAFETY: The header in the initial storage is the one from the account against
// which the transaction is executed, so accessing that slot index
// should be fine.
let (_, initial_value) =
storage_header.slot(*slot_idx as usize).expect("index should be in bounds");
new_value != initial_value
});
}

// On the key-value level: Keep only the key-value pairs whose new value is different from
// the initial value.
Expand Down