Best Practices for Structuring Large GenLayer Contracts: A Complete Guide
Structure GenLayer contracts with dedicated modules, deterministic storage types, encapsulated non-deterministic logic, and strict equivalence principles to maintain scalability and consensus safety.
GenLayer introduces a unique execution model where Python contracts run in a deterministic-plus-non-deterministic virtual machine (GenVM). As projects grow beyond simple prototypes, architectural discipline becomes critical for consensus correctness, testability, and long-term maintainability. This guide distills proven patterns from the genlayerlabs/genlayer-project-boilerplate repository, showing exactly how to organize large GenLayer contracts with concrete code examples and source file references.
Separate Concerns with Dedicated Contract Modules
Each contract file should own a single responsibility. The boilerplate demonstrates this through clear file boundaries:
- Domain contracts like
contracts/football_bets.pyencapsulate complete business logic for specific use cases - Pattern reference contracts like
contracts/PatternTest.pyserve as living documentation for SDK capabilities
This separation helps the genvm-lint tooling catch issues early and makes unit testing straightforward. When a contract grows beyond 200 lines, consider splitting storage structures and helper logic into separate internal modules.
Use Explicit Deterministic Storage Types
GenLayer only persists VM-compatible containers. Avoid Python native types like list or dict in contract state. Instead, use these built-in primitives from the GenLayer SDK:
| Type | Purpose | Example from Source |
|---|---|---|
TreeMap[Key, Value] |
Sorted key-value mappings | bets: TreeMap[Address, TreeMap[str, Bet]] |
u256 |
Unsigned 256-bit integers for counters/balances | points: TreeMap[Address, u256] |
DynArray[T, N] or Array[T, N] |
Fixed or dynamic ordered collections | Use instead of Python list |
In contracts/football_bets.py (lines 22-25), the storage declaration follows this pattern precisely:
class FootballBets(gl.Contract):
bets: TreeMap[Address, TreeMap[str, Bet]]
Nested TreeMap structures enable complex relationships—here, bets grouped first by user address, then by bet identifier—without sacrificing determinism.
Leverage @allow_storage for Custom Data Classes
Rich domain objects require explicit opt-in for persistence. Wrap data in @dataclass with the @allow_storage decorator to signal VM compatibility:
@allow_storage
@dataclass
class Bet:
id: str
has_resolved: bool
game_date: str
resolution_url: str
team1: str
team2: str
predicted_winner: str
real_winner: str
real_score: str
This definition appears in contracts/football_bets.py (lines 8-20). The decorator ensures the GenVM can serialize and deserialize Bet instances deterministically across all nodes in the consensus set.
Encapsulate Non-Deterministic Logic in Private Helpers
Web fetches and LLM prompts introduce external variability. Isolate these calls in private methods (no decorator) to enable test mocking and keep public interfaces deterministic:
def _check_match(self, resolution_url: str, team1: str, team2: str) -> dict:
def get_match_result() -> str:
web_data = gl.nondet.web.render(resolution_url, mode="text")
task = f"""
Extract the match result for:
Team 1: {team1}
Team 2: {team2}
Web content:
{web_data}
Respond in JSON: {{ "score": str, "winner": int }}
"""
result = gl.nondet.exec_prompt(task, response_format="json")
return json.dumps(result, sort_keys=True)
result_json = json.loads(gl.eq_principle.strict_eq(get_match_result))
return result_json
This implementation from contracts/football_bets.py (lines 29-55) demonstrates the pattern: an inner closure captures non-deterministic operations, while the outer method applies equivalence principles and returns parsed results.
Apply Equivalence Principles Consistently
All non-deterministic leader functions must be wrapped with an equivalence principle. The boilerplate uses gl.eq_principle.strict_eq to ensure all replicas reach identical conclusions:
result_json = json.loads(gl.eq_principle.strict_eq(get_match_result))
This line (lines 54-55 in contracts/football_bets.py) is the critical consensus checkpoint. Alternative principles like eq_principle.eq or custom equivalence functions exist for specialized use cases, but strict_eq provides the strongest guarantee for JSON-structured data.
Expose a Minimal Public API
Use GenLayer decorators sparingly and intentionally:
@gl.public.writefor state-mutating operations:create_bet,resolve_bet@gl.public.viewfor read-only queries:get_bets,get_points,get_player_points- No decorator for internal helpers:
_check_match
This boundary appears throughout contracts/football_bets.py (lines 57-119). Keeping the public surface small reduces attack vectors and simplifies frontend integration.
Write Deterministic Unit Tests with Direct Mode
The tests/direct/ directory contains tests that execute contracts locally without network dependencies. Use direct_vm.mock_web and direct_vm.mock_llm to stub non-deterministic layers:
def test_create_bet(direct_vm, direct_deploy, direct_alice):
contract = direct_deploy("contracts/football_bets.py")
direct_vm.sender = direct_alice
# Mock the web page containing fixture lists
direct_vm.mock_web(r".*bbc.com/sport/football.*", {
"status": 200,
"body": "<html>…</html>"
})
# Mock LLM response for match extraction
direct_vm.mock_llm(r".*Extract the match result.*", {
"winner": -1,
"score": "-"
})
contract.create_bet("2024-10-01", "TeamA", "TeamB", "TeamA")
assert "teama_teamb" in contract.get_bets()[direct_alice.as_hex]
Direct-mode tests validate contract logic in milliseconds, enabling tight feedback loops during development.
Centralize Configuration in a Single Entry Point
Deployment-specific values belong exclusively in config/genlayer_config.py. Import this module rather than scattering literals across contracts. This pattern ensures environment parity between local testing, staging, and production deployments.
Document Patterns in a Living Reference Contract
The contracts/PatternTest.py file serves as executable documentation for common SDK patterns:
u256arithmetic and overflow handlingAddresscomparisons and conversions- JSON stability techniques
- Nested
TreeMapworkarounds for advanced data structures
Refer to this contract when encountering unfamiliar GenLayer behaviors rather than experimenting in production code.
Keep Frontend Code Fully Decoupled
The Next.js frontend in frontend/ consumes contracts through generated TypeScript bindings like frontend/lib/contracts/FootballBets.ts. This abstraction allows contract interfaces to evolve while frontend teams update bindings independently.
Summary
- Modular structure prevents monolithic contracts—separate domain logic, utilities, and patterns into dedicated files
- Deterministic storage requires
TreeMap,u256, and@allow_storagedataclasses—never use raw Python containers - Non-deterministic isolation through private helper methods enables testability and equivalence principle application
- Strict equivalence via
gl.eq_principle.strict_eqguarantees consensus for all external calls - Minimal public API decorated with
@gl.public.writeand@gl.public.viewreduces complexity and attack surface - Direct-mode testing with mocked web and LLM layers provides fast, deterministic validation
- Centralized configuration in
config/genlayer_config.pymaintains environment consistency
Frequently Asked Questions
What storage types does GenLayer support?
GenLayer supports TreeMap for sorted mappings, u256 for unsigned integers, and DynArray/Array for ordered collections. Python native types like list and dict cannot be persisted in contract state. The GenVM enforces these restrictions to guarantee deterministic serialization across all consensus nodes.
How do I make custom classes storable in GenLayer?
Decorate your @dataclass with @allow_storage before the dataclass decorator. This signals the GenVM compiler to generate serialization logic. All fields must themselves be storable types—nested custom classes each require their own @allow_storage annotation.
Why must non-deterministic logic be isolated?
Non-deterministic operations (web requests, LLM prompts) produce different results across nodes and time. Isolating them in private methods allows wrapping with equivalence principles for consensus, enables mocking in direct-mode tests, and prevents accidental exposure through public APIs. The contracts/football_bets.py implementation demonstrates this pattern in the _check_match method.
What is the equivalence principle and when should I use strict_eq?
The equivalence principle defines how replicas agree on non-deterministic results. gl.eq_principle.strict_eq requires bitwise-identical responses—ideal for JSON with sorted keys. Other principles like eq_principle.eq allow semantic equivalence for more flexible matching. Apply the strictest principle that your use case permits to maximize consensus safety.
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 →