Persisting data on the blockchain

Usual data structures aren't suitable for representing blockchain persistent storage:

  1. Allocated addresses (id in python terms) are not persistent
  2. Allocation requires knowledge about all allocated addresses, which takes a lot of space and would cost a lot of reads at start time
  3. 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 with DynArray[T]
  • dict[K, V] needs to be replaced with TreeMap[K, V]
  • int type isn't supported on purpose. You most likely wish to use some fixed-size integer type, such as i32 or u256. If this is not the case and you are sure that you need big integers, you can annotate your field with bigint, which is just an alias for python int
⚠️

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!