Skip to content
Draft
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<V>` 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<T>`) 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<T>`) 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<u128> a; Lazy<u128> 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::<T>::try_get` is rejected at compile time for sub-32-byte `T` (e.g. `Lazy<u128>`) 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<u128>,
max_supply: Lazy<u128>,
}

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(())
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<Settings>,
}

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(())
}
}
}
15 changes: 14 additions & 1 deletion crates/pvm-contract-benchmarks/src/bin/build-and-measure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,13 @@ fn build_variant(
}

fn variants_for_contract(contract: &str) -> Vec<Variant> {
// The `packed_*` contracts are an A/B comparison of the storage value
// model (`Lazy<S>` over a `#[derive(SolType)]` struct) vs the handle
// bundle model (`#[storage]` of `Lazy<T>` 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);
Expand All @@ -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
Expand Down
13 changes: 8 additions & 5 deletions crates/pvm-contract-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pub use pvm_contract_types::{
SolEvent,
StaticDecode,
StaticEncodedLen,
StorageArrayElement,
StorageDecode,
StorageEncode,
StorageFlags,
Expand Down Expand Up @@ -123,12 +124,14 @@ pub use pvm_contract_core::call::{
// Typed storage helpers. `Lazy<T>` / `Mapping<K, V>` 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<u8>` is intentionally not a
// storage value — use `Bytes` for `bytes`-shaped storage (`Vec<u8>` 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<T>` models Solidity's
// `T[]` dynamic arrays. `Vec<u8>` is intentionally not a storage value —
// use `Bytes` for `bytes`-shaped storage (`Vec<u8>` 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")]
Expand Down
2 changes: 1 addition & 1 deletion crates/pvm-contract-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
122 changes: 122 additions & 0 deletions crates/pvm-contract-types/src/storage_codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,128 @@ impl<const N: usize> 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<T: StorageArrayElement, const N: usize> 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<T: StorageArrayElement, const N: usize> 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.
//
Expand Down
Loading
Loading