diff --git a/CLAUDE.md b/CLAUDE.md index 8716ab5d..66f75c01 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -283,7 +283,7 @@ The `pvm-storage` crate provides typed storage helpers with Solidity-compatible - `set(&mut self)` / `insert(&mut self)` / `entry(&mut self)` take `&mut self` for future view enforcement - `Mapping::entry()` returns a `Lazy` handle for the derived slot, allowing read-then-write on the same key with a single keccak derivation instead of two - Nested mappings via chaining: `self.allowances.get(&owner).get(&spender)` -- **Composability:** `#[storage]` structs auto-implement `StorageComponent` (slot reservation) and, under `--features abi-gen`, `StorageLayoutEmit` (so the outer contract flattens their leaves into the `storageLayout` JSON with dotted labels like `erc20.total_supply`). Hand-rolled storage components (e.g. a future `StorageVec`) must implement both traits to participate in auto-numbered layouts and abi-gen output. `StorageComponent::PACKED_BYTES = 32` declares "always start a fresh slot" — mappings, multi-slot composites, and embedded `#[storage]` sub-structs all set this; sub-32-byte primitives propagate `T::PACKED_BYTES` +- **Composability:** `#[storage]` structs auto-implement `StorageComponent` (slot reservation) and, under `--features abi-gen`, `StorageLayoutEmit` (so the outer contract flattens their leaves into the `storageLayout` JSON with dotted labels like `erc20.total_supply`). Hand-rolled storage components (e.g. `StorageVec`) must implement both traits to participate in auto-numbered layouts and abi-gen output. `StorageComponent::PACKED_BYTES = 32` declares "always start a fresh slot" — mappings, multi-slot composites, and embedded `#[storage]` sub-structs all set this; sub-32-byte primitives propagate `T::PACKED_BYTES` - **Field-level packing:** adjacent sub-32-byte contract fields share a slot byte-for-byte with solc's layout — `Lazy a; Lazy b;` lands at `(slot=0, offset=16)` and `(slot=0, offset=0)`. The macro walker (`pvm_storage::layout_step`) is the const-fn computing each placement; the `storageLayout` JSON's `offset` field matches solc. Packed writes are read-modify-write (one SLOAD + one SSTORE), matching solc/Stylus gas profile - **`try_get` is full-slot only:** `Lazy::::try_get` is rejected at compile time for sub-32-byte `T` (e.g. `Lazy`) with a const-assert message — a neighbour's write to the same slot would make `try_get` indistinguishable from `get`. For packed fields, use `.get()` and compare to the zero value of `T` - **Test-harness contract modules:** `#[contract(no_main)]` suppresses the abi-gen `fn main()` emission so a `#[contract]` can sit inside a `tests/` integration test or library crate without colliding with the test harness's own `main`. `__abi_json()` / `__storage_layout_json()` accessors are still emitted on the module diff --git a/crates/cargo-pvm-contract/templates/examples/packed_handle/Packed.sol b/crates/cargo-pvm-contract/templates/examples/packed_handle/Packed.sol new file mode 100644 index 00000000..e29f64fd --- /dev/null +++ b/crates/cargo-pvm-contract/templates/examples/packed_handle/Packed.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +interface Packed { + function feeBps() external view returns (uint128); + function maxSupply() external view returns (uint128); + function setFeeBps(uint128 v) external; + function setMaxSupply(uint128 v) external; +} diff --git a/crates/cargo-pvm-contract/templates/examples/packed_handle/packed_handle_no_alloc.rs b/crates/cargo-pvm-contract/templates/examples/packed_handle/packed_handle_no_alloc.rs new file mode 100644 index 00000000..6ebbc136 --- /dev/null +++ b/crates/cargo-pvm-contract/templates/examples/packed_handle/packed_handle_no_alloc.rs @@ -0,0 +1,41 @@ +#![no_main] +#![no_std] + +#[pvm_contract_sdk::contract("Packed.sol", buffer = 256)] +mod packed { + use pvm_contract_sdk::Lazy; + + pub struct Packed { + fee_bps: Lazy, + max_supply: Lazy, + } + + impl Packed { + #[pvm_contract_sdk::constructor] + pub fn new(&mut self) -> Result<(), pvm_contract_sdk::EmptyError> { + Ok(()) + } + + #[pvm_contract_sdk::method] + pub fn fee_bps(&self) -> u128 { + self.fee_bps.get() + } + + #[pvm_contract_sdk::method] + pub fn max_supply(&self) -> u128 { + self.max_supply.get() + } + + #[pvm_contract_sdk::method] + pub fn set_fee_bps(&mut self, v: u128) -> Result<(), pvm_contract_sdk::EmptyError> { + self.fee_bps.set(&v); + Ok(()) + } + + #[pvm_contract_sdk::method] + pub fn set_max_supply(&mut self, v: u128) -> Result<(), pvm_contract_sdk::EmptyError> { + self.max_supply.set(&v); + Ok(()) + } + } +} diff --git a/crates/cargo-pvm-contract/templates/examples/packed_value/Packed.sol b/crates/cargo-pvm-contract/templates/examples/packed_value/Packed.sol new file mode 100644 index 00000000..e29f64fd --- /dev/null +++ b/crates/cargo-pvm-contract/templates/examples/packed_value/Packed.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +interface Packed { + function feeBps() external view returns (uint128); + function maxSupply() external view returns (uint128); + function setFeeBps(uint128 v) external; + function setMaxSupply(uint128 v) external; +} diff --git a/crates/cargo-pvm-contract/templates/examples/packed_value/packed_value_no_alloc.rs b/crates/cargo-pvm-contract/templates/examples/packed_value/packed_value_no_alloc.rs new file mode 100644 index 00000000..843cee63 --- /dev/null +++ b/crates/cargo-pvm-contract/templates/examples/packed_value/packed_value_no_alloc.rs @@ -0,0 +1,50 @@ +#![no_main] +#![no_std] + +#[pvm_contract_sdk::contract("Packed.sol", buffer = 256)] +mod packed { + use pvm_contract_sdk::Lazy; + + #[derive(pvm_contract_sdk::SolType)] + pub struct Settings { + pub fee_bps: u128, + pub max_supply: u128, + } + + pub struct Packed { + settings: Lazy, + } + + impl Packed { + #[pvm_contract_sdk::constructor] + pub fn new(&mut self) -> Result<(), pvm_contract_sdk::EmptyError> { + Ok(()) + } + + #[pvm_contract_sdk::method] + pub fn fee_bps(&self) -> u128 { + self.settings.get().fee_bps + } + + #[pvm_contract_sdk::method] + pub fn max_supply(&self) -> u128 { + self.settings.get().max_supply + } + + #[pvm_contract_sdk::method] + pub fn set_fee_bps(&mut self, v: u128) -> Result<(), pvm_contract_sdk::EmptyError> { + let mut s = self.settings.get(); + s.fee_bps = v; + self.settings.set(&s); + Ok(()) + } + + #[pvm_contract_sdk::method] + pub fn set_max_supply(&mut self, v: u128) -> Result<(), pvm_contract_sdk::EmptyError> { + let mut s = self.settings.get(); + s.max_supply = v; + self.settings.set(&s); + Ok(()) + } + } +} diff --git a/crates/pvm-contract-benchmarks/src/bin/build-and-measure.rs b/crates/pvm-contract-benchmarks/src/bin/build-and-measure.rs index 291a376c..a960124b 100644 --- a/crates/pvm-contract-benchmarks/src/bin/build-and-measure.rs +++ b/crates/pvm-contract-benchmarks/src/bin/build-and-measure.rs @@ -251,6 +251,13 @@ fn build_variant( } fn variants_for_contract(contract: &str) -> Vec { + // The `packed_*` contracts are an A/B comparison of the storage value + // model (`Lazy` over a `#[derive(SolType)]` struct) vs the handle + // bundle model (`#[storage]` of `Lazy` leaves). They only build the + // no-alloc variant — the alloc/dsl axes are orthogonal to the question. + if contract.starts_with("packed_") { + return vec![Variant::NoAlloc]; + } let mut variants = vec![Variant::NoAlloc, Variant::WithAlloc, Variant::BuilderDsl]; if contract == "mytoken" { variants.push(Variant::Storage); @@ -270,7 +277,13 @@ fn main() -> Result<()> { let artifacts_dir = PathBuf::from("target/benchmark-artifacts"); fs::create_dir_all(&artifacts_dir).context("Failed to create artifacts directory")?; - let contracts = vec!["fibonacci", "mytoken", "multi"]; + let contracts = vec![ + "fibonacci", + "mytoken", + "multi", + "packed_handle", + "packed_value", + ]; let profiles = vec!["debug", "release"]; let total: usize = contracts diff --git a/crates/pvm-contract-sdk/src/lib.rs b/crates/pvm-contract-sdk/src/lib.rs index 5d7bd21b..6a904945 100644 --- a/crates/pvm-contract-sdk/src/lib.rs +++ b/crates/pvm-contract-sdk/src/lib.rs @@ -96,6 +96,7 @@ pub use pvm_contract_types::{ SolEvent, StaticDecode, StaticEncodedLen, + StorageArrayElement, StorageDecode, StorageEncode, StorageFlags, @@ -123,12 +124,14 @@ pub use pvm_contract_core::call::{ // Typed storage helpers. `Lazy` / `Mapping` cover both static // 32-byte values (`U256`, `Address`, `[u8; 32]`, …) and dynamic ones // (`String`, `Bytes`, structs with dynamic fields) through their -// `StorageEncode`/`StorageDecode` impls. `Vec` is intentionally not a -// storage value — use `Bytes` for `bytes`-shaped storage (`Vec` is ABI -// `uint8[]`, a different on-chain layout). `StorageComponent` is the trait -// typed storage helpers implement to participate in auto-numbered slot layout. +// `StorageEncode`/`StorageDecode` impls. `StorageVec` models Solidity's +// `T[]` dynamic arrays. `Vec` is intentionally not a storage value — +// use `Bytes` for `bytes`-shaped storage (`Vec` is ABI `uint8[]`, a +// different on-chain layout). `StorageComponent` is the trait typed +// storage helpers implement to participate in auto-numbered slot layout. pub use pvm_storage::{ - AsStorageKey, LayoutStep, Lazy, Mapping, Ref, RefMut, StorageComponent, StorageKey, layout_step, + AsStorageKey, LayoutStep, Lazy, Mapping, Ref, RefMut, StorageComponent, StorageKey, StorageVec, + layout_step, }; #[cfg(feature = "abi-gen")] diff --git a/crates/pvm-contract-types/src/lib.rs b/crates/pvm-contract-types/src/lib.rs index 8d252758..4be51c21 100644 --- a/crates/pvm-contract-types/src/lib.rs +++ b/crates/pvm-contract-types/src/lib.rs @@ -62,7 +62,7 @@ mod i256; pub use i256::{I256, ParseI256Error}; mod storage_codec; -pub use storage_codec::{StorageDecode, StorageEncode, StoragePackable}; +pub use storage_codec::{StorageArrayElement, StorageDecode, StorageEncode, StoragePackable}; #[doc(hidden)] pub use const_format; diff --git a/crates/pvm-contract-types/src/storage_codec.rs b/crates/pvm-contract-types/src/storage_codec.rs index ebb4ad8c..56d75986 100644 --- a/crates/pvm-contract-types/src/storage_codec.rs +++ b/crates/pvm-contract-types/src/storage_codec.rs @@ -578,6 +578,128 @@ impl StoragePackable for [u8; N] { } } +// --------------------------------------------------------------------------- +// Fixed-size arrays `[T; N]` for T != u8. +// +// solc supports `T[N]` for any static storage type T. This impl mirrors +// solc's layout for the shapes the SDK ships impls for: +// - sub-word T (`uint16`..`uint128`, `int16`..`int128`, `bool`, `Address`, +// `[u8; M]` for M < 32): density elements per slot +// (`density = 32 / PACKED_BYTES`), right-aligned within each slot; +// total slots = ceil(N / density). +// - single-slot full-word T (`U256`, `I256`, `[u8; 32]`): one element per +// slot; total slots = N. +// - multi-slot static T (e.g. `[U256; M]` if added via marker, derived +// structs spanning >1 slot): each element strides by `T::STORAGE_SLOTS`; +// total slots = N * STORAGE_SLOTS. +// +// `[u8; N]` keeps its dedicated `bytesN` impl above (the marker excludes +// `u8`). Tuples are not in the default `StorageArrayElement` list — `[(A, +// B); N]` won't compile out of the box. Downstream code that wants +// `[MyTuple; N]` or `[MyStruct; N]` must `impl StorageArrayElement` for the +// element type manually. +// +// Dynamic-body T (`String`, `Bytes`) is not supported in fixed arrays — +// solc's storage layout for those involves per-element headers and is left +// as a follow-up. Manually `impl StorageArrayElement` for a dynamic-body T +// will type-check but **panic at runtime** (its `encode_slot` / +// `from_slots` are `unreachable!()` stubs). +// --------------------------------------------------------------------------- + +/// Marker trait gating which element types can appear in `[T; N]` storage. +/// +/// Implemented in-tree for every primitive scalar except `u8` (`[u8; N]` +/// keeps its dedicated `bytesN` impl). Downstream code can implement this +/// for custom **static** `SolType`-derived structs (or tuples) to opt them +/// into `[MyStruct; N]` support. +/// +/// # Do not implement for dynamic-body types +/// +/// `String`, `Bytes`, and any `SolType` struct with `HAS_DYNAMIC_BODY = +/// true` route their encode/decode through `write_to_storage` / +/// `read_from_storage`; their `encode_slot` / `from_slots` are +/// `unreachable!()` stubs. The generic `[T; N]` impl dispatches through +/// `encode_slot` / `from_slots`, so a dynamic-body T would compile but +/// panic at runtime. Stick to static element types. +pub trait StorageArrayElement: StorageEncode + StorageDecode {} + +macro_rules! impl_storage_array_element { + ($($T:ty),+ $(,)?) => { + $(impl StorageArrayElement for $T {})+ + }; +} + +impl_storage_array_element!( + u16, u32, u64, u128, U256, i16, i32, i64, i128, I256, bool, Address, +); + +impl StorageEncode for [T; N] { + /// Sub-word: ceil(N / density). Single-slot full-word: N. Multi-slot + /// static: N * STORAGE_SLOTS. + const STORAGE_SLOTS: usize = { + if T::PACKED_BYTES < 32 { + let density = 32 / T::PACKED_BYTES; + N.div_ceil(density) + } else { + N * T::STORAGE_SLOTS + } + }; + + /// Fixed arrays always start a fresh slot (`PACKED_BYTES = 32`), matching + /// solc's layout for `T[N]` fields. + const PACKED_BYTES: usize = 32; + + fn encode_slot(&self, slot_idx: usize, buf: &mut [u8; 32]) { + *buf = [0u8; 32]; + if T::PACKED_BYTES < 32 { + // Sub-word: pack `density` elements right-aligned within this slot. + let density = 32 / T::PACKED_BYTES; + let start = slot_idx * density; + let end = ((slot_idx + 1) * density).min(N); + let mut tmp = [0u8; 32]; + let elem_start = 32 - T::PACKED_BYTES; + for (within, elem) in self[start..end].iter().enumerate() { + let offset = 32 - T::PACKED_BYTES * (within + 1); + tmp.fill(0); + elem.encode_slot(0, &mut tmp); + buf[offset..offset + T::PACKED_BYTES] + .copy_from_slice(&tmp[elem_start..elem_start + T::PACKED_BYTES]); + } + } else if T::STORAGE_SLOTS == 1 { + // One element per slot. + self[slot_idx].encode_slot(0, buf); + } else { + // Multi-slot static: stride `T::STORAGE_SLOTS` per element. + let elem_idx = slot_idx / T::STORAGE_SLOTS; + let within_elem = slot_idx % T::STORAGE_SLOTS; + self[elem_idx].encode_slot(within_elem, buf); + } + } +} + +impl StorageDecode for [T; N] { + fn from_slots(slots: &[[u8; 32]]) -> Self { + core::array::from_fn(|i| { + if T::PACKED_BYTES < 32 { + let density = 32 / T::PACKED_BYTES; + let slot_idx = i / density; + let within = i % density; + let offset = 32 - T::PACKED_BYTES * (within + 1); + let mut tmp = [0u8; 32]; + let elem_start = 32 - T::PACKED_BYTES; + tmp[elem_start..elem_start + T::PACKED_BYTES] + .copy_from_slice(&slots[slot_idx][offset..offset + T::PACKED_BYTES]); + T::from_slots(&[tmp]) + } else if T::STORAGE_SLOTS == 1 { + T::from_slots(&slots[i..i + 1]) + } else { + let start = i * T::STORAGE_SLOTS; + T::from_slots(&slots[start..start + T::STORAGE_SLOTS]) + } + }) + } +} + // --------------------------------------------------------------------------- // Tuple impls — same packing rules as structs. // diff --git a/crates/pvm-storage/src/lib.rs b/crates/pvm-storage/src/lib.rs index 021ae6fd..e57377f5 100644 --- a/crates/pvm-storage/src/lib.rs +++ b/crates/pvm-storage/src/lib.rs @@ -1,8 +1,9 @@ //! Typed storage helpers for PVM smart contracts with Solidity-compatible slot layout. //! -//! Provides [`Lazy`] for single-value storage and [`Mapping`] for key-value -//! storage, both using Solidity-compatible key derivation so tools like `cast storage` -//! and `cast index` work out of the box. +//! Provides [`Lazy`] for single-value storage, [`Mapping`] for +//! key-value storage, and [`StorageVec`] for dynamic arrays (Solidity's +//! `T[]`). All three use Solidity-compatible key/index derivation so tools +//! like `cast storage` and `cast index` work out of the box. //! //! [`Lazy`] and [`Mapping`] bind `T`/`V` to //! [`StorageEncode`](pvm_contract_types::StorageEncode) + @@ -221,6 +222,56 @@ fn clear_n_slots(host: &Host, key: &[u8; 32], n: usize) { inc_slot(&mut k); } } +// Body-base derivation for a dynamic array (`StorageVec`): +// `keccak256(pad32(slot))`. Element `i` of a full-slot single-slot `T` array +// lives at `body_base + i`; multi-slot/packed shapes scale this stride. The +// formula has no key component — unlike `Mapping`, the array's elements are +// addressed by index, not by hashed key. Matches Solidity's `T[]` layout. +fn storage_derive_body_base(host: &Host, slot_key: &[u8; 32]) -> [u8; 32] { + let mut output = [0u8; 32]; + host.hash_keccak_256(slot_key, &mut output); + output +} + +/// Add `n` to a 32-byte big-endian integer in-place, propagating carries +/// up through all 32 bytes. Used by `StorageVec` to address element `i` +/// at `body_base + i` without iterating `inc_slot` `i` times. +fn inc_slot_by(slot: &mut [u8; 32], n: u64) { + let mut carry: u64 = n; + for byte in slot.iter_mut().rev() { + if carry == 0 { + return; + } + let sum = *byte as u64 + (carry & 0xff); + *byte = sum as u8; + carry = (carry >> 8) + (sum >> 8); + } +} + +/// Read a u64 length from a storage slot's lower 8 bytes (big-endian). +/// Solidity stores array lengths as `uint256`; we cap support at `u64::MAX` +/// elements (more than enough for any real-world contract) and panic if the +/// upper 24 bytes are non-zero, which would indicate either corrupted state +/// or a length set via raw uAPI that exceeds our supported range. +fn read_len_u64(host: &Host, slot_key: &[u8; 32]) -> u64 { + let buf = storage_get_32(host, slot_key); + assert!( + buf[..24].iter().all(|&b| b == 0), + "StorageVec length exceeds u64::MAX" + ); + u64::from_be_bytes([ + buf[24], buf[25], buf[26], buf[27], buf[28], buf[29], buf[30], buf[31], + ]) +} + +/// Write a u64 length to a storage slot as a big-endian `uint256` (upper 24 +/// bytes zero). When `n == 0` the host's `set_storage_or_clear` deletes the +/// slot, matching Solidity's `delete arr.length` behaviour. +fn write_len_u64(host: &Host, slot_key: &[u8; 32], n: u64) { + let mut buf = [0u8; 32]; + buf[24..32].copy_from_slice(&n.to_be_bytes()); + storage_set_32(host, slot_key, &buf); +} // --------------------------------------------------------------------------- // StorageKey @@ -1148,6 +1199,640 @@ impl } } +// --------------------------------------------------------------------------- +// Mapping> — `mapping(K => T[])` in Solidity. +// --------------------------------------------------------------------------- + +/// Solidity supports `mapping(K => T[])` directly: the mapping derives a +/// slot `keccak256(pad32(K) ++ pad32(slot))` that holds the array length, +/// with elements at `keccak256() + i * stride` (the same layout +/// `StorageVec` produces at a top-level slot). `StorageVec` is a +/// handle rather than a value, so it can't satisfy the `V: StorageEncode + +/// StorageDecode` bound on the generic `Mapping` impl — this +/// dedicated impl gives `Mapping>` the same get/entry +/// pair the nested-`Mapping` impl provides, returning a `Ref`/`RefMut` +/// guard over an inner `StorageVec` rooted at the derived key. +impl Mapping> { + /// Read path: derive the inner `StorageVec`'s root slot and return a + /// [`Ref`] so the inner vec inherits the caller's `&self` borrow. Only + /// `&self` methods on `StorageVec` (e.g. `len`, `get`, `try_get`) + /// are reachable through it; `push`/`pop`/`set` would require `&mut + /// self` and are blocked at compile time. + pub fn get(&self, key: &K) -> Ref<'_, StorageVec> { + // SAFETY: the inner `StorageVec` is immediately wrapped in `Ref<'_, + // _>`, which only exposes `&self` methods. No bypass surface is + // widened by producing the inner handle via the `unsafe` + // constructor here. + Ref::new(unsafe { StorageVec::new(self.root.derive(&self.host, key), self.host.clone()) }) + } + + /// Write path: derive the inner `StorageVec`'s root slot and return a + /// [`RefMut`] tied to the caller's `&mut self` borrow. The full + /// mutating API on `StorageVec` (`push`, `pop`, `set`, `clear`) is + /// reachable through the returned guard. + pub fn entry(&mut self, key: &K) -> RefMut<'_, StorageVec> { + // SAFETY: `entry` requires `&mut self`. The caller already holds + // mutating access through the parent borrow; the inner handle just + // forwards that capability. + RefMut::new(unsafe { + StorageVec::new(self.root.derive(&self.host, key), self.host.clone()) + }) + } +} + +// --------------------------------------------------------------------------- +// StorageVec — dynamic array with Solidity-compatible storage layout. +// --------------------------------------------------------------------------- + +/// A dynamic array backed by on-chain storage, matching Solidity's `T[]` +/// storage layout byte-for-byte. +/// +/// The element count lives at the root slot encoded as `uint256` +/// (big-endian). Element `i`'s slot is `keccak256(pad32(slot)) + stride(i)`, +/// where the stride depends on `T`'s shape: +/// - sub-word `T` (`PACKED_BYTES < 32`): `stride(i) = i / per_slot`, where +/// `per_slot = 32 / PACKED_BYTES` (multiple elements share a slot). +/// - single-slot `T` (`PACKED_BYTES == 32, STORAGE_SLOTS == 1`): +/// `stride(i) = i` (one slot per element). +/// - multi-slot static `T` (`STORAGE_SLOTS > 1`): +/// `stride(i) = i * STORAGE_SLOTS` (each element walks `STORAGE_SLOTS` +/// consecutive slots). +/// +/// `StorageVec` corresponds to Solidity's `uint8[]` (one byte per +/// element, 32 elements per slot) — **distinct from** +/// [`Bytes`](pvm_contract_types::Bytes), which models Solidity's `bytes` type +/// (inline header or spilled body). Use `Bytes` when you need `bytes`-shaped +/// storage; use `StorageVec` when you need a `uint8[]` array. +/// +/// # Cross-check vs Stylus +/// +/// API surface mirrors `stylus-sdk::storage::vec::StorageVec`. Notable +/// differences from Stylus: +/// - `get(i)` / `pop()` return `T` by value (Stylus returns a guarded handle +/// because its `T` is itself a storage handle, not a value). +/// - `set(i, &value)` is the direct write analogue of Stylus's +/// `setter(i)`. There's no per-element handle on flat `StorageVec` — +/// handles only appear on the nested impl (`StorageVec>`) +/// where `entry(i)` returns a `RefMut<'_, StorageVec>`. +/// - `pop()` zeros the freed slot only when the freed element was the first +/// packed element in its slot (same gas-optimal policy as Stylus / solc). +/// For full-slot elements, every pop frees a full slot. +/// +/// # Element shapes supported +/// +/// All `T: StorageEncode + StorageDecode` with `T::STORAGE_SLOTS <= +/// MAX_STATIC_SLOTS`. The implementation dispatches on `T`'s properties: +/// +/// - **Sub-word multi-pack** (`T::PACKED_BYTES < 32`): elements share a +/// 32-byte slot, `per_slot = 32 / PACKED_BYTES` elements per slot, packed +/// right-aligned (solc-compatible). Covers `uint8`..`uint128`, +/// `int8`..`int128`, `bool`, `Address` (`per_slot = 1`), and `[u8; N]` for +/// `N < 32`. `set` does read-modify-write to preserve neighbours; `pop` +/// clears the whole slot only when the freed element was the first one in +/// its slot. +/// - **Single-slot full-word** (`STORAGE_SLOTS == 1, PACKED_BYTES == 32`): +/// one slot per element, fast path with no RMW. Covers `U256`, `I256`, +/// `[u8; 32]` (i.e. `bytes32`), `[T; N]` whose total bytes fit in one +/// slot, and single-slot derived structs. +/// - **Multi-slot static** (`STORAGE_SLOTS > 1, !HAS_DYNAMIC_BODY`): +/// stride of `STORAGE_SLOTS` slots per element. Covers tuples, fixed +/// arrays `[T; N]` that span >1 slot (e.g. `[U256; 3]`, `[u32; 9]`), and +/// derived structs that span 2..=8 slots. +/// - **Dynamic-body** (`HAS_DYNAMIC_BODY`): each element gets its own +/// inline/spilled layout — header lives in the element's slot, spilled +/// body at `keccak256(header_slot) + i`. Covers `String` and `Bytes`. +/// +/// Nested arrays (`StorageVec>`, i.e. Solidity's `T[][]`) +/// are supported via the dedicated nested impl block below. +pub struct StorageVec { + root: StorageKey, + base: core::cell::OnceCell<[u8; 32]>, + host: Host, + _marker: PhantomData, +} + +impl StorageVec { + /// Compile-time shape validation. Referencing `_SHAPE_CHECK` in every + /// public method forces the const evaluator to run the check at each + /// monomorphization — same pattern as `Lazy::_SIZE_CHECK`. + const _SHAPE_CHECK: () = { + assert!( + T::STORAGE_SLOTS >= 1, + "StorageVec: T::STORAGE_SLOTS must be positive" + ); + assert!( + T::STORAGE_SLOTS <= MAX_STATIC_SLOTS, + "StorageVec: T::STORAGE_SLOTS exceeds MAX_STATIC_SLOTS. \ + Raise MAX_STATIC_SLOTS or use a dynamic-body type (String, Bytes)." + ); + // Sub-word multi-pack types always occupy a single slot. solc has no + // notion of "multi-slot sub-word" — every sub-word value claims at + // most one slot. Guard against malformed `StorageEncode` impls that + // mix the two. + assert!( + T::PACKED_BYTES == 32 || T::STORAGE_SLOTS == 1, + "StorageVec: sub-word T (PACKED_BYTES < 32) must satisfy STORAGE_SLOTS == 1" + ); + }; + + /// Create a new `StorageVec` rooted at the given storage key. + /// + /// # Safety + /// + /// Same safety contract as [`Lazy::new`] and [`Mapping::new`]. Direct + /// construction outside macro-generated code lets a `&self` (view) + /// method reconstruct a writable handle and bypass the borrow-check + /// view gate. Use [`StorageComponent::new_at`] from macro expansion; + /// reach for this constructor only when an arbitrary `StorageKey` is + /// required. Contract crates that want belt-and-braces enforcement + /// should add `#![forbid(unsafe_code)]` at the crate root. + pub unsafe fn new(root: StorageKey, host: Host) -> Self { + let () = Self::_SHAPE_CHECK; + StorageVec { + root, + base: core::cell::OnceCell::new(), + host, + _marker: PhantomData, + } + } + + /// Lazily compute and cache the body base `keccak256(pad32(slot))`. + /// View methods that touch only the length (`len`, `is_empty`) skip + /// this — only element accessors trigger the keccak. Matches Stylus's + /// `OnceCell` body-base caching. + fn body_base(&self) -> &[u8; 32] { + self.base + .get_or_init(|| storage_derive_body_base(&self.host, self.root.as_bytes())) + } + + /// Elements per storage slot for sub-word packing. Always `1` for + /// full-slot `T` (`PACKED_BYTES == 32`); for sub-word `T` returns + /// `32 / PACKED_BYTES` (e.g. `4` for `u64`, `8` for `u32`, `32` for `u8`). + const fn per_slot() -> u64 { + if T::PACKED_BYTES == 32 { + 1 + } else { + (32 / T::PACKED_BYTES) as u64 + } + } + + /// Slot index (offset from `body_base`) for element `i`. + /// - Sub-word: `i / per_slot` (multiple elements share a slot) + /// - Multi-slot static: `i * STORAGE_SLOTS` (stride) + /// - Single-slot full-word / dynamic-body header: `i` + fn slot_index_for(i: u64) -> u64 { + if T::PACKED_BYTES < 32 { + i / Self::per_slot() + } else if T::STORAGE_SLOTS > 1 { + // Multi-slot static. Dynamic-body always has STORAGE_SLOTS == 1 + // (one header slot per element; bodies derive elsewhere). + i * (T::STORAGE_SLOTS as u64) + } else { + i + } + } + + /// Byte offset within slot for sub-word element `i`. Solc places the + /// element at index 0 right-aligned (lowest within the slot), so: + /// `offset = 32 - PACKED_BYTES * (within + 1)`. + /// + /// Only meaningful when `T::PACKED_BYTES < 32`. + fn within_slot_offset(i: u64) -> usize { + let within = (i % Self::per_slot()) as usize; + 32 - T::PACKED_BYTES * (within + 1) + } + + /// Storage key for element `i`'s base slot (the element's slot for + /// sub-word/single-slot/dynamic-header, or the first of N slots for + /// multi-slot static). + fn element_slot(&self, i: u64) -> [u8; 32] { + let mut key = *self.body_base(); + inc_slot_by(&mut key, Self::slot_index_for(i)); + key + } + + /// Return the number of elements. + pub fn len(&self) -> u64 { + let () = Self::_SHAPE_CHECK; + read_len_u64(&self.host, self.root.as_bytes()) + } + + /// Return `true` if the array contains no elements. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Read the element at `index`. + /// + /// # Panics + /// + /// Panics if `index >= len()` (matches solc's `Panic(0x32)` revert for + /// out-of-bounds array access). + pub fn get(&self, index: u64) -> T { + let () = Self::_SHAPE_CHECK; + let len = self.len(); + assert!( + index < len, + "StorageVec::get: index {} out of bounds (len = {})", + index, + len + ); + self.read_at(index) + } + + /// Read the element at `index`, returning `None` if out of bounds. + pub fn try_get(&self, index: u64) -> Option { + let () = Self::_SHAPE_CHECK; + if index >= self.len() { + return None; + } + Some(self.read_at(index)) + } + + /// Element read, dispatched on `T`'s shape. Caller is responsible for + /// the bounds check. + fn read_at(&self, i: u64) -> T { + let key = self.element_slot(i); + if T::HAS_DYNAMIC_BODY { + // One header slot per element; the body (if spilled) lives at + // `keccak256(header_slot) + j`. Delegate to T's own dyn-body path. + T::read_from_storage::(&self.host, &key) + } else if T::PACKED_BYTES < 32 { + // Sub-word multi-pack: one shared slot, unpack at byte offset. + let slot = storage_get_32(&self.host, &key); + T::__unpack_from_dispatched(&slot, Self::within_slot_offset(i)) + } else if T::STORAGE_SLOTS == 1 { + // Single-slot full-word fast path. + T::from_slots(&[storage_get_32(&self.host, &key)]) + } else { + // Multi-slot static: read STORAGE_SLOTS consecutive slots. + let mut slots = [[0u8; 32]; MAX_STATIC_SLOTS]; + let n = T::STORAGE_SLOTS; + read_slots(&self.host, &key, &mut slots[..n]); + T::from_slots(&slots[..n]) + } + } + + /// Write the element at `index`. + /// + /// # Panics + /// + /// Panics if `index >= len()`. Use [`push`](Self::push) to extend the + /// array. + pub fn set(&mut self, index: u64, value: &T) { + let () = Self::_SHAPE_CHECK; + let len = self.len(); + assert!( + index < len, + "StorageVec::set: index {} out of bounds (len = {})", + index, + len + ); + self.write_at(index, value); + } + + /// Element write, dispatched on `T`'s shape. `&mut self` so the + /// borrow-checker enforces that mutation flows through a `&mut` view + /// of the vec (defence-in-depth — the public callers already require + /// `&mut self`). + fn write_at(&mut self, i: u64, value: &T) { + let key = self.element_slot(i); + if T::HAS_DYNAMIC_BODY { + // T's own write_to_storage handles header + body (inline or spilled). + value.write_to_storage(&self.host, &key); + } else if T::PACKED_BYTES < 32 { + // Sub-word: read-modify-write to preserve sibling elements in the + // same slot. Even when `i % per_slot == 0` (a fresh slot from + // push's perspective), RMW is safe and avoids a fast-path that + // could leak stale data if external state ever pre-existed. + let mut buf = storage_get_32(&self.host, &key); + let offset = Self::within_slot_offset(i); + buf[offset..offset + T::PACKED_BYTES].fill(0); + value.__pack_into_dispatched(&mut buf, offset); + storage_set_32(&self.host, &key, &buf); + } else if T::STORAGE_SLOTS == 1 { + // Single-slot full-word fast path. + let mut buf = [0u8; 32]; + value.encode_slot(0, &mut buf); + storage_set_32(&self.host, &key, &buf); + } else { + // Multi-slot static: stream-encode slot-by-slot. + write_value(&self.host, &key, value); + } + } + + /// Append an element. Writes the value at the tail position, then + /// increments the length. + /// + /// # Panics + /// + /// Panics if the length would overflow `u64::MAX` (practically + /// unreachable — the storage budget is exhausted long before). + pub fn push(&mut self, value: &T) { + let () = Self::_SHAPE_CHECK; + let len = self.len(); + let new_len = len + .checked_add(1) + .expect("StorageVec::push: length overflow"); + self.write_at(len, value); + write_len_u64(&self.host, self.root.as_bytes(), new_len); + } + + /// Remove and return the last element, or `None` if the array is empty. + /// + /// The freed slot(s) are cleared (zero write through + /// `set_storage_or_clear`) so the SSTORE-to-zero gas refund applies — + /// matching Solidity's `pop()`. + /// + /// For **sub-word** elements the whole slot is cleared only when the + /// freed element was the first one packed in its slot (`within == 0`); + /// otherwise a read-modify-write zeros just that element's byte range, + /// preserving the remaining packed siblings. For **multi-slot static** + /// `T` every pop clears `STORAGE_SLOTS` slots. For **dynamic-body** T + /// the header slot and any spilled body chunks are cleared via + /// `T::clear_storage`. + pub fn pop(&mut self) -> Option { + let () = Self::_SHAPE_CHECK; + let len = self.len(); + if len == 0 { + return None; + } + let new_len = len - 1; + let value = self.read_at(new_len); + self.clear_at(new_len); + write_len_u64(&self.host, self.root.as_bytes(), new_len); + Some(value) + } + + /// Clear the storage occupied by element `i`. Dispatches on shape; see + /// [`pop`](Self::pop) for the gas-refund policy. `&mut self` so a + /// future `&self` method can't accidentally invoke this private + /// mutating helper. + fn clear_at(&mut self, i: u64) { + let key = self.element_slot(i); + if T::HAS_DYNAMIC_BODY { + // Tears down inline header + any spilled body chunks. + ::clear_storage(&self.host, &key, T::STORAGE_SLOTS); + } else if T::PACKED_BYTES < 32 { + // First element in a slot has no surviving siblings (the higher + // within indices were popped first), so clear the whole slot. + // Otherwise RMW zero only this element's bytes. + let within = i % Self::per_slot(); + if within == 0 { + storage_set_32(&self.host, &key, &[0u8; 32]); + } else { + let mut buf = storage_get_32(&self.host, &key); + let offset = Self::within_slot_offset(i); + buf[offset..offset + T::PACKED_BYTES].fill(0); + storage_set_32(&self.host, &key, &buf); + } + } else if T::STORAGE_SLOTS == 1 { + storage_set_32(&self.host, &key, &[0u8; 32]); + } else { + clear_n_slots(&self.host, &key, T::STORAGE_SLOTS); + } + } + + /// Remove every element and reset length to zero. + /// + /// **O(n) gas** — every element's storage is cleared. For arrays with + /// many entries, consider draining via repeated `pop()` across multiple + /// transactions instead. Matches solc's `delete arr` for dynamic arrays. + pub fn clear(&mut self) { + let () = Self::_SHAPE_CHECK; + let len = self.len(); + if len > 0 { + if T::HAS_DYNAMIC_BODY { + // Each element may have spilled body chunks; delegate per-element. + for i in 0..len { + self.clear_at(i); + } + } else if T::PACKED_BYTES < 32 { + // Clear every body slot the array touched: ceil(len / per_slot). + let per = Self::per_slot(); + let total_slots = len.div_ceil(per); + let mut key = *self.body_base(); + for _ in 0..total_slots { + storage_set_32(&self.host, &key, &[0u8; 32]); + inc_slot(&mut key); + } + } else { + // Single-slot full-word or multi-slot static: clear + // `len * STORAGE_SLOTS` consecutive slots. + let total_slots = len * (T::STORAGE_SLOTS as u64); + let mut key = *self.body_base(); + for _ in 0..total_slots { + storage_set_32(&self.host, &key, &[0u8; 32]); + inc_slot(&mut key); + } + } + } + storage_set_32(&self.host, self.root.as_bytes(), &[0u8; 32]); + } +} + +impl StorageComponent for StorageVec { + /// One root slot for the length header. Elements live at + /// `keccak256(slot) + i` and consume no additional contract-layout slots. + const SLOTS: u64 = 1; + + /// Never packs with neighbours — the length header always claims a full + /// slot. Matches `Mapping`'s `PACKED_BYTES = 32` and solc's storage + /// layout for dynamic arrays. + const PACKED_BYTES: usize = 32; + + fn new_at(slot: u64, offset: u8, host: Host) -> Self { + debug_assert_eq!( + offset, 0, + "StorageVec always full-slot; offset must be 0" + ); + let _ = offset; + // SAFETY: macro-only safe entry point. See `Lazy::new_at` for the + // full justification — bypass would require direct user calls to + // `StorageVec::new`, which is what the `unsafe` keyword marks. + unsafe { StorageVec::new(StorageKey::from_slot(slot), host) } + } +} + +// --------------------------------------------------------------------------- +// StorageVec> — `T[][]` in Solidity. +// --------------------------------------------------------------------------- + +/// Solidity's `T[][]` lays out the outer length at the parent slot, each +/// inner array's "root" (length slot) at `keccak256(parent_root) + i`, and +/// then the inner body at `keccak256(inner_root) + j`. Stylus exposes the +/// same shape via nested `StorageVec`; this impl matches it. +/// +/// `StorageVec` is a handle (not a `StorageEncode` value), so the +/// generic `StorageVec::new` won't construct a `StorageVec>` +/// — its bound requires the inner type to be a value. This block provides +/// a dedicated `new_nested` constructor plus `outer_len` / `get` / `entry` / +/// `push_empty` / `pop` / `clear` for managing the outer-and-inner pair. +impl StorageVec> { + /// Construct a nested storage vec rooted at `root`. + /// + /// # Safety + /// Same safety contract as [`StorageVec::new`]. Direct construction + /// outside macro-generated code lets a `&self` (view) method reconstruct + /// a writable handle and bypass the borrow-check view gate. + pub unsafe fn new_nested(root: StorageKey, host: Host) -> Self { + StorageVec { + root, + base: core::cell::OnceCell::new(), + host, + _marker: PhantomData, + } + } + + /// Number of inner arrays appended via [`push_empty`](Self::push_empty). + pub fn outer_len(&self) -> u64 { + read_len_u64(&self.host, self.root.as_bytes()) + } + + /// `true` if no inner arrays have been appended. + pub fn is_empty(&self) -> bool { + self.outer_len() == 0 + } + + /// Read-only view of the inner array at index `i`. The returned [`Ref`] + /// only exposes `&self` methods on `StorageVec` (`len`, `get`, + /// `try_get`); mutating ops are blocked by the borrow. + /// + /// # Panics + /// + /// Panics if `i >= outer_len()` — matches solc's `Panic(0x32)` for + /// out-of-bounds array access and stays consistent with flat + /// [`StorageVec::get`]. + pub fn get(&self, i: u64) -> Ref<'_, StorageVec> { + let len = self.outer_len(); + assert!( + i < len, + "StorageVec::get: index {} out of bounds (outer_len = {})", + i, + len + ); + let inner_root = self.inner_root(i); + // SAFETY: the inner handle is immediately wrapped in `Ref<'_, _>`, + // which forwards only `&self` methods. The parent `&self` borrow + // gates mutation: `push`/`pop`/`set` require `&mut self` and are + // unreachable through a `Ref`. + Ref::new(unsafe { StorageVec::::new(StorageKey(inner_root), self.host.clone()) }) + } + + /// Mutable handle to the inner array at index `i`. Permits the full + /// mutating API on `StorageVec` (`push`, `pop`, `set`, `clear`). + /// + /// # Panics + /// + /// Panics if `i >= outer_len()` — solc rejects writes through + /// out-of-bounds indices with `Panic(0x32)`. Grow the outer first via + /// [`push_empty`](Self::push_empty). + pub fn entry(&mut self, i: u64) -> RefMut<'_, StorageVec> { + let len = self.outer_len(); + assert!( + i < len, + "StorageVec::entry: index {} out of bounds (outer_len = {})", + i, + len + ); + let inner_root = self.inner_root(i); + // SAFETY: `&mut self` proves mutating access through the parent + // borrow; the inner handle just forwards that capability. + RefMut::new(unsafe { StorageVec::::new(StorageKey(inner_root), self.host.clone()) }) + } + + /// Append a new (empty) inner array. Increments outer length by 1; the + /// inner array starts with length 0 and no element slots written. + /// + /// # Panics + /// Panics if the outer length would overflow `u64::MAX`. + pub fn push_empty(&mut self) { + let len = self.outer_len(); + let new_len = len + .checked_add(1) + .expect("StorageVec::push_empty: length overflow"); + write_len_u64(&self.host, self.root.as_bytes(), new_len); + } + + /// Remove the last inner array, recursively clearing its storage. + /// + /// Matches solc's `T[][].pop()`, which destroys the popped inner array + /// (its length slot and every element slot are zeroed, allowing the + /// SSTORE-to-zero gas refund). Returns `true` if an inner array was + /// popped, `false` if the outer was already empty. + /// + /// **O(inner_len) gas** — every element of the popped inner is cleared. + pub fn pop(&mut self) -> bool { + let len = self.outer_len(); + if len == 0 { + return false; + } + let new_len = len - 1; + let inner_root = self.inner_root(new_len); + // SAFETY: short-lived handle used purely to dispatch `clear()` over + // the inner array's body slots. Same justification as `entry`: the + // parent `&mut self` borrow gates the operation. + let mut inner = unsafe { StorageVec::::new(StorageKey(inner_root), self.host.clone()) }; + inner.clear(); + write_len_u64(&self.host, self.root.as_bytes(), new_len); + true + } + + /// Remove every inner array and reset outer length to zero. + /// + /// Matches solc's `delete matrix` on a `T[][]`: recursively clears each + /// inner array's length slot and body slots, then zeroes the outer + /// length slot. + /// + /// **O(total elements) gas** — every element across every inner is + /// cleared. For large matrices, drain via repeated `pop()` across + /// multiple transactions instead. + pub fn clear(&mut self) { + let len = self.outer_len(); + for i in 0..len { + let inner_root = self.inner_root(i); + // SAFETY: see `pop` — short-lived handle, parent borrow gates + // the mutation. + let mut inner = + unsafe { StorageVec::::new(StorageKey(inner_root), self.host.clone()) }; + inner.clear(); + } + storage_set_32(&self.host, self.root.as_bytes(), &[0u8; 32]); + } + + /// Inner array's root slot (its length slot). Lives at + /// `keccak256(pad32(outer_root)) + i`. + fn inner_root(&self, i: u64) -> [u8; 32] { + let body = self + .base + .get_or_init(|| storage_derive_body_base(&self.host, self.root.as_bytes())); + let mut key = *body; + inc_slot_by(&mut key, i); + key + } +} + +/// `StorageComponent` for the nested case so `#[storage]` / `#[contract]` +/// can place a `StorageVec>` field on a contract struct +/// without users having to opt into `unsafe`. Mirrors the flat +/// `StorageVec` impl (one root slot, full-slot, never packs). +impl StorageComponent for StorageVec> { + const SLOTS: u64 = 1; + const PACKED_BYTES: usize = 32; + + fn new_at(slot: u64, offset: u8, host: Host) -> Self { + debug_assert_eq!( + offset, 0, + "StorageVec> always full-slot; offset must be 0" + ); + let _ = offset; + // SAFETY: macro-only safe entry point. Same justification as the flat + // `StorageVec` `new_at` — bypass would require direct user calls + // to `StorageVec::new_nested`, which is marked `unsafe`. + unsafe { StorageVec::new_nested(StorageKey::from_slot(slot), host) } + } +} + // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/crates/pvm-storage/src/tests.rs b/crates/pvm-storage/src/tests.rs index 59b7132c..bf529b61 100644 --- a/crates/pvm-storage/src/tests.rs +++ b/crates/pvm-storage/src/tests.rs @@ -1899,3 +1899,1140 @@ fn lazy_string_native_layout_matches_solc_short() { assert!(header[5..31].iter().all(|&b| b == 0)); assert_eq!(header[31], 5 * 2); } + +// --- StorageVec --- + +#[test] +fn storage_vec_empty_defaults() { + let v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + assert_eq!(v.len(), 0); + assert!(v.is_empty()); + assert_eq!(v.try_get(0), None); +} + +#[test] +fn storage_vec_push_pop_u256() { + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + v.push(&U256::from(10u64)); + v.push(&U256::from(20u64)); + v.push(&U256::from(30u64)); + assert_eq!(v.len(), 3); + assert_eq!(v.get(0), U256::from(10u64)); + assert_eq!(v.get(1), U256::from(20u64)); + assert_eq!(v.get(2), U256::from(30u64)); + + assert_eq!(v.pop(), Some(U256::from(30u64))); + assert_eq!(v.pop(), Some(U256::from(20u64))); + assert_eq!(v.len(), 1); + assert_eq!(v.pop(), Some(U256::from(10u64))); + assert_eq!(v.len(), 0); + assert_eq!(v.pop(), None); +} + +#[test] +fn storage_vec_push_pop_address() { + let mut v = unsafe { StorageVec::
::new(StorageKey::from_slot(7), h()) }; + let a = Address([0xAA; 20]); + let b = Address([0xBB; 20]); + v.push(&a); + v.push(&b); + assert_eq!(v.get(0), a); + assert_eq!(v.get(1), b); + assert_eq!(v.pop(), Some(b)); + assert_eq!(v.pop(), Some(a)); + assert_eq!(v.pop(), None); +} + +#[test] +fn storage_vec_set_overwrites() { + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + v.push(&U256::from(1u64)); + v.push(&U256::from(2u64)); + v.set(0, &U256::from(100u64)); + assert_eq!(v.get(0), U256::from(100u64)); + assert_eq!(v.get(1), U256::from(2u64)); +} + +#[test] +fn storage_vec_try_get_oob_returns_none() { + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + v.push(&U256::from(42u64)); + assert_eq!(v.try_get(0), Some(U256::from(42u64))); + assert_eq!(v.try_get(1), None); + assert_eq!(v.try_get(u64::MAX), None); +} + +#[test] +#[should_panic(expected = "out of bounds")] +fn storage_vec_get_oob_panics() { + let v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + let _ = v.get(0); +} + +#[test] +#[should_panic(expected = "out of bounds")] +fn storage_vec_set_oob_panics() { + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + v.set(0, &U256::from(1u64)); +} + +#[test] +fn storage_vec_clear_resets_everything() { + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + let host = v.host.clone(); + let root = v.root; + v.push(&U256::from(1u64)); + v.push(&U256::from(2u64)); + v.push(&U256::from(3u64)); + + // Capture the body base via the keccak path the same way the impl does. + let mut body = [0u8; 32]; + host.hash_keccak_256(root.as_bytes(), &mut body); + + v.clear(); + assert_eq!(v.len(), 0); + assert!(v.is_empty()); + + // Length slot is fully zeroed. + assert_eq!(storage_get_32(&host, root.as_bytes()), [0u8; 32]); + + // Each former element slot is also zeroed (no stale data left behind). + let mut k = body; + for _ in 0..3 { + assert_eq!(storage_get_32(&host, &k), [0u8; 32]); + inc_slot(&mut k); + } +} + +#[test] +fn storage_vec_pop_zeros_freed_slot() { + // Pop must clear the freed slot so a later push doesn't see stale + // bytes — and so the SSTORE-to-zero refund applies (matches solc). + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + let host = v.host.clone(); + v.push(&U256::from(0xABCDu64)); + + // The element lives at keccak256(slot_0) + 0 = keccak256(slot_0). + let mut body = [0u8; 32]; + host.hash_keccak_256(v.root.as_bytes(), &mut body); + assert_ne!(storage_get_32(&host, &body), [0u8; 32]); + + let _ = v.pop(); + assert_eq!( + storage_get_32(&host, &body), + [0u8; 32], + "pop must zero the freed slot", + ); +} + +#[test] +fn storage_vec_layout_matches_solidity_uint256_array() { + // Solidity layout for `uint256[] a;` at slot 5: + // slot[5] = length + // element i = slot[ keccak256(pad32(5)) + i ] + // + // Cross-check by writing through StorageVec and reading raw slots. + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(5), h()) }; + let host = v.host.clone(); + v.push(&U256::from(0xAAu64)); + v.push(&U256::from(0xBBu64)); + v.push(&U256::from(0xCCu64)); + + // Length lives at the root slot, big-endian uint256. + let len_slot = storage_get_32(&host, v.root.as_bytes()); + assert_eq!(len_slot[31], 3); + assert!(len_slot[..31].iter().all(|&b| b == 0)); + + // Body base is keccak256(pad32(5)). + let mut body = [0u8; 32]; + host.hash_keccak_256(v.root.as_bytes(), &mut body); + + // Elements at body + 0, body + 1, body + 2. + let e0 = storage_get_32(&host, &body); + assert_eq!(e0[31], 0xAA); + let mut k = body; + inc_slot(&mut k); + let e1 = storage_get_32(&host, &k); + assert_eq!(e1[31], 0xBB); + inc_slot(&mut k); + let e2 = storage_get_32(&host, &k); + assert_eq!(e2[31], 0xCC); +} + +#[test] +fn storage_vec_body_base_independent_of_other_slots() { + // Two arrays at different slots must not collide. + let host = h(); + let mut v0 = unsafe { StorageVec::::new(StorageKey::from_slot(0), host.clone()) }; + let mut v1 = unsafe { StorageVec::::new(StorageKey::from_slot(1), host.clone()) }; + v0.push(&U256::from(111u64)); + v1.push(&U256::from(222u64)); + assert_eq!(v0.get(0), U256::from(111u64)); + assert_eq!(v1.get(0), U256::from(222u64)); + assert_eq!(v0.len(), 1); + assert_eq!(v1.len(), 1); +} + +#[test] +fn storage_vec_get_after_set_reuses_body_base_cache() { + // Smoke test the OnceCell: many element accesses on the same handle. + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + for i in 0..16u64 { + v.push(&U256::from(i + 100)); + } + for i in 0..16u64 { + assert_eq!(v.get(i), U256::from(i + 100)); + } +} + +#[test] +fn storage_vec_storage_component_metadata() { + assert_eq!( as StorageComponent>::SLOTS, 1); + assert_eq!( as StorageComponent>::PACKED_BYTES, 32); + assert_eq!( as StorageComponent>::SLOTS, 1); + assert_eq!( as StorageComponent>::PACKED_BYTES, 32); +} + +#[test] +fn storage_vec_new_at_matches_unsafe_new() { + // `StorageComponent::new_at` is the macro-emitted safe path; assert + // it produces a handle indistinguishable from `unsafe { new(...) }`. + let host = h(); + let mut a = unsafe { StorageVec::::new(StorageKey::from_slot(3), host.clone()) }; + let mut b = as StorageComponent>::new_at(3, 0, host); + a.push(&U256::from(7u64)); + assert_eq!(b.get(0), U256::from(7u64)); + b.push(&U256::from(9u64)); + assert_eq!(a.get(1), U256::from(9u64)); +} + +// --- StorageVec for sub-word T (uint32[], uint64[], bool[], etc.) --- + +#[test] +fn storage_vec_subword_u32_roundtrip() { + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + for i in 0u32..20 { + v.push(&(i * 100)); + } + assert_eq!(v.len(), 20); + for i in 0u32..20 { + assert_eq!(v.get(i as u64), i * 100); + } + // Overwrite a middle element via `set`. + v.set(5, &9999u32); + assert_eq!(v.get(5), 9999u32); + // Neighbours in the same packed slot are preserved. + assert_eq!(v.get(4), 400u32); + assert_eq!(v.get(6), 600u32); +} + +#[test] +fn storage_vec_subword_u32_layout_matches_solc() { + // solc `uint32[]` at slot S: + // length at S + // element i at slot keccak(pad32(S)) + (i / 8), bytes 32 - 4*(within+1).. + // where within = i % 8 (per_slot = 32 / 4 = 8) + let host = h(); + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(2), host.clone()) }; + for i in 0u32..10 { + v.push(&(0xC0DE_0000 | i)); + } + let body = storage_derive_body_base(&host, StorageKey::from_slot(2).as_bytes()); + + // Slot 0 of body holds elements 0..8 (right-aligned: 0 lowest, 7 highest). + let s0 = storage_get_32(&host, &body); + for within in 0u32..8 { + let off = 32 - 4 * (within as usize + 1); + let mut bytes = [0u8; 4]; + bytes.copy_from_slice(&s0[off..off + 4]); + assert_eq!(u32::from_be_bytes(bytes), 0xC0DE_0000 | within); + } + + // Slot 1 of body holds elements 8..10. + let mut s1_key = body; + inc_slot(&mut s1_key); + let s1 = storage_get_32(&host, &s1_key); + // Element 8 lives at within=0 (bytes 28..32). + assert_eq!( + u32::from_be_bytes([s1[28], s1[29], s1[30], s1[31]]), + 0xC0DE_0008 + ); + // Element 9 at within=1 (bytes 24..28). + assert_eq!( + u32::from_be_bytes([s1[24], s1[25], s1[26], s1[27]]), + 0xC0DE_0009 + ); + // No data above element 9 (high bytes of slot 1 are zero). + assert_eq!(&s1[..24], &[0u8; 24]); +} + +#[test] +fn storage_vec_subword_bool_packs_32_per_slot() { + // `bool` has PACKED_BYTES = 1, so 32 bools fit per slot. + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + for i in 0..40 { + v.push(&(i % 3 == 0)); + } + for i in 0..40u64 { + assert_eq!(v.get(i), (i % 3) == 0); + } +} + +#[test] +fn storage_vec_subword_pop_preserves_packed_siblings() { + // After pushing 5 u32s and popping the last one, the remaining 4 must + // still be readable — pop's RMW must zero only the freed bytes. + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + for i in 0u32..5 { + v.push(&(0x1000 + i)); + } + assert_eq!(v.pop(), Some(0x1004)); + assert_eq!(v.len(), 4); + for i in 0u32..4 { + assert_eq!(v.get(i as u64), 0x1000 + i); + } +} + +#[test] +fn storage_vec_subword_pop_clears_slot_when_freeing_first_in_slot() { + // u32 with per_slot = 8. Push 9 elements (slot 0: indices 0..8, slot 1: + // index 8). Popping index 8 should clear slot 1 entirely — verify via + // raw read that the slot is zero. + let host = h(); + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), host.clone()) }; + for i in 0u32..9 { + v.push(&(i + 1)); + } + let body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let mut slot1_key = body; + inc_slot(&mut slot1_key); + // Before pop: slot 1 has element 8 at bytes 28..32. + let before = storage_get_32(&host, &slot1_key); + assert_eq!( + u32::from_be_bytes([before[28], before[29], before[30], before[31]]), + 9 + ); + + assert_eq!(v.pop(), Some(9)); + + // After pop: slot 1 is fully cleared (it had only one element). + let after = storage_get_32(&host, &slot1_key); + assert_eq!(after, [0u8; 32]); + + // And slot 0 still has all 8 surviving elements packed. + let slot0 = storage_get_32(&host, &body); + assert_ne!(slot0, [0u8; 32]); +} + +#[test] +fn storage_vec_subword_clear_resets_all_body_slots() { + let host = h(); + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), host.clone()) }; + for i in 0u32..17 { + // 17 u32s span 3 slots (8 + 8 + 1). + v.push(&(i + 1)); + } + v.clear(); + assert_eq!(v.len(), 0); + + // Verify all 3 body slots are zero. + let body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let mut key = body; + for _ in 0..3 { + assert_eq!(storage_get_32(&host, &key), [0u8; 32]); + inc_slot(&mut key); + } +} + +#[test] +fn storage_vec_subword_storage_component_metadata() { + // Sub-word StorageVecs report the same metadata as full-word ones: + // one root slot, never packs with neighbours. + assert_eq!( as StorageComponent>::SLOTS, 1); + assert_eq!( as StorageComponent>::PACKED_BYTES, 32); + assert_eq!( as StorageComponent>::SLOTS, 1); + assert_eq!( as StorageComponent>::SLOTS, 1); +} + +// --- StorageVec for multi-slot static T ((U256, U256)[], etc.) --- + +#[test] +fn storage_vec_multislot_tuple_roundtrip() { + // `(U256, U256)` has STORAGE_SLOTS == 2 — each element claims two slots. + let mut v = unsafe { StorageVec::<(U256, U256)>::new(StorageKey::from_slot(0), h()) }; + let pairs: [(U256, U256); 4] = [ + (U256::from(1u64), U256::from(2u64)), + (U256::from(3u64), U256::from(4u64)), + (U256::from(5u64), U256::from(6u64)), + (U256::from(7u64), U256::from(8u64)), + ]; + for p in &pairs { + v.push(p); + } + assert_eq!(v.len(), 4); + for (i, expected) in pairs.iter().enumerate() { + assert_eq!(v.get(i as u64), *expected); + } +} + +#[test] +fn storage_vec_multislot_layout_uses_stride() { + // Verify that element i occupies slots [body_base + i*N, body_base + i*N + N). + let host = h(); + let mut v = unsafe { StorageVec::<(U256, U256)>::new(StorageKey::from_slot(5), host.clone()) }; + v.push(&(U256::from(0x1111u64), U256::from(0x2222u64))); + v.push(&(U256::from(0x3333u64), U256::from(0x4444u64))); + + let body = storage_derive_body_base(&host, StorageKey::from_slot(5).as_bytes()); + // Element 0 occupies slots body+0, body+1. + let e0_s0 = storage_get_32(&host, &body); + let mut k = body; + inc_slot(&mut k); + let e0_s1 = storage_get_32(&host, &k); + // Tuple `(a, b)` encoding: a at slot 0, b at slot 1 (both right-aligned U256s). + assert_eq!(U256::from_be_bytes(e0_s0), U256::from(0x1111u64)); + assert_eq!(U256::from_be_bytes(e0_s1), U256::from(0x2222u64)); + + // Element 1 occupies slots body+2, body+3. + inc_slot(&mut k); + let e1_s0 = storage_get_32(&host, &k); + inc_slot(&mut k); + let e1_s1 = storage_get_32(&host, &k); + assert_eq!(U256::from_be_bytes(e1_s0), U256::from(0x3333u64)); + assert_eq!(U256::from_be_bytes(e1_s1), U256::from(0x4444u64)); +} + +#[test] +fn storage_vec_multislot_set_overwrites() { + let mut v = unsafe { StorageVec::<(U256, U256)>::new(StorageKey::from_slot(0), h()) }; + v.push(&(U256::from(1u64), U256::from(2u64))); + v.push(&(U256::from(3u64), U256::from(4u64))); + v.set(0, &(U256::from(99u64), U256::from(100u64))); + assert_eq!(v.get(0), (U256::from(99u64), U256::from(100u64))); + assert_eq!(v.get(1), (U256::from(3u64), U256::from(4u64))); +} + +#[test] +fn storage_vec_multislot_pop_clears_all_slots() { + let host = h(); + let mut v = unsafe { StorageVec::<(U256, U256)>::new(StorageKey::from_slot(0), host.clone()) }; + v.push(&(U256::from(7u64), U256::from(8u64))); + v.push(&(U256::from(9u64), U256::from(10u64))); + + assert_eq!(v.pop(), Some((U256::from(9u64), U256::from(10u64)))); + assert_eq!(v.len(), 1); + + // Verify the freed element's slots are both zero. + let body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let mut k = body; + inc_slot_by(&mut k, 2); // element 1 starts at body + 2 + assert_eq!(storage_get_32(&host, &k), [0u8; 32]); + inc_slot(&mut k); + assert_eq!(storage_get_32(&host, &k), [0u8; 32]); +} + +// --- StorageVec for fixed-size arrays [T; N] (T != u8) --- + +#[test] +fn storage_vec_fixed_array_u32_roundtrip() { + // `uint32[4]` fits in one slot (4*4 = 16 bytes); the array's STORAGE_SLOTS + // is 1, so each StorageVec element claims exactly one body slot. + let mut v = unsafe { StorageVec::<[u32; 4]>::new(StorageKey::from_slot(0), h()) }; + v.push(&[10, 20, 30, 40]); + v.push(&[1, 2, 3, 4]); + assert_eq!(v.len(), 2); + assert_eq!(v.get(0), [10, 20, 30, 40]); + assert_eq!(v.get(1), [1, 2, 3, 4]); +} + +#[test] +fn storage_vec_fixed_array_u32_boundary_crossing() { + // `uint32[9]` spans 2 slots (8 elements + 1 spillover). Verify byte + // placement matches solc: + // slot 0: elements 0..8 right-aligned (element i at bytes 28-4i .. 32-4i) + // slot 1: element 8 at bytes 28..32, rest zero + let host = h(); + let mut v = unsafe { StorageVec::<[u32; 9]>::new(StorageKey::from_slot(0), host.clone()) }; + let arr: [u32; 9] = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18]; + v.push(&arr); + + let body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let slot0 = storage_get_32(&host, &body); + // element 0 right-aligned in slot 0 + assert_eq!( + u32::from_be_bytes([slot0[28], slot0[29], slot0[30], slot0[31]]), + 0x10 + ); + // element 7 at the high end of slot 0 + assert_eq!( + u32::from_be_bytes([slot0[0], slot0[1], slot0[2], slot0[3]]), + 0x17 + ); + + let mut slot1_key = body; + inc_slot(&mut slot1_key); + let slot1 = storage_get_32(&host, &slot1_key); + // element 8 right-aligned in slot 1 + assert_eq!( + u32::from_be_bytes([slot1[28], slot1[29], slot1[30], slot1[31]]), + 0x18 + ); + // high 28 bytes of slot 1 are zero + assert_eq!(&slot1[..28], &[0u8; 28]); + + // Round-trip + assert_eq!(v.get(0), arr); +} + +#[test] +fn storage_vec_fixed_array_bool_packing() { + // `bool[40]` spans 2 slots (32 + 8). Each bool is 1 byte, density 32. + let mut v = unsafe { StorageVec::<[bool; 40]>::new(StorageKey::from_slot(0), h()) }; + let mut arr = [false; 40]; + for (i, slot) in arr.iter_mut().enumerate() { + *slot = i % 3 == 0; + } + v.push(&arr); + assert_eq!(v.get(0), arr); +} + +#[test] +fn storage_vec_fixed_array_address_no_packing() { + // `address[3]` — Address is 20 bytes, density = 32/20 = 1 (no packing + // across addresses). Should consume 3 slots, one per element. + assert_eq!(<[Address; 3] as StorageEncode>::STORAGE_SLOTS, 3); + let host = h(); + let mut v = unsafe { StorageVec::<[Address; 3]>::new(StorageKey::from_slot(0), host.clone()) }; + let arr = [ + Address([0xAA; 20]), + Address([0xBB; 20]), + Address([0xCC; 20]), + ]; + v.push(&arr); + assert_eq!(v.get(0), arr); + + // Verify the 3 slots each hold one address right-aligned. + let body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let mut k = body; + for expected in arr.iter() { + let slot = storage_get_32(&host, &k); + assert_eq!(&slot[..12], &[0u8; 12]); + assert_eq!(&slot[12..32], &expected.0); + inc_slot(&mut k); + } +} + +#[test] +fn storage_vec_fixed_array_u256_full_slot() { + // `uint256[3]` — full-slot T, 3 slots per element. STORAGE_SLOTS = 3. + assert_eq!(<[U256; 3] as StorageEncode>::STORAGE_SLOTS, 3); + let mut v = unsafe { StorageVec::<[U256; 3]>::new(StorageKey::from_slot(0), h()) }; + let arr = [ + U256::from(0xAAu64), + U256::from(0xBBu64), + U256::from(0xCCu64), + ]; + v.push(&arr); + v.push(&[U256::from(1u64), U256::from(2u64), U256::from(3u64)]); + assert_eq!(v.get(0), arr); + assert_eq!( + v.get(1), + [U256::from(1u64), U256::from(2u64), U256::from(3u64)] + ); +} + +#[test] +fn storage_vec_fixed_array_pop_clears_all_slots() { + // Popping an `[u32; 9]` must clear both its slots so a later push doesn't + // see stale bytes — matches solc's `pop()` zeroing. + let host = h(); + let mut v = unsafe { StorageVec::<[u32; 9]>::new(StorageKey::from_slot(0), host.clone()) }; + v.push(&[0xFF; 9]); + + let body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let mut slot1 = body; + inc_slot(&mut slot1); + assert_ne!(storage_get_32(&host, &body), [0u8; 32]); + assert_ne!(storage_get_32(&host, &slot1), [0u8; 32]); + + let _ = v.pop(); + assert_eq!(v.len(), 0); + assert_eq!(storage_get_32(&host, &body), [0u8; 32]); + assert_eq!(storage_get_32(&host, &slot1), [0u8; 32]); +} + +#[test] +fn storage_vec_fixed_array_storage_slots_metadata() { + // Compile-time STORAGE_SLOTS for representative shapes. + assert_eq!(<[u32; 4] as StorageEncode>::STORAGE_SLOTS, 1); // fits 1 slot + assert_eq!(<[u32; 8] as StorageEncode>::STORAGE_SLOTS, 1); // exactly fills 1 slot + assert_eq!(<[u32; 9] as StorageEncode>::STORAGE_SLOTS, 2); // spills to 2 + assert_eq!(<[u64; 4] as StorageEncode>::STORAGE_SLOTS, 1); // 4*8 = 32 + assert_eq!(<[u64; 5] as StorageEncode>::STORAGE_SLOTS, 2); + assert_eq!(<[bool; 32] as StorageEncode>::STORAGE_SLOTS, 1); + assert_eq!(<[bool; 33] as StorageEncode>::STORAGE_SLOTS, 2); + assert_eq!(<[U256; 1] as StorageEncode>::STORAGE_SLOTS, 1); + assert_eq!(<[U256; 5] as StorageEncode>::STORAGE_SLOTS, 5); + assert_eq!(<[Address; 3] as StorageEncode>::STORAGE_SLOTS, 3); + // PACKED_BYTES is always 32 — arrays start a fresh slot. + assert_eq!(<[u32; 4] as StorageEncode>::PACKED_BYTES, 32); +} + +// --- StorageVec for dynamic-body T (string[], bytes[]) --- + +#[cfg(feature = "alloc")] +#[test] +fn storage_vec_dynamic_string_roundtrip() { + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + v.push(&String::from("short")); + // 32+ byte string forces the spilled-body path. + v.push(&String::from( + "this is a much longer string that exceeds 31 bytes", + )); + v.push(&String::new()); + v.push(&String::from("another")); + + assert_eq!(v.len(), 4); + assert_eq!(v.get(0), "short"); + assert_eq!( + v.get(1), + "this is a much longer string that exceeds 31 bytes" + ); + assert_eq!(v.get(2), ""); + assert_eq!(v.get(3), "another"); +} + +#[cfg(feature = "alloc")] +#[test] +fn storage_vec_dynamic_bytes_roundtrip() { + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + v.push(&Bytes(alloc::vec![0xAA, 0xBB, 0xCC])); + v.push(&Bytes(alloc::vec![0x11; 100])); // forces spill + v.push(&Bytes(alloc::vec::Vec::new())); + + assert_eq!(v.len(), 3); + assert_eq!(v.get(0).0, alloc::vec![0xAA, 0xBB, 0xCC]); + assert_eq!(v.get(1).0, alloc::vec![0x11; 100]); + assert_eq!(v.get(2).0, alloc::vec::Vec::::new()); +} + +#[cfg(feature = "alloc")] +#[test] +fn storage_vec_dynamic_string_set_replaces_inline_with_spilled() { + // Replacing a short (inline) element with a long (spilled) one must + // overwrite cleanly through `T::write_to_storage`. + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + v.push(&String::from("short")); + let long: String = str::repeat("x", 80); + v.set(0, &long); + assert_eq!(v.get(0), long); +} + +#[cfg(feature = "alloc")] +#[test] +fn storage_vec_dynamic_pop_clears_spilled_body() { + // pop must call T::clear_storage which tears down both header and any + // spilled body chunks — verify via len + try_get. + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), h()) }; + let s100: String = str::repeat("x", 100); // spilled + let s80: String = str::repeat("y", 80); // spilled + v.push(&s100); + v.push(&s80); + + let popped: Option = v.pop(); + assert_eq!(popped.as_ref().map(|s| s.len()), Some(80usize)); + assert_eq!(v.len(), 1); + assert_eq!(v.try_get(1), None); + assert_eq!(v.get(0).len(), 100); +} + +#[cfg(feature = "alloc")] +#[test] +fn storage_vec_dynamic_clear_zeros_spilled_bodies() { + // clear() must call T::clear_storage on every element, tearing down + // both the inline header and any spilled body chunks. Verify the + // header slots AND a representative spilled-body slot all read zero + // after clear(). + let host = h(); + let mut v = unsafe { StorageVec::::new(StorageKey::from_slot(0), host.clone()) }; + let s_a: String = str::repeat("a", 100); // spilled (4 body chunks) + let s_b: String = str::repeat("b", 80); // spilled (3 body chunks) + let s_c: String = str::repeat("c", 50); // spilled (2 body chunks) + v.push(&s_a); + v.push(&s_b); + v.push(&s_c); + assert_eq!(v.len(), 3); + + // Capture the first spilled-body chunk of element 0 (proves it's + // non-zero before clear, so the post-clear assertion is meaningful). + let body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let header_e0 = body; + let mut spilled_e0_chunk0 = [0u8; 32]; + host.hash_keccak_256(&header_e0, &mut spilled_e0_chunk0); + assert_ne!(storage_get_32(&host, &spilled_e0_chunk0), [0u8; 32]); + + v.clear(); + assert_eq!(v.len(), 0); + assert!(v.is_empty()); + + // Outer length slot is zero. + assert_eq!( + storage_get_32(&host, StorageKey::from_slot(0).as_bytes()), + [0u8; 32] + ); + + // Each element's header slot (in the array body) is zero. + let mut header_key = body; + for _ in 0..3 { + assert_eq!(storage_get_32(&host, &header_key), [0u8; 32]); + inc_slot(&mut header_key); + } + + // The previously-non-zero spilled chunk of element 0 is now zero. + assert_eq!(storage_get_32(&host, &spilled_e0_chunk0), [0u8; 32]); +} + +// --- Mapping> — `mapping(K => T[])` in Solidity --- + +#[test] +fn mapping_of_storage_vec_roundtrip() { + let mut m = unsafe { Mapping::>::new(StorageKey::from_slot(4), h()) }; + let alice = Address([0xAA; 20]); + let bob = Address([0xBB; 20]); + + { + let mut alice_vec = m.entry(&alice); + alice_vec.push(&U256::from(1u64)); + alice_vec.push(&U256::from(2u64)); + alice_vec.push(&U256::from(3u64)); + } + { + let mut bob_vec = m.entry(&bob); + bob_vec.push(&U256::from(99u64)); + } + + assert_eq!(m.get(&alice).len(), 3); + assert_eq!(m.get(&alice).get(0), U256::from(1u64)); + assert_eq!(m.get(&alice).get(2), U256::from(3u64)); + assert_eq!(m.get(&bob).len(), 1); + assert_eq!(m.get(&bob).get(0), U256::from(99u64)); +} + +#[test] +fn mapping_of_storage_vec_keys_are_independent() { + // Two keys must produce non-overlapping element ranges. + let mut m = unsafe { Mapping::>::new(StorageKey::from_slot(0), h()) }; + let a = Address([0x11; 20]); + let b = Address([0x22; 20]); + + m.entry(&a).push(&U256::from(100u64)); + m.entry(&b).push(&U256::from(200u64)); + m.entry(&a).push(&U256::from(101u64)); + + assert_eq!(m.get(&a).len(), 2); + assert_eq!(m.get(&b).len(), 1); + assert_eq!(m.get(&a).get(0), U256::from(100u64)); + assert_eq!(m.get(&a).get(1), U256::from(101u64)); + assert_eq!(m.get(&b).get(0), U256::from(200u64)); +} + +#[test] +fn mapping_of_storage_vec_matches_solc_layout() { + // Cross-check that `Mapping>` lays out elements at the + // same slots solc would for `mapping(K => T[]) at slot S`: + // T[]_slot = keccak256(pad32(K) ++ pad32(S)) + // length = T[]_slot + // element i = keccak256(T[]_slot) + i + let host = h(); + let mut m = unsafe { + Mapping::>::new(StorageKey::from_slot(7), host.clone()) + }; + let key = Address([0x33; 20]); + + m.entry(&key).push(&U256::from(0xDEADu64)); + m.entry(&key).push(&U256::from(0xBEEFu64)); + + // Derive the inner-vec root the same way `Mapping::slot_of` does. + let inner_root = StorageKey::from_slot(7).derive(&host, &key); + // Length lives at the inner root. + let len_slot = storage_get_32(&host, inner_root.as_bytes()); + assert_eq!(len_slot[31], 2); + + // Element 0 lives at keccak256(inner_root). + let body_base = storage_derive_body_base(&host, inner_root.as_bytes()); + let e0 = storage_get_32(&host, &body_base); + assert_eq!(U256::from_be_bytes(e0), U256::from(0xDEADu64)); + + let mut e1_key = body_base; + inc_slot(&mut e1_key); + let e1 = storage_get_32(&host, &e1_key); + assert_eq!(U256::from_be_bytes(e1), U256::from(0xBEEFu64)); +} + +#[test] +fn mapping_of_storage_vec_view_borrow_is_read_only() { + // Behavioural check that the `Ref<'_, StorageVec>` returned by + // `Mapping>::get` exposes only `&self` methods. The + // compile-fail UI test pins the type-level guarantee; this test + // exercises the read path. + let mut m = unsafe { Mapping::>::new(StorageKey::from_slot(0), h()) }; + let k = Address([0x44; 20]); + m.entry(&k).push(&U256::from(7u64)); + + let view = m.get(&k); + assert_eq!(view.len(), 1); + assert_eq!(view.get(0), U256::from(7u64)); + assert_eq!(view.try_get(1), None); +} + +// --- StorageVec> — `T[][]` in Solidity --- + +#[test] +fn nested_storage_vec_roundtrip() { + let mut outer = + unsafe { StorageVec::>::new_nested(StorageKey::from_slot(0), h()) }; + + outer.push_empty(); + outer.push_empty(); + assert_eq!(outer.outer_len(), 2); + + { + let mut row0 = outer.entry(0); + row0.push(&U256::from(10u64)); + row0.push(&U256::from(20u64)); + } + { + let mut row1 = outer.entry(1); + row1.push(&U256::from(30u64)); + } + + assert_eq!(outer.get(0).len(), 2); + assert_eq!(outer.get(0).get(0), U256::from(10u64)); + assert_eq!(outer.get(0).get(1), U256::from(20u64)); + assert_eq!(outer.get(1).len(), 1); + assert_eq!(outer.get(1).get(0), U256::from(30u64)); +} + +#[test] +fn nested_storage_vec_inner_rows_are_independent() { + // Two different outer indices must derive non-overlapping inner roots. + let mut outer = + unsafe { StorageVec::>::new_nested(StorageKey::from_slot(0), h()) }; + + outer.push_empty(); + outer.push_empty(); + outer.entry(0).push(&U256::from(0xAAu64)); + outer.entry(1).push(&U256::from(0xBBu64)); + outer.entry(0).push(&U256::from(0xCCu64)); + + assert_eq!(outer.get(0).len(), 2); + assert_eq!(outer.get(1).len(), 1); + assert_eq!(outer.get(0).get(0), U256::from(0xAAu64)); + assert_eq!(outer.get(0).get(1), U256::from(0xCCu64)); + assert_eq!(outer.get(1).get(0), U256::from(0xBBu64)); +} + +#[test] +fn nested_storage_vec_matches_solc_layout() { + // Cross-check that nested layout matches solc's `T[][] at slot S`: + // inner_root(i) = keccak256(pad32(S)) + i + // inner length at inner_root(i) + // inner body at keccak256(inner_root(i)) + j + let host = h(); + let mut outer = unsafe { + StorageVec::>::new_nested(StorageKey::from_slot(11), host.clone()) + }; + + outer.push_empty(); + outer.push_empty(); + outer.push_empty(); + outer.entry(0).push(&U256::from(0x1111u64)); + outer.entry(2).push(&U256::from(0x2222u64)); + outer.entry(2).push(&U256::from(0x3333u64)); + + let outer_body = storage_derive_body_base(&host, StorageKey::from_slot(11).as_bytes()); + + // inner_root(0) = outer_body + 0 + let inner0_root = outer_body; + let inner0_len = storage_get_32(&host, &inner0_root); + assert_eq!(inner0_len[31], 1, "inner[0] length"); + + let inner0_body = storage_derive_body_base(&host, &inner0_root); + let inner0_e0 = storage_get_32(&host, &inner0_body); + assert_eq!(U256::from_be_bytes(inner0_e0), U256::from(0x1111u64)); + + // inner_root(2) = outer_body + 2 + let mut inner2_root = outer_body; + inc_slot_by(&mut inner2_root, 2); + let inner2_len = storage_get_32(&host, &inner2_root); + assert_eq!(inner2_len[31], 2, "inner[2] length"); + + let inner2_body = storage_derive_body_base(&host, &inner2_root); + let inner2_e0 = storage_get_32(&host, &inner2_body); + assert_eq!(U256::from_be_bytes(inner2_e0), U256::from(0x2222u64)); + + let mut inner2_e1_key = inner2_body; + inc_slot(&mut inner2_e1_key); + let inner2_e1 = storage_get_32(&host, &inner2_e1_key); + assert_eq!(U256::from_be_bytes(inner2_e1), U256::from(0x3333u64)); +} + +#[test] +fn nested_storage_vec_outer_len_tracks_push_empty() { + let mut outer = + unsafe { StorageVec::>::new_nested(StorageKey::from_slot(0), h()) }; + + assert_eq!(outer.outer_len(), 0); + assert!(outer.is_empty()); + + outer.push_empty(); + assert_eq!(outer.outer_len(), 1); + assert!(!outer.is_empty()); + + outer.push_empty(); + outer.push_empty(); + assert_eq!(outer.outer_len(), 3); + + // Inner array at index 0 is empty (no elements pushed). + assert_eq!(outer.get(0).len(), 0); + assert!(outer.get(0).is_empty()); +} + +#[test] +fn nested_storage_vec_view_borrow_is_read_only() { + let mut outer = + unsafe { StorageVec::>::new_nested(StorageKey::from_slot(0), h()) }; + outer.push_empty(); + outer.entry(0).push(&U256::from(42u64)); + + let view = outer.get(0); + assert_eq!(view.len(), 1); + assert_eq!(view.get(0), U256::from(42u64)); + assert_eq!(view.try_get(1), None); +} + +#[test] +fn nested_storage_vec_storage_component_metadata() { + // `StorageVec>` plugs into the `#[storage]` / + // `#[contract]` macro path as a full-slot, non-packing component — + // matches the flat `StorageVec` shape exactly. + assert_eq!(> as StorageComponent>::SLOTS, 1); + assert_eq!( + > as StorageComponent>::PACKED_BYTES, + 32 + ); +} + +#[test] +fn nested_storage_vec_new_at_matches_new_nested() { + // `StorageComponent::new_at` is the macro-emitted safe path; assert it + // produces a handle indistinguishable from `unsafe { new_nested(...) }`. + let host = h(); + let mut a = unsafe { + StorageVec::>::new_nested(StorageKey::from_slot(4), host.clone()) + }; + let mut b = > as StorageComponent>::new_at(4, 0, host); + a.push_empty(); + a.entry(0).push(&U256::from(0xAAu64)); + // Both handles see the same underlying storage. + assert_eq!(b.outer_len(), 1); + assert_eq!(b.get(0).get(0), U256::from(0xAAu64)); + b.entry(0).push(&U256::from(0xBBu64)); + assert_eq!(a.get(0).len(), 2); + assert_eq!(a.get(0).get(1), U256::from(0xBBu64)); +} + +#[test] +fn nested_storage_vec_subword_inner_roundtrip() { + // u64 has PACKED_BYTES = 8, so 4 elements pack per slot in the inner + // body. Verify nested dispatch threads through to sub-word read/write + // and that two inner rows packing 6 and 10 elements stay independent. + let mut outer = + unsafe { StorageVec::>::new_nested(StorageKey::from_slot(0), h()) }; + outer.push_empty(); + outer.push_empty(); + + { + let mut row0 = outer.entry(0); + for i in 0u64..6 { + row0.push(&(0xA000 + i)); + } + } + { + let mut row1 = outer.entry(1); + for i in 0u64..10 { + row1.push(&(0xB000 + i)); + } + } + + let r0 = outer.get(0); + assert_eq!(r0.len(), 6); + for i in 0u64..6 { + assert_eq!(r0.get(i), 0xA000 + i); + } + drop(r0); + let r1 = outer.get(1); + assert_eq!(r1.len(), 10); + for i in 0u64..10 { + assert_eq!(r1.get(i), 0xB000 + i); + } +} + +#[test] +fn nested_storage_vec_pop_recursively_clears_inner() { + // pop() must destroy the popped inner array's storage — both its + // length slot and its body slots — matching solc `T[][].pop()`. + let host = h(); + let mut outer = unsafe { + StorageVec::>::new_nested(StorageKey::from_slot(0), host.clone()) + }; + outer.push_empty(); + outer.push_empty(); + outer.entry(0).push(&U256::from(11u64)); + outer.entry(1).push(&U256::from(22u64)); + outer.entry(1).push(&U256::from(33u64)); + + // Snapshot the soon-to-be-popped inner's length and first body slot + // as non-zero so the post-pop zero check is meaningful. + let outer_body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let mut inner1_root = outer_body; + inc_slot_by(&mut inner1_root, 1); + let inner1_body = storage_derive_body_base(&host, &inner1_root); + assert_ne!(storage_get_32(&host, &inner1_root), [0u8; 32]); + assert_ne!(storage_get_32(&host, &inner1_body), [0u8; 32]); + + assert!(outer.pop()); + assert_eq!(outer.outer_len(), 1); + + // Popped inner's length slot and body slots are zero. + assert_eq!(storage_get_32(&host, &inner1_root), [0u8; 32]); + assert_eq!(storage_get_32(&host, &inner1_body), [0u8; 32]); + let mut inner1_body_next = inner1_body; + inc_slot(&mut inner1_body_next); + assert_eq!(storage_get_32(&host, &inner1_body_next), [0u8; 32]); + + // Surviving inner row is untouched. + assert_eq!(outer.get(0).len(), 1); + assert_eq!(outer.get(0).get(0), U256::from(11u64)); + + // pop draining to empty returns true once more, then false. + assert!(outer.pop()); + assert_eq!(outer.outer_len(), 0); + assert!(!outer.pop()); +} + +#[test] +fn nested_storage_vec_clear_recursively_clears_all_inners() { + // clear() walks every inner, calls its clear(), and zeroes the outer + // length — matching solc `delete matrix`. + let host = h(); + let mut outer = unsafe { + StorageVec::>::new_nested(StorageKey::from_slot(0), host.clone()) + }; + outer.push_empty(); + outer.push_empty(); + outer.entry(0).push(&U256::from(1u64)); + outer.entry(0).push(&U256::from(2u64)); + outer.entry(1).push(&U256::from(3u64)); + + let outer_body = storage_derive_body_base(&host, StorageKey::from_slot(0).as_bytes()); + let inner0_root = outer_body; + let mut inner1_root = outer_body; + inc_slot(&mut inner1_root); + let inner0_body = storage_derive_body_base(&host, &inner0_root); + let inner1_body = storage_derive_body_base(&host, &inner1_root); + + // Sanity: storage is non-zero before clear. + assert_ne!(storage_get_32(&host, &inner0_root), [0u8; 32]); + assert_ne!(storage_get_32(&host, &inner0_body), [0u8; 32]); + assert_ne!(storage_get_32(&host, &inner1_root), [0u8; 32]); + assert_ne!(storage_get_32(&host, &inner1_body), [0u8; 32]); + + outer.clear(); + assert_eq!(outer.outer_len(), 0); + assert!(outer.is_empty()); + + // Outer length slot is zero. + assert_eq!( + storage_get_32(&host, StorageKey::from_slot(0).as_bytes()), + [0u8; 32] + ); + // Each inner's length and body slots are zero. + assert_eq!(storage_get_32(&host, &inner0_root), [0u8; 32]); + assert_eq!(storage_get_32(&host, &inner0_body), [0u8; 32]); + let mut inner0_body_next = inner0_body; + inc_slot(&mut inner0_body_next); + assert_eq!(storage_get_32(&host, &inner0_body_next), [0u8; 32]); + assert_eq!(storage_get_32(&host, &inner1_root), [0u8; 32]); + assert_eq!(storage_get_32(&host, &inner1_body), [0u8; 32]); +} + +#[test] +#[should_panic(expected = "out of bounds")] +fn nested_storage_vec_get_panics_on_oob() { + let outer = + unsafe { StorageVec::>::new_nested(StorageKey::from_slot(0), h()) }; + // outer_len == 0: any index is OOB. + let _ = outer.get(0); +} + +#[test] +#[should_panic(expected = "out of bounds")] +fn nested_storage_vec_entry_panics_on_oob() { + let mut outer = + unsafe { StorageVec::>::new_nested(StorageKey::from_slot(0), h()) }; + outer.push_empty(); + // outer_len == 1: index 1 is OOB. + let _ = outer.entry(1); +} + +#[test] +fn inc_slot_by_zero_is_identity() { + let mut slot = [0u8; 32]; + slot[31] = 0xAB; + let original = slot; + inc_slot_by(&mut slot, 0); + assert_eq!(slot, original); +} + +#[test] +fn inc_slot_by_small() { + let mut slot = [0u8; 32]; + inc_slot_by(&mut slot, 5); + let mut expected = [0u8; 32]; + expected[31] = 5; + assert_eq!(slot, expected); +} + +#[test] +fn inc_slot_by_carries_through_low_bytes() { + let mut slot = [0u8; 32]; + slot[31] = 0xFF; + inc_slot_by(&mut slot, 1); + let mut expected = [0u8; 32]; + expected[30] = 1; + expected[31] = 0; + assert_eq!(slot, expected); +} + +#[test] +fn inc_slot_by_full_u64_range() { + let mut slot = [0u8; 32]; + inc_slot_by(&mut slot, u64::MAX); + // Lowest 8 bytes should be 0xFF...FF, upper 24 bytes zero. + let mut expected = [0u8; 32]; + expected[24..].copy_from_slice(&u64::MAX.to_be_bytes()); + assert_eq!(slot, expected); +} + +#[test] +fn inc_slot_by_carries_into_high_bytes() { + // Start at "all 0xFF in low 8 bytes", add 1 -> carry into byte 23. + let mut slot = [0u8; 32]; + slot[24..].fill(0xFF); + inc_slot_by(&mut slot, 1); + let mut expected = [0u8; 32]; + expected[23] = 1; + // Bytes 24..32 wrap to zero. + assert_eq!(slot, expected); +} diff --git a/crates/pvm-storage/tests/ui/lazy_of_fixed_array_non_u8_rejected.rs b/crates/pvm-storage/tests/ui/lazy_of_fixed_array_non_u8_rejected.rs deleted file mode 100644 index 95b5b76b..00000000 --- a/crates/pvm-storage/tests/ui/lazy_of_fixed_array_non_u8_rejected.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! `[T; N]` for `T != u8` has no `StorageEncode` impl, so `Lazy<[U256; 3]>` -//! cannot be constructed. Byte-arrays (`[u8; N]`) are the special case that -//! IS supported (mapping to Solidity `bytesN`). Anything else — a fixed -//! array of integers, addresses, structs — has to be modelled via -//! `Mapping` until a generic array accessor lands. -use pvm_contract_types::{Host, MockHostBuilder}; -use pvm_storage::{Lazy, StorageKey}; -use ruint::aliases::U256; -use std::rc::Rc; - -fn main() { - let host = Host::from_dyn(Rc::new(MockHostBuilder::new().build())); - let _bad = unsafe { Lazy::<[U256; 3]>::new(StorageKey::from_slot(0), 0, host) }; -} diff --git a/crates/pvm-storage/tests/ui/lazy_of_fixed_array_non_u8_rejected.stderr b/crates/pvm-storage/tests/ui/lazy_of_fixed_array_non_u8_rejected.stderr deleted file mode 100644 index 544742c9..00000000 --- a/crates/pvm-storage/tests/ui/lazy_of_fixed_array_non_u8_rejected.stderr +++ /dev/null @@ -1,9 +0,0 @@ -error[E0599]: the function or associated item `new` exists for struct `Lazy<[Uint<256, 4>; 3]>`, but its trait bounds were not satisfied - --> tests/ui/lazy_of_fixed_array_non_u8_rejected.rs:13:44 - | -13 | let _bad = unsafe { Lazy::<[U256; 3]>::new(StorageKey::from_slot(0), 0, host) }; - | ^^^ function or associated item cannot be called on `Lazy<[Uint<256, 4>; 3]>` due to unsatisfied trait bounds - | - = note: the following trait bounds were not satisfied: - `[Uint<256, 4>; 3]: StorageEncode` - `[Uint<256, 4>; 3]: StorageDecode`