Persisting data on the blockchain
Usual data structures aren't suitable for representing blockchain persistent storage:
- Allocated addresses (
id
in python terms) are not persistent - Allocation requires knowledge about all allocated addresses, which takes a lot of space and would cost a lot of reads at start time
- Serialization works poorly as it will rewrite entire storage (consider rehash)
Intelligent Contracts store data publicly on chain, attached to their account's address. The storage starts zero-initialized until a contract is deployed initializes a state.
For storage declaration GenLayer uses contract class fields.
All persistent fields must be declared in the class body and annotated with types.
Fields declared outside the class body by creating new instance variables (self.field = value
) are not persistent and will be discarded after the contract execution.
Example:
@gl.contract
class PersistentContract:
minter: Address
def __init__(self):
self.minter = gl.message.sender_account
In your contracts, you can use any Python types, but for persisted fields, there are some restrictions:
list[T]
needs to be replaced withDynArray[T]
dict[K, V]
needs to be replaced withTreeMap[K, V]
int
type isn't supported on purpose. You most likely wish to use some fixed-size integer type, such asi32
oru256
. If this is not the case and you are sure that you need big integers, you can annotate your field withbigint
, which is just an alias for pythonint
Only fully instantiated generic types can be used, so TreeMap
is forbidden, while TreeMap[str, u256]
is not
Simple examples:
@gl.contract
class PersistentContract:
a: str
b: bytes
# c: list[str] # ❌ `list` is forbidden!
c: DynArray[str]
# b: dict[Address, u256] # ❌ `dict` is forbidden!
# b: TreeMap # ❌ only fully specialized generic types are allowed!
b: TreeMap[Address, u256]
# d: int # ❌ `int` is forbidden
d: bigint # ⚠️ most likely you don't need an arbitrary big integer
d_sized: i256
Few words about DynArray
and TreeMap
These types implement python collections.abc.MutableSequence
and collections.abc.MutableMapping
which makes them compatible with most of the python code
They can be encoded into calldata as-is as well, which means that following code is correct:
@gl.contract
class PersistentContract:
storage: DynArray[str]
@gl.public.view
def get_complete_storage(self) -> collections.abc.Sequence[str]:
return self.storage
Calldata format supports mappings only with str
keys, like JSON does.
Using custom data types
In the future it may be required to decorate classes that are used in the storage
You can use other python classes in storage, for example:
@dataclass
class User:
name: str
birthday: datetime.datetime
@gl.contract
class Contract:
users: DynArray[User]
Differences from regular python types
Even though storage classes mimic python types, remember that they provide you only with a view on memory, not actual data that is "here". For example, consider the above example
self.users.append(User("Ada"))
user = self.users[-1]
self.users[-1] = User("Definitely not Ada", datetime.datetime.now())
assert user.name == "Definitely not Ada" # this is true!