合约存储
与合约存储进行交互的最常见方式是通过存储变量。正如前面所述,存储变量允许您存储将存储在合约的存储中的数据,而该存储本身存储在区块链上。这些数据是持久的,一旦合约部署后,可以随时访问和修改。
Starknet合约中的存储变量被存储在一个特殊的结构中,称为Storage
:
use starknet::ContractAddress;
#[starknet::interface]
trait INameRegistry<TContractState> {
fn store_name(
ref self: TContractState, name: felt252, registration_type: NameRegistry::RegistrationType
);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
fn get_owner(self: @TContractState) -> NameRegistry::Person;
}
#[starknet::contract]
mod NameRegistry {
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
registration_type: LegacyMap::<ContractAddress, RegistrationType>,
total_names: u128,
owner: Person
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Person {
name: felt252,
address: ContractAddress
}
#[derive(Drop, Serde, starknet::Store)]
enum RegistrationType {
finite: u64,
infinite
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.write(owner.address, owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
#[external(v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
let name = self.names.read(address);
name
}
fn get_owner(self: @ContractState) -> Person {
let owner = self.owner.read();
owner
}
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let mut total_names = self.total_names.read();
self.names.write(user, name);
self.registration_type.write(user, registration_type);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
fn get_contract_name() -> felt252 {
'Name Registry'
}
fn get_owner_storage_address(self: @ContractState) -> starknet::StorageBaseAddress {
self.owner.address()
}
}
Storage结构体 与其他结构体一样,
只是它 必须 带有 #[storage]
注解。这个注解告诉编译器生成与区块链状态交互所需的代码,并允许您从存储中读取和写入数据。此外,这还允许您使用 LegacyMap
类型定义存储映射。
存储结构中存储的每个变量都存储在合约存储的不同位置。变量的存储地址由变量名决定,如果是映射,则还由变量的最终键决定。
存储地址
存储变量的地址计算方式如下:
- 如果变量是单个值(不是映射),则地址是变量名 ASCII 编码的
sn_keccak
哈希值。sn_keccak
_是 Starknet 的 Keccak256 哈希函数版本,其输出被截断为 250 位。 - 如果变量是映射,,则键为
k_1,...,k_n
的值的地址是h(...h(h(sn_keccak(variable_name),k_1),k_2),...,k_n)
,其中 ℎ 是 Pedersen 哈希,最终值取mod (2^251) − 256
。 - 如果它是映射到复杂值(例如,元组或结构),则此复杂值位于从上一点计算的地址开始的连续段中。请注意,256 个域元素是当前复杂存储值最大大小的限制。
通过在变量上调用 address
函数,你可以访问存储变量的地址,该函数返回一个 StorageBaseAddress
值。
use starknet::ContractAddress;
#[starknet::interface]
trait INameRegistry<TContractState> {
fn store_name(
ref self: TContractState, name: felt252, registration_type: NameRegistry::RegistrationType
);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
fn get_owner(self: @TContractState) -> NameRegistry::Person;
}
#[starknet::contract]
mod NameRegistry {
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
registration_type: LegacyMap::<ContractAddress, RegistrationType>,
total_names: u128,
owner: Person
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Person {
name: felt252,
address: ContractAddress
}
#[derive(Drop, Serde, starknet::Store)]
enum RegistrationType {
finite: u64,
infinite
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.write(owner.address, owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
#[external(v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
let name = self.names.read(address);
name
}
fn get_owner(self: @ContractState) -> Person {
let owner = self.owner.read();
owner
}
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let mut total_names = self.total_names.read();
self.names.write(user, name);
self.registration_type.write(user, registration_type);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
fn get_contract_name() -> felt252 {
'Name Registry'
}
fn get_owner_storage_address(self: @ContractState) -> starknet::StorageBaseAddress {
self.owner.address()
}
}
与存储变量交互
存储在存储结构中的变量可以使用 read
和 write
函数进行访问和修改,还可以使用 addr
函数获取它们在存储中的地址。这些函数由编译器为每个存储变量自动生成。
要读取 owner
存储变量的值,该变量是一个单一值,我们调用 owner
变量上的 read
函数,不传入任何参数。
use starknet::ContractAddress;
#[starknet::interface]
trait INameRegistry<TContractState> {
fn store_name(
ref self: TContractState, name: felt252, registration_type: NameRegistry::RegistrationType
);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
fn get_owner(self: @TContractState) -> NameRegistry::Person;
}
#[starknet::contract]
mod NameRegistry {
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
registration_type: LegacyMap::<ContractAddress, RegistrationType>,
total_names: u128,
owner: Person
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Person {
name: felt252,
address: ContractAddress
}
#[derive(Drop, Serde, starknet::Store)]
enum RegistrationType {
finite: u64,
infinite
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.write(owner.address, owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
#[external(v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
let name = self.names.read(address);
name
}
fn get_owner(self: @ContractState) -> Person {
let owner = self.owner.read();
owner
}
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let mut total_names = self.total_names.read();
self.names.write(user, name);
self.registration_type.write(user, registration_type);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
fn get_contract_name() -> felt252 {
'Name Registry'
}
fn get_owner_storage_address(self: @ContractState) -> starknet::StorageBaseAddress {
self.owner.address()
}
}
要读取存储变量 names
的值,这个变量是从 ContractAddress
映射到 felt252
的,我们在 names
变量上调用 read
函数,将键 address
作为参数传入。如果这个映射有多个键,我们也会将其他键作为参数传入。
use starknet::ContractAddress;
#[starknet::interface]
trait INameRegistry<TContractState> {
fn store_name(
ref self: TContractState, name: felt252, registration_type: NameRegistry::RegistrationType
);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
fn get_owner(self: @TContractState) -> NameRegistry::Person;
}
#[starknet::contract]
mod NameRegistry {
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
registration_type: LegacyMap::<ContractAddress, RegistrationType>,
total_names: u128,
owner: Person
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Person {
name: felt252,
address: ContractAddress
}
#[derive(Drop, Serde, starknet::Store)]
enum RegistrationType {
finite: u64,
infinite
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.write(owner.address, owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
#[external(v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
let name = self.names.read(address);
name
}
fn get_owner(self: @ContractState) -> Person {
let owner = self.owner.read();
owner
}
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let mut total_names = self.total_names.read();
self.names.write(user, name);
self.registration_type.write(user, registration_type);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
fn get_contract_name() -> felt252 {
'Name Registry'
}
fn get_owner_storage_address(self: @ContractState) -> starknet::StorageBaseAddress {
self.owner.address()
}
}
要将值写入存储变量,我们需要调用 write
函数,并将其最终键和值作为参数传递。与 read 函数一样,参数的数量取决于键的数量——这里,我们只需要将要写入 owner
变量的值作为参数,因为它是一个简单的变量。
use starknet::ContractAddress;
#[starknet::interface]
trait INameRegistry<TContractState> {
fn store_name(
ref self: TContractState, name: felt252, registration_type: NameRegistry::RegistrationType
);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
fn get_owner(self: @TContractState) -> NameRegistry::Person;
}
#[starknet::contract]
mod NameRegistry {
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
registration_type: LegacyMap::<ContractAddress, RegistrationType>,
total_names: u128,
owner: Person
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Person {
name: felt252,
address: ContractAddress
}
#[derive(Drop, Serde, starknet::Store)]
enum RegistrationType {
finite: u64,
infinite
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.write(owner.address, owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
#[external(v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
let name = self.names.read(address);
name
}
fn get_owner(self: @ContractState) -> Person {
let owner = self.owner.read();
owner
}
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let mut total_names = self.total_names.read();
self.names.write(user, name);
self.registration_type.write(user, registration_type);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
fn get_contract_name() -> felt252 {
'Name Registry'
}
fn get_owner_storage_address(self: @ContractState) -> starknet::StorageBaseAddress {
self.owner.address()
}
}
use starknet::ContractAddress;
#[starknet::interface]
trait INameRegistry<TContractState> {
fn store_name(
ref self: TContractState, name: felt252, registration_type: NameRegistry::RegistrationType
);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
fn get_owner(self: @TContractState) -> NameRegistry::Person;
}
#[starknet::contract]
mod NameRegistry {
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
registration_type: LegacyMap::<ContractAddress, RegistrationType>,
total_names: u128,
owner: Person
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Person {
name: felt252,
address: ContractAddress
}
#[derive(Drop, Serde, starknet::Store)]
enum RegistrationType {
finite: u64,
infinite
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.write(owner.address, owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
#[external(v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
let name = self.names.read(address);
name
}
fn get_owner(self: @ContractState) -> Person {
let owner = self.owner.read();
owner
}
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let mut total_names = self.total_names.read();
self.names.write(user, name);
self.registration_type.write(user, registration_type);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
fn get_contract_name() -> felt252 {
'Name Registry'
}
fn get_owner_storage_address(self: @ContractState) -> starknet::StorageBaseAddress {
self.owner.address()
}
}
存储自定义结构体
Store
trait,定义在 starknet::storage_access
模块中,用于指定类型如何在存储中存储。为了将类型存储在存储中,它必须实现 Store
trait。大多数来自核心库的类型,例如无符号整数 (u8
, u128
, u256
...),、felt252
、bool
、ContractAddress
等都实现了 Store
trait,因此无需进一步操作即可存储。
但是,如果您想存储您自己定义的类型,例如枚举或结构,该怎么办?在这种情况下,您必须明确地告诉编译器如何存储这种类型。
在我们的例子中,我们想将 Person
结构存储在存储中,这可以通过为 Person
类型实现 Store
特性来实现。这可以通过在结构体定义顶部简单添加 #[derive(starknet::Store)]
属性来实现。
use starknet::ContractAddress;
#[starknet::interface]
trait INameRegistry<TContractState> {
fn store_name(
ref self: TContractState, name: felt252, registration_type: NameRegistry::RegistrationType
);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
fn get_owner(self: @TContractState) -> NameRegistry::Person;
}
#[starknet::contract]
mod NameRegistry {
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
registration_type: LegacyMap::<ContractAddress, RegistrationType>,
total_names: u128,
owner: Person
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Person {
name: felt252,
address: ContractAddress
}
#[derive(Drop, Serde, starknet::Store)]
enum RegistrationType {
finite: u64,
infinite
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.write(owner.address, owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
#[external(v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
let name = self.names.read(address);
name
}
fn get_owner(self: @ContractState) -> Person {
let owner = self.owner.read();
owner
}
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let mut total_names = self.total_names.read();
self.names.write(user, name);
self.registration_type.write(user, registration_type);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
fn get_contract_name() -> felt252 {
'Name Registry'
}
fn get_owner_storage_address(self: @ContractState) -> starknet::StorageBaseAddress {
self.owner.address()
}
}
类似地,枚举也可以在实现 Store
trait的情况下写入存储,只要所有关联类型也实现 Store
trait,就可以简单地推导它。
use starknet::ContractAddress;
#[starknet::interface]
trait INameRegistry<TContractState> {
fn store_name(
ref self: TContractState, name: felt252, registration_type: NameRegistry::RegistrationType
);
fn get_name(self: @TContractState, address: ContractAddress) -> felt252;
fn get_owner(self: @TContractState) -> NameRegistry::Person;
}
#[starknet::contract]
mod NameRegistry {
use starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
names: LegacyMap::<ContractAddress, felt252>,
registration_type: LegacyMap::<ContractAddress, RegistrationType>,
total_names: u128,
owner: Person
}
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
StoredName: StoredName,
}
#[derive(Drop, starknet::Event)]
struct StoredName {
#[key]
user: ContractAddress,
name: felt252
}
#[derive(Copy, Drop, Serde, starknet::Store)]
struct Person {
name: felt252,
address: ContractAddress
}
#[derive(Drop, Serde, starknet::Store)]
enum RegistrationType {
finite: u64,
infinite
}
#[constructor]
fn constructor(ref self: ContractState, owner: Person) {
self.names.write(owner.address, owner.name);
self.total_names.write(1);
self.owner.write(owner);
}
#[external(v0)]
impl NameRegistry of super::INameRegistry<ContractState> {
fn store_name(ref self: ContractState, name: felt252, registration_type: RegistrationType) {
let caller = get_caller_address();
self._store_name(caller, name, registration_type);
}
fn get_name(self: @ContractState, address: ContractAddress) -> felt252 {
let name = self.names.read(address);
name
}
fn get_owner(self: @ContractState) -> Person {
let owner = self.owner.read();
owner
}
}
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
fn _store_name(
ref self: ContractState,
user: ContractAddress,
name: felt252,
registration_type: RegistrationType
) {
let mut total_names = self.total_names.read();
self.names.write(user, name);
self.registration_type.write(user, registration_type);
self.total_names.write(total_names + 1);
self.emit(StoredName { user: user, name: name });
}
}
fn get_contract_name() -> felt252 {
'Name Registry'
}
fn get_owner_storage_address(self: @ContractState) -> starknet::StorageBaseAddress {
self.owner.address()
}
}
结构体的存储布局
在 Starknet 上,结构会被存储为基本类型序列。结构的元素按照结构定义中的顺序进行存储。结构的第一个元素存储在结构的基地址,该地址根据 存储地址 中的说明计算,可以通过调用 var.address()
获取,后面的元素则存储在与第一个元素相邻的地址。
例如,类型为 Person
的 owner
变量的存储布局将如下所示:
Fields | Address |
---|---|
name | owner.address() |
address | owner.address() +1 |
枚举的存储布局
当您存储一个枚举变体时,您实际上存储的是变体的索引和最终的关联值。这个索引从您的枚举的第一个变体开始为 0,并为每个后续变体增加 1。
如果您的变体具有关联值,它会从基地址之后的第一个地址开始存储。
例如,假设我们有一个带有 finite
变体的 RegistrationType
枚举,该变体带有关联的截止日期。存储布局将如下所示:
Element | Address |
---|---|
Variant index (e.g. 1 for finite) | registration_type.address() |
Associated limit date | registration_type.address() + 1 |
存储映射
存储映射类似于哈希表,它们允许将键映射到值。但是,与典型的哈希表不同,不存储键数据本身 - 仅使用其哈希码在合约的存储中查找关联的值。 映射没有长度概念,也没有键/值对是否设置的概念。删除映射的唯一方法是将其值设置为默认的零值。
映射只用于根据某些键计算合约存储中数据的位置。因此,它们只能作为存储变量使用。它们不能用作合约函数的参数或返回值参数,也不能用作结构体内的类型。
要声明一个映射,请使用尖括号 <>
包围的 LegacyMap
类型,指定键和值类型。
你还可以创建具有多个键的更复杂的映射。在示例 99-2bis 中可以找到一个示例,就像ERC20标准中的流行的 allowances
存储变量一样,它使用多个键将 owner
和允许的 spender
映射到其 allowance
金额,这些键被传递到一个元组中:
#[storage]
struct Storage {
allowances: LegacyMap::<(ContractAddress, ContractAddress), u256>
}
存储在映射中的变量的存储地址根据 存储地址 部分的描述进行计算。
如果映射的键是一个结构体,则结构体的每个元素都构成一个键。此外,结构体应该实现 Hash
trait,可以使用 #[derive(Hash)]
属性派生。例如,如果您的结构体有两个字段,则地址将是 h(h(sn_keccak(variable_name),k_1),k_2)
- 其中 k_1
和 k_2
是结构体这两个字段的值。
类似地,对于嵌套映射,例如 LegacyMap((ContractAddress, ContractAddress), u8)
,地址将以相同的方式计算:h(h(sn_keccak(variable_name),k_1),k_2)
。
如果你想了解更多关于合约存储布局的细节,可以访问 Starknet 文档