-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: store using packing optimisation (#72)
* feat: store using packing optimisation * feat: add packing contract * chore: update to v2.2.0
- Loading branch information
Showing
7 changed files
with
141 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
[package] | ||
name = "store_using_packing" | ||
version = "0.1.0" | ||
|
||
[dependencies] | ||
starknet = ">=2.2.0" | ||
|
||
[[target.starknet-contract]] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
mod packing; |
92 changes: 92 additions & 0 deletions
92
listings/ch03-optimisations/store_using_packing/src/packing.cairo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// ANCHOR: Packing | ||
#[starknet::interface] | ||
trait ITime<TContractState> { | ||
fn set(ref self: TContractState, value: TimeContract::Time); | ||
fn get(self: @TContractState) -> TimeContract::Time; | ||
} | ||
|
||
#[starknet::contract] | ||
mod TimeContract { | ||
use starknet::storage_access::StorePacking; | ||
use integer::{ | ||
U8IntoFelt252, Felt252TryIntoU16, U16DivRem, u16_as_non_zero, U16IntoFelt252, | ||
Felt252TryIntoU8 | ||
}; | ||
use traits::{Into, TryInto, DivRem}; | ||
use option::OptionTrait; | ||
use serde::Serde; | ||
|
||
#[storage] | ||
struct Storage { | ||
time: Time | ||
} | ||
|
||
#[derive(Copy, Serde, Drop)] | ||
struct Time { | ||
hour: u8, | ||
minute: u8 | ||
} | ||
|
||
impl TimePackable of StorePacking<Time, felt252> { | ||
fn pack(value: Time) -> felt252 { | ||
let msb: felt252 = 256 * value.hour.into(); | ||
let lsb: felt252 = value.minute.into(); | ||
return msb + lsb; | ||
} | ||
fn unpack(value: felt252) -> Time { | ||
let value: u16 = value.try_into().unwrap(); | ||
let (q, r) = U16DivRem::div_rem(value, u16_as_non_zero(256)); | ||
let hour: u8 = Into::<u16, felt252>::into(q).try_into().unwrap(); | ||
let minute: u8 = Into::<u16, felt252>::into(r).try_into().unwrap(); | ||
return Time { hour, minute }; | ||
} | ||
} | ||
|
||
#[external(v0)] | ||
impl TimeContract of super::ITime<ContractState> { | ||
fn set(ref self: ContractState, value: Time) { | ||
// This will call the pack method of the TimePackable trait | ||
// and store the resulting felt252 | ||
self.time.write(value); | ||
} | ||
fn get(self: @ContractState) -> Time { | ||
// This will read the felt252 value from storage | ||
// and return the result of the unpack method of the TimePackable trait | ||
return self.time.read(); | ||
} | ||
} | ||
} | ||
// ANCHOR_END: Packing | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::TimeContract; | ||
use super::TimeContract::Time; | ||
use super::{ITimeDispatcher, ITimeDispatcherTrait}; | ||
|
||
use starknet::deploy_syscall; | ||
use starknet::class_hash::Felt252TryIntoClassHash; | ||
|
||
use debug::PrintTrait; | ||
|
||
#[test] | ||
#[available_gas(20000000)] | ||
fn test_packing() { | ||
// Set up. | ||
let mut calldata: Array<felt252> = ArrayTrait::new(); | ||
let (address0, _) = deploy_syscall( | ||
TimeContract::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false | ||
) | ||
.unwrap(); | ||
let mut contract = ITimeDispatcher { contract_address: address0 }; | ||
|
||
// Store a Time struct. | ||
let time = Time { hour: 1, minute: 2 }; | ||
contract.set(time); | ||
|
||
// Read the stored struct. | ||
let read_time: Time = contract.get(); | ||
assert(read_time.hour == time.hour, 'Time.hour mismatch'); | ||
assert(read_time.minute == time.minute, 'Time.minute mismatch'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Optimisations | ||
|
||
A collection of optimisation patterns to save gas and steps. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# Storage optimisation | ||
|
||
A smart contract has a limited amount of **storage slots**. Each slot can store a single `felt252` value. | ||
Writing to a storage slot has a cost, so we want to use as few storage slots as possible. | ||
|
||
In Cairo, every type is derived from the `felt252` type, which uses 252 bits to store a value. | ||
This design is quite simple, but it does have a drawback: it is not storage efficient. For example, if we want to store a `u8` value, we need to use an entire slot, even though we only need 8 bits. | ||
|
||
## Packing | ||
|
||
When storing multiple values, we can use a technique called **packing**. Packing is a technique that allows us to store multiple values in a single felt value. This is done by using the bits of the felt value to store multiple values. | ||
|
||
For example, if we want to store two `u8` values, we can use the first 8 bits of the felt value to store the first `u8` value, and the last 8 bits to store the second `u8` value. This way, we can store two `u8` values in a single felt value. | ||
|
||
Cairo provides a built-in store using packing that you can use with the `StorePacking` trait. | ||
|
||
```rust | ||
trait StorePacking<T, PackedT> { | ||
fn pack(value: T) -> PackedT; | ||
fn unpack(value: PackedT) -> T; | ||
} | ||
``` | ||
|
||
This allows to store the type `T` by first packing it into the type `PackedT` with the `pack` function, and then storing the `PackedT` value with it's `Store` implementation. When reading the value, we first retrieve the `PackedT` value, and then unpack it into the type `T` using the `unpack` function. | ||
|
||
Here's an example of storing a `Time` struct with two `u8` values using the `StorePacking` trait: | ||
|
||
```rust | ||
{{#include ../listings/ch03-optimisations/store_using_packing/src/packing.cairo:Packing}} | ||
``` | ||
|
||
Play with this contract in [Remix](https://remix.ethereum.org/?#activate=Starknet-cairo1-compiler&url=https://github.com/NethermindEth/StarknetByExample/blob/main/listings/ch03-optimisations/store_using_packing/src/packing.cairo). |