How to Use DynArray Storage in GenLayer Contracts: A Complete Guide
DynArray is the deterministic, dynamic-array storage type used in GenLayer contracts to provide O(1) appends and persistent index-based access for data verified by the GenVM.
GenLayer contracts rely on specialized collection types for deterministic on-chain storage that survives across transactions. The DynArray type, available in the genlayerlabs/genlayer-project-boilerplate repository, acts like a Python list but is fully compatible with the GenVM linter and runtime. Learning how to use DynArray storage in GenLayer contracts ensures your state remains valid, persistent, and accepted by the GenVM.
Why Choose DynArray for GenLayer Contract Storage
The GenVM tracks every element stored in a DynArray and automatically persists the array’s length in the contract’s state. This guarantees that data remains available across transactions without manual serialization.
Key characteristics of DynArray storage include:
- O(1) append and O(1) access by index, making it efficient for sequential data.
- Automatic length tracking by the VM, which updates the stored metadata on every mutation.
- Deterministic behavior required for consensus, unlike native Python collections.
As documented in the repository, native Python types such as list and dict are rejected by the linter. The README.md explicitly notes that invalid storage types must be replaced with approved types such as TreeMap, DynArray, or u256 (see README.md#L64-L66). Similarly, CLAUDE.md lists the supported storage types and explains the linter’s restrictions (CLAUDE.md#L72-L73).
Declaring a DynArray with Storage-Compatible Types
To store custom objects inside a DynArray, you must decorate the class with @allow_storage and @dataclass. The generic syntax DynArray[<type>] declares the element type for the compiler.
from genlayer import *
@allow_storage
@dataclass
class Item:
id: u256
name: str
class MyContract(gl.Contract):
items: DynArray[Item] # Declaration of a dynamic array of `Item`
The type parameter inside DynArray[...] must be a storage-compatible class. Built-in types such as u256 are also valid. Without @allow_storage, the GenVM linter will refuse to store the custom class in contract state.
Appending and Reading Elements
New elements are added with append(), which executes in constant time. Reading by index uses standard bracket notation, and the VM performs automatic bounds checking that reverts the transaction on an out-of-range access.
@gl.public.write
def add_item(self, item_id: u256, item_name: str) -> None:
new_item = Item(id=item_id, name=item_name)
self.items.append(new_item) # O(1) push
@gl.public.view
def get_item(self, index: u256) -> Item:
# Bounds checking is automatic; out‑of‑range raises a revert
return self.items[index]
Because the VM does not perform implicit type coercion, you should validate inputs before appending them to the array.
Iterating Over and Removing Elements
DynArray implements the Python iterator protocol, so you can safely loop over it inside view methods. The type also supports pop() to remove the last element and remove_at(index) to delete a specific index and shift subsequent elements left. Both operations update the stored length automatically.
@gl.public.view
def list_names(self) -> list[str]:
return [item.name for item in self.items]
@gl.public.write
def pop_last(self) -> None:
self.items.pop() # Removes the final element
When you need keyed lookups rather than ordered traversal, the repository’s example contracts recommend using TreeMap instead. The contracts/football_bets.py file demonstrates indexed storage patterns (contracts/football_bets.py#L22-L25) that apply equally to DynArray when choosing the right collection for your data layout.
Complete DynArray Contract Example
The following ScoreBoard contract, modeled after contracts/dynarray_example.py, shows the full lifecycle of a DynArray: declaration, append, length inspection, index lookup, and removal.
# contracts/dynarray_example.py
from genlayer import *
@allow_storage
@dataclass
class Score:
player: Address
points: u256
class ScoreBoard(gl.Contract):
# A dynamic array that stores Score objects
scores: DynArray[Score]
@gl.public.write
def record_score(self, player: str, pts: u256) -> None:
addr = Address(player)
self.scores.append(Score(player=addr, points=pts))
@gl.public.view
def total_players(self) -> u256:
return self.scores.length # length property is always available
@gl.public.view
def get_score(self, idx: u256) -> Score:
return self.scores[idx]
@gl.public.write
def remove_last(self) -> None:
self.scores.pop()
This example follows the repository’s pattern of separating write methods (record_score, remove_last) from view methods (total_players, get_score) to keep state mutations explicit.
Testing DynArray Storage Operations
You can exercise the contract in direct test mode by deploying the file and calling its methods through the test fixtures provided in the boilerplate.
def test_dynarray(direct_vm, direct_deploy, direct_alice):
contract = direct_deploy("contracts/dynarray_example.py")
direct_vm.sender = direct_alice
contract.record_score("0x1111111111111111111111111111111111111111", 10)
contract.record_score("0x2222222222222222222222222222222222222222", 20)
assert contract.total_players() == 2
assert contract.get_score(0).points == 10
contract.remove_last()
assert contract.total_players() == 1
The test confirms that append() increases the length, bracket indexing returns the correct element, and pop() decrements the stored count.
Storage Best Practices for GenLayer Contracts
Follow these rules to keep your DynArray usage deterministic and linter-compliant:
- Never store native Python collections such as
listordict. The GenVM linter rejects them at compile time, as enforced by the rules inCLAUDE.mdandREADME.md. - Always apply
@allow_storageto any custom class you intend to persist inside aDynArray. - Validate all inputs before appending. The VM does not coerce types automatically.
- Prefer
TreeMapfor keyed lookups and reserveDynArrayfor ordered, index-based data, aligning with the design shown incontracts/football_bets.py.
Summary
- DynArray is the GenVM-approved dynamic array for persistent, deterministic contract storage.
- Declare it with
DynArray[<type>]and decorate custom classes using@allow_storage. - Use
append()for O(1) inserts, bracket indexing for reads, andpop()orremove_at()for deletions. - Native Python collections are forbidden; consult
CLAUDE.mdandREADME.mdfor the approved type list. - Write explicit tests using the boilerplate’s direct-mode fixtures to verify length and element access.
Frequently Asked Questions
What happens if I use a native Python list instead of DynArray in a GenLayer contract?
The GenVM linter rejects native Python collections at compile time. According to the storage rules documented in CLAUDE.md#L72-L73 and README.md#L64-L66, you must replace list and dict with approved types such as DynArray or TreeMap.
Does DynArray support arbitrary Python types?
No. The generic parameter in DynArray[<type>] must be a storage-compatible type, such as u256 or a custom class decorated with @allow_storage and @dataclass. Arbitrary Python objects that lack these annotations cannot be persisted in contract state.
How do I check the number of elements in a DynArray?
Access the length property on the array instance. The VM updates this value automatically on every append(), pop(), or remove_at() operation, so it always reflects the current stored element count.
Is DynArray or TreeMap better for keyed lookups in GenLayer?
Use TreeMap when you need keyed or associative lookups. Reserve DynArray for ordered, index-based data, as demonstrated by the storage patterns in contracts/football_bets.py.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →