From 8744e292f4712a3f3d12f2f3846e9a74ad196142 Mon Sep 17 00:00:00 2001 From: Mathieu <60658558+enitrat@users.noreply.github.com> Date: Wed, 13 Sep 2023 15:04:16 +0200 Subject: [PATCH] feat: store packing (#343) * feat: store packing * Apply suggestions from code review Co-authored-by: glihm * fix: build * add suggestions * fmt --------- Co-authored-by: glihm --- .../listing_99_13_storage_packing/.gitignore | 1 + .../listing_99_13_storage_packing/Scarb.toml | 9 ++ .../src/lib.cairo | 85 +++++++++++++++++++ src/SUMMARY.md | 1 + src/ch99-01-03-05-optimizing-storage.md | 35 ++++++++ 5 files changed, 131 insertions(+) create mode 100644 listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/.gitignore create mode 100644 listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/Scarb.toml create mode 100644 listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/src/lib.cairo create mode 100644 src/ch99-01-03-05-optimizing-storage.md diff --git a/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/.gitignore b/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/.gitignore new file mode 100644 index 000000000..eb5a316cb --- /dev/null +++ b/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/.gitignore @@ -0,0 +1 @@ +target diff --git a/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/Scarb.toml b/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/Scarb.toml new file mode 100644 index 000000000..8723451c1 --- /dev/null +++ b/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "listing_99_13_storage_packing" +version = "0.1.0" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest + +[dependencies] +# foo = { path = "vendor/foo" } +starknet = ">=2.1.0" diff --git a/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/src/lib.cairo b/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/src/lib.cairo new file mode 100644 index 000000000..ec5b4dc7c --- /dev/null +++ b/listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/src/lib.cairo @@ -0,0 +1,85 @@ +//ANCHOR:here +use starknet::{StorePacking}; +use integer::{u128_safe_divmod, u128_as_non_zero}; +use traits::{Into, TryInto}; +use option::OptionTrait; + +#[derive(Drop, Serde)] +struct Sizes { + tiny: u8, + small: u32, + medium: u64, +} + +const TWO_POW_8: u128 = 0x100; +const TWO_POW_40: u128 = 0x10000000000; + +const MASK_8: u128 = 0xff; +const MASK_32: u128 = 0xffffffff; + + +impl SizesStorePacking of StorePacking { + fn pack(value: Sizes) -> u128 { + value.tiny.into() + (value.small.into() * TWO_POW_8) + (value.medium.into() * TWO_POW_40) + } + + fn unpack(value: u128) -> Sizes { + let tiny = value & MASK_8; + let small = (value / TWO_POW_8) & MASK_32; + let medium = (value / TWO_POW_40); + + Sizes { + tiny: tiny.try_into().unwrap(), + small: small.try_into().unwrap(), + medium: medium.try_into().unwrap(), + } + } +} + +#[starknet::contract] +mod SizeFactory { + use super::Sizes; + use super::SizesStorePacking; //don't forget to import it! + + #[storage] + struct Storage { + remaining_sizes: Sizes + } + + #[external(v0)] + fn update_sizes(ref self: ContractState, sizes: Sizes) { + // This will automatically pack the + // struct into a single u128 + self.remaining_sizes.write(sizes); + } + + + #[external(v0)] + fn get_sizes(ref self: ContractState) -> Sizes { + // this will automatically unpack the + // packed-representation into the Sizes struct + self.remaining_sizes.read() + } +} + + +//ANCHOR_END:here + +#[cfg(test)] +mod tests { + use super::{SizesStorePacking, Sizes}; + use starknet::StorePacking; + #[test] + #[available_gas(200000)] + fn test_pack_unpack() { + let value = Sizes { tiny: 0x12, small: 0x12345678, medium: 0x1234567890, }; + + let packed = SizesStorePacking::pack(value); + assert(packed == 0x12345678901234567812, 'wrong packed value'); + + let unpacked = SizesStorePacking::unpack(packed); + assert(unpacked.tiny == 0x12, 'wrong unpacked tiny'); + assert(unpacked.small == 0x12345678, 'wrong unpacked small'); + assert(unpacked.medium == 0x1234567890, 'wrong unpacked medium'); + } +} diff --git a/src/SUMMARY.md b/src/SUMMARY.md index b1eb7fe59..20caf9ee7 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -93,6 +93,7 @@ - [Contract Functions](./ch99-01-03-02-contract-functions.md) - [Contract Events](./ch99-01-03-03-contract-events.md) - [Reducing boilerplate](./ch99-01-03-04-reducing-boilerplate.md) + - [Optimizing storage costs](./ch99-01-03-05-optimizing-storage.md) - [ABIs and Cross-contract Interactions](./ch99-02-00-abis-and-cross-contract-interactions.md) - [ABIs and Interfaces](./ch99-02-01-abis-and-interfaces.md) - [Contract Dispatchers, Library Dispachers and system calls](./ch99-02-02-contract-dispatcher-library-dispatcher-and-system-calls.md) diff --git a/src/ch99-01-03-05-optimizing-storage.md b/src/ch99-01-03-05-optimizing-storage.md new file mode 100644 index 000000000..cfd613f79 --- /dev/null +++ b/src/ch99-01-03-05-optimizing-storage.md @@ -0,0 +1,35 @@ +## Storage Optimization with `StorePacking` + +Bit-packing is a simple concept: Use as few bit as possible to store a piece of data. When done well, it can significantly reduce the size of the data you need to store. This is especially important in smart contracts, where storage is expensive. + +When writing Cairo smart contracts, it is important to optimize storage usage to reduce gas costs. Indeed, most of the cost associated to a transaction is related to storage updates; and each storage slot costs gas to write to. +This means that by packing multiple values into fewer slots, you can decrease the gas cost incurred to the users of your smart contract. + +Cairo provides the `StorePacking` trait to enable packing struct fields into a fewer number of storage slots. For example, consider a `Sizes` struct with 3 fields of different types. The total size is 8 + 32 + 64 = 104 bits. This is less than the 128 bits of a single `u128`. This means we can pack all 3 fields into a single `u128` variable. Since a storage slot can hold up to 251 bits, our packed value will take only one storage slot instead of 3. + +```rust +{{#include ../listings/ch99-starknet-smart-contracts/listing_99_13_storage_packing/src/lib.cairo:here}} +``` + +Optimizing storage by implementing the `StorePacking` trait + +The `pack` function combines all three fields into a single `u128` value by performing bitshift and additions. The `unpack` reverses this process to extract the original fields back into a struct. + +If you're not familiar with bit operations, here's an explanation of the operations performed in the example: +The goal is to pack the `tiny`, `small`, and `medium` fields into a single `u128` value. +First, when packing: + +- `tiny` is a `u8` so we just convert it directly to a `u128` with `.into()`. This creates a `u128` value with the low 8 bits set to `tiny`'s value. +- `small` is a `u32` so we first shift it left by 8 bits (add 8 bits with the value 0 to the left) to create room for the 8 bites taken by `tiny`. Then we add `tiny` to `small` to combine them into a single `u128` value. The value of `tiny` now takes bits 0-7 and the value of small takes bits 8-39. +- Similarly `medium` is a `u64` so we shift it left by 40 (8 + 32) bits (`TWO_POW_40`) to make space for the previous fields. This takes bits 40-103. + +When unpacking: + +- First we extract `tiny` by bitwise ANDing (&) with a bitmask of 8 ones (`& MASK_8`). This isolates the lowest 8 bits of the packed value, which is `tiny`'s value. +- For `small`, we right shift by 8 bits (`/ TWO_POW_8`) to align it with the bitmask, then use bitwise AND with the 32 ones bitmask. +- For `medium` we right shift by 40 bits. Since it is the last value packed, we don't need to apply a bitmask as the higher bits are already 0. + +This technique can be used for any group of fields that fit within the bit size of the packed storage type. For example, if you have a struct with multiple fields whose bit sizes add up to 256 bits, you can pack them into a single `u256` variable. If the bit sizes add up to 512 bits, you can pack them into a single `u512` variable, and so on. You can define your own structs and logic to pack and unpack them. + +The rest of the work is done magically by the compiler - if a type implements the `StorePacking` trait, then the compiler will know it can use the `StoreUsingPacking` implementation of the `Store` trait in order to pack before writing and unpack after reading from storage. +One important details, however, is that the type that `StorePacking::pack` spits out also has to implement `Store` for `StoreUsingPacking` to work. Most of the time, we will want to pack into a felt252 or u256 - but if you want to pack into a type of your own, make sure that this one implements the `Store` trait.