Implement `AccountStorage` upgrades
- Lingua principale
- Rust
- Stelle
- 132
- Fork
- 167
- Merge medio
- 1g 23h
- PR unite (30g)
- 110
Descrizione
While working on https://github.com/0xMiden/miden-base/issues/2078, it seemed useful to think about how we can support adding and removing storage slots, so that the storage delta structure is already set up for this and doesn't have to be refactored again.
With the proposed structure from that issue this should be possible and the following more concrete proposal is based on https://github.com/0xMiden/miden-base/discussions/1771#discussioncomment-14293579.
Upgrading storage, i.e. creating or removing slots could work like this:
- User selects account component they want to add or remove.
- Notably, for removal, an abstract description of a component is sufficient, e.g. its MAST roots and slot names; no concretely instantiated component is necessary. This could perhaps be a newly added `AccountComponentSchema` which contains an `AccountCodeSchema` and `AccountStorageSchema`, the latter mentioned here: https://github.com/0xMiden/miden-base/issues/2122.
- Rust APIs compute the new `AccountStorage` (ignoring code here).
- Account storage commitment is added to the advice map with the storage slots as the content, just like we do now.
- The new account storage commitment is put on the advice stack which is processed during the prologue. Set to `EMPTY_WORD` for "no update". The prologue stores this somewhere in kernel memory.
- In `finalize_transaction`, _before_ `execute_auth_procedure` the new slot commitment is read from kernel memory and piped from advice map into a new memory location (so it does not overwrite the existing ones) and the new slot commitment is computed and validated against the one from kernel memory.
- During account delta computation, we need to find the delta operation (created, updated, removed) for each slot, by comparing the set of old with the set of new slots.
- The new slots are committed to by the account delta and therefore also `TransactionSummary`, so this should be safe. \*
The overall pseudo logic in `account_delta.masm` could look like this:
```text
proc update_storage_delta(hasher)
let old_slots = if exec.memory::is_new_account {
# if the account is new, use an empty storage to compare against
# this means all new slots will be flagged as "created"
# note: this procedure does not currently exist
exec.memory::get_empty_storage_slots_section_ptr
} else {
# if the account is not new, use the initial storage slots
exec.memory::get_native_account_initial_storage_slots_ptr
}
let new_slots = if is_storage_upgrade {
# if storage is being upgraded, use a pointer to the new storage slots section
# note: this procedure does not currently exist
exec.memory::get_new_storage_slots_ptr
} else {
# if storage is not being upgraded, use the "current" storage slots
exec.memory::get_native_account_storage_slots_ptr
}
# now compare old against new slots, checking which slots were created, updated or removed
# from old to new.
# this iterates in slot ID order
old_idx = 0
new_idx = 0
# TODO: assumes only value slots exist for simplicity
while let Some((delta_op, old_incr, new_incr)) =
exec.compute_storage_diff(old_slots[old_idx], new_slots[new_idx]) {
match delta_op {
created => {
(slot_id, VALUE) = new_slots[new_idx]
hasher.hash(
[[domain = 2, delta_op, slot_id_suffix, slot_id_prefix], VALUE]
)
},
exists => {
# TODO: validate that slot type hasn't changed.
(_slot_id, OLD_VALUE) = old_slots[old_idx]
(slot_id, NEW_VALUE) = new_slots[new_idx]
if OLD_VALUE != NEW_VALUE {
hasher.hash(
[[domain = 2, delta_op, slot_id_suffix, slot_id_prefix], NEW_VALUE]
)
}
},
removed => {
(slot_id, _VALUE) = old_slots[new_idx]
hasher.hash(
[[domain = 2, delta_op, slot_id_suffix, slot_id_prefix], EMPTY_WORD]
)
}
}
old_idx += old_incr
new_idx += new_incr
}
end
#! Inputs: [old_slots_ptr, new_slots_ptr]
#! Outputs: [slot_delta_op, old_incr, new_incr]
proc compute_storage_diff
# produce diff on sorted sequence, returning created, exists or removed
end
```
What I like about this is that it handles the special case of account creation, regular storage deltas as well as storage upgrades with one loop and logic.
## Slot Set Difference
The `compute_storage_diff` procedure could be implemented as follows, with the example using numbers as slot IDs:
- old slots: `[1, 2, 5]`
- new slots: `[2, 3, 5]`
- Note that these are sorted slot IDs.
- Start iterating comparing `old_slots[old_index = 0]` with `new_slots[new_index = 0]`. 1 < 2, so 1 is a _removed_ slot. Increment `old_index`.
- Continue iterating, comparing `old_slots[old_index = 1]` with `new_slots[new_index = 0]`. 2 == 2, so 2 is an _existing_ slot (check if it has been updated). Increment both indices.
- Continue iterating, comparing `old_slots[old_index = 2]` with `new_slots[new_index = 1]`. 5 > 3, so 3 is a _created_ slot. Increment `new_index`.
- Continue iterating, comparing `old_slots[old_index = 2]` with `new_slots[new_index = 2]`. 5 == 5, so 5 is an _existing_ slot (check if it has been updated). Increment both indices.
- Done iterating since both indices exceed their respective list length.
- So in essence, we iterate both storage slot sections in parallel. This allows us to process slot IDs in order, which is a requirement for the account delta.
## Alternatives
There is an alternative approach. Instead of providing the entire new account storage to the kernel, it would also be feasible to somehow communicate individual slots that should be created or removed to the kernel, for example via a tx script. The difficulty for the tx kernel is that it needs to compute the delta commitment by sorted slot ID (for reproducibility). So, creating a slot would mean inserting the new slot into the current slots section at the correct location, shifting all other slots to the right. This seems computationally intensive for even a single slot.
## Relationship to `AccountCode` upgrades
The above could work basically the same for account code, but this is probably best left for a separate issue.
One question is whether the code delta should commit to a set of removed or added procedure MAST roots, similar to the storage delta, or simply the entire new commitment.
- The individual procedures are more fine-grained, and this would enable signing a delta that, e.g. adds a single procedure to the account code, without committing to the entire code. This should make it easier to have multiple in-flight deltas that mutate account code, though not sure how necessary that is in practice.
- If we want that, then we'd need to change account procedures to be sorted, so that we can produce a diff for new/old procedures in the same way as for old/new slots.
## Open Questions
- \* Should it be possible to disallow upgrades at the interface level instead of at the auth level? Thinking about accounts that do not have auth but still want to prevent unwanted upgrades. If so, there needs to be a slightly different mechanism for triggering the update, maybe not automatically, but rather as part of a tx script.
- Another question is whether it's fine for storage updates that happen during a transaction to be overwritten by the storage provided as part of the upgrade. We could maybe validate that for all slots that continue to exist, their values match in the old and new section, but it's not clear to me yet whether this provides a lot of value.
Guida per i contributori
Apri la guida per i contributori
Valutazione
Questa issue non è ancora stata valutata.