How to Use TreeMap Storage in GenLayer Contracts: Declaration, Nested Maps, and JSON Workarounds
TreeMap storage in GenLayer contracts is a deterministic, persistent key-value container declared with generic syntax TreeMap[KeyType, ValueType] and managed through lazy initialization, direct assignment, and safe read helpers.
GenLayer contracts store mutable data in deterministic containers that the GenVM can serialize and replay across nodes. The primary mapping primitive for this state is TreeMap, which acts like a persistent dictionary with strict type requirements. This guide explains the exact patterns found in the genlayerlabs/genlayer-project-boilerplate repository for declaring, writing to, and reading from TreeMap storage in real-world contracts.
What Is TreeMap Storage in GenLayer?
TreeMap is the canonical key-value store for on-chain state in GenLayer. It guarantees deterministic ordering, ensuring every node produces an identical state hash during execution. This property supports efficient Merkle-based verification and proof generation.
The GenVM linter explicitly rejects native Python dict and list types for contract storage. According to the boilerplate's README.md, using plain Python collections triggers a lint error under the invalid storage types rules. TreeMap is the accepted abstraction for mapping hashable keys to valid storage values.
Declaring TreeMap Storage in a Contract
You declare a TreeMap field at the class level using generic bracket syntax: TreeMap[KeyType, ValueType]. Keys must be hashable SDK types such as Address, str, or int. Values can be any valid storage type, including nested TreeMap, DynArray, u256, or an @allow_storage dataclass.
In contracts/football_bets.py, the contract stores user bets with a nested map structure:
# contracts/football_bets.py (lines 23-24)
bets: TreeMap[Address, TreeMap[str, Bet]]
points: TreeMap[Address, u256]
This pattern maps each Address to an inner TreeMap that holds Bet objects keyed by a str identifier.
Initializing and Writing to TreeMap Storage
Lazy Instantiation with get_or_insert_default
TreeMap objects are lazily instantiated. You do not need an explicit constructor call to create them. When you need to ensure a nested map exists before writing, use get_or_insert_default(key).
In contracts/football_bets.py (lines 87-88), the contract initializes a user's bet map the first time they create a bet:
# contracts/football_bets.py (lines 87-88)
self.bets.get_or_insert_default(gl.message.sender_address)[bet_id] = bet
This single expression guarantees the outer map contains an empty TreeMap[str, Bet] for the sender before assigning the bet.
Direct Assignment
For simple updates, assign directly using bracket notation. Because the GenVM tracks every state change, each assignment persists on-chain automatically.
self.profiles[addr] = Profile(name=name, age=age)
Reading from TreeMap Storage
You can read values using standard bracket syntax or the get helper. The bracket syntax raises an error if the key is absent, while get returns None for missing keys.
# Returns None if the address has no profile
profile = self.profiles.get(Address(addr))
# Direct lookup; ensure the key exists or handle exceptions
bet = self.bets[Address(bettor)][bet_id]
Advanced Pattern: Storing Lists Inside a TreeMap
GenLayer does not allow native Python list objects inside TreeMap values. The contracts/PatternTest.py file demonstrates a workaround at lines 101-114: serialize the list to a JSON string before storage.
import json
@gl.public.write
def add_item(self, key: str, item: str) -> str:
# Retrieve the JSON-encoded list, defaulting to empty
lst = json.loads(self.index.get(key) or "[]")
lst.append(item)
# Store back as a JSON string
self.index[key] = json.dumps(lst)
return self.index[key]
@gl.public.view
def get_items(self, key: str) -> list:
return json.loads(self.index.get(key) or "[]")
This pattern lets you store variable-length collections inside a TreeMap[str, str] while remaining fully compliant with GenVM storage rules.
Complete Example: Profiles and Nested Bets
The following contract combines simple and nested TreeMap patterns with @allow_storage dataclasses:
# contracts/example.py
from genlayer import *
@allow_storage
@dataclass
class Profile:
name: str
age: u256
class ExampleContract(gl.Contract):
# Simple map: address → Profile
profiles: TreeMap[Address, Profile]
# Nested map: address → (bet_id → Bet)
bets: TreeMap[Address, TreeMap[str, Bet]]
def __init__(self):
# No explicit constructor needed; TreeMap is created on first use
pass
@gl.public.write
def set_profile(self, name: str, age: u256):
addr = gl.message.sender_address
self.profiles[addr] = Profile(name=name, age=age)
@gl.public.view
def get_profile(self, addr: str) -> dict:
# Return a plain dict for easier consumption by front-end
profile = self.profiles.get(Address(addr))
if not profile:
return {}
return {"name": profile.name, "age": int(profile.age)}
@gl.public.write
def add_bet(self, bet_id: str, bet: Bet):
# Ensure a nested TreeMap exists for the sender
self.bets.get_or_insert_default(gl.message.sender_address)[bet_id] = bet
@gl.public.view
def get_bet(self, bettor: str, bet_id: str) -> dict:
bet = self.bets[Address(bettor)][bet_id]
return {
"id": bet.id,
"team1": bet.team1,
"team2": bet.team2,
"predicted_winner": bet.predicted_winner,
}
Why TreeMap Is Required Over Native Python Collections
- Deterministic ordering guarantees that the same state hash is produced on every node.
- Merkle efficiency supports proof-generation for on-chain verification.
- Linter compliance ensures the GenVM accepts your contract; native Python dictionaries and lists cause compile-time errors as documented in the repository's
README.md.
Summary
- Declare
TreeMap[KeyType, ValueType]at the class level for any persistent key-value state. - Initialize nested maps lazily with
get_or_insert_default(key)to avoid missing-key errors. - Read safely with
get()for optional keys, or use bracket notation when the key must exist. - Store complex collections by serializing them to strings (e.g., JSON) when native types are unsupported.
- Avoid native Python
dictandlistin storage fields; the GenVM linter rejects them.
Frequently Asked Questions
What types can I use as TreeMap keys in GenLayer?
TreeMap keys must be hashable types supported by the GenLayer SDK. Common choices include Address, str, and int. Values can be any valid storage type such as u256, DynArray, nested TreeMap, or an @allow_storage dataclass.
How do I initialize a nested TreeMap for a new user?
Call get_or_insert_default(key) on the outer TreeMap. As shown in contracts/football_bets.py (lines 87-88), this method returns the existing value or creates a default empty map for that key, allowing immediate chained assignment.
Can I store a Python list directly inside a TreeMap?
No. Native Python list is not a valid GenVM storage type. As implemented in contracts/PatternTest.py (lines 101-114), you can JSON-serialize the list into a str value, store it in a TreeMap[str, str], and deserialize it on retrieval.
Does TreeMap require an explicit constructor in GenLayer contracts?
No. TreeMap fields are lazily instantiated on first access. You can declare them at the class level without initialization logic inside __init__, and they will be created automatically when you first read from or write to them.
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 →