Common Linting Rules in GenLayer Contracts: Enforcing Determinism with genvm-lint

The GenVM linter (genvm-lint) enforces over 20 rules—including forbidden imports, storage type validation, and mandatory equivalence-principle blocks for non-deterministic calls—to ensure GenLayer contracts remain deterministic, secure, and compatible with the GenVM execution environment.

GenLayer contracts are Python-based smart contracts that run on the GenVM (Generative Virtual Machine). To prevent non-deterministic behavior and security vulnerabilities, the genlayer-project-boilerplate repository integrates the genvm-lint validation tool, which analyzes contract code against strict linting criteria before deployment. These rules are documented in the project's CLAUDE.md and README.md files, and are exemplified in the reference implementation at contracts/football_bets.py.

Forbidden Imports and Host System Access

The linter strictly prohibits imports that could access the host environment or introduce platform-specific behavior. According to the source analysis, importing modules such as os, sys, subprocess, or socket triggers immediate validation failures with the message: "Import of forbidden module 'os' detected."

This rule prevents contracts from performing file system operations, spawning processes, or creating network sockets, ensuring all validators execute identical bytecode without external dependencies.


# ❌ Forbidden - triggers forbidden imports rule

import os
import socket

# ✅ Allowed - GenLayer SDK and deterministic libraries

from genlayer import gl

Non-Deterministic Call Constraints

Any invocation of non-deterministic APIs must occur within an equivalence-principle block to guarantee consensus across the validator network. The linter validates that calls to gl.nondet.web, gl.nondet.exec_prompt, or similar APIs are wrapped in gl.vm.run_nondet or gl.eq_principle.strict_eq() contexts.

Unwrapped non-deterministic calls result in the error: "Non-deterministic call outside run_nondet block."

@gl.public.write
def fetch_price(self) -> None:
    # ✅ Correctly wrapped for consensus validation

    result = gl.vm.run_nondet(
        lambda: gl.nondet.web.get("https://api.example.com/price"),
        lambda leader, validator: leader == validator
    )
    self.price = result.body

Storage Type Validation

GenLayer contracts must use GenVM-specific storage containers rather than native Python types for persistent state. The linter enforces that state variables use TreeMap, DynArray, Array, u256, i256, or classes decorated with @allow_storage.

Using standard Python containers like dict, list, or set for contract storage produces the error: "Invalid storage type 'dict'; use TreeMap instead."

class MyContract(gl.Contract):
    # ✅ Valid GenVM storage primitives

    balances: TreeMap[Address, u256]
    history: DynArray[str]
    
    def __init__(self):
        self.balances = TreeMap()
        
    # ❌ Invalid - native Python types lack deterministic serialization

    # temp_cache: dict[str, int] = {}

Public Method Decorators and Type Safety

Every public method must declare its visibility and return type explicitly. The linter enforces two mandatory requirements:

  • Visibility Decorators: All public methods must use @gl.public.view (read-only) or @gl.public.write (state-changing).
  • Return Type Annotations: All public methods must declare return types using Python type hints (e.g., -> str, -> u256).

Violations generate: "Public method without @gl.public decorator" or "Public method lacks return type annotation."

@gl.public.view
def get_balance(self, addr: Address) -> u256:
    """Returns the balance for a given address."""
    return self.balances.get(addr, 0)

@gl.public.write
def deposit(self, amount: u256) -> None:
    """Accepts a deposit and updates state."""
    self.balances[gl.message.sender_address] += amount

Exception Handling Standards

Contracts must raise gl.vm.UserError (or subclasses) for contract-level validation failures rather than generic Python exceptions. The linter flags bare Exception raises and assert statements.

The enforced standard requires: "Use gl.vm.UserError for contract-level errors."

@gl.public.write
def withdraw(self, amount: u256) -> None:
    current_balance = self.balances.get(gl.message.sender_address, 0)
    if amount > current_balance:
        # ✅ Correct error type for deterministic failure handling

        raise gl.vm.UserError("Insufficient balance")
    self.balances[gl.message.sender_address] -= amount

Code Style and Formatting Rules

Beyond semantic validation, genvm-lint enforces PEP 8 compliance and limits line length to 120 characters to maintain readability and consistency across the codebase.

Running the GenVM Linter

To validate a contract against these 20+ rules, execute the linter from the project directory as referenced in README.md and demonstrated with contracts/football_bets.py:

genvm-lint check contracts/football_bets.py

This command performs static analysis to catch forbidden patterns, missing decorators, and type violations before deployment to the GenVM network.

Summary

  • Forbidden Imports: Blocks os, sys, subprocess, and socket to prevent host system access and ensure sandboxed execution.
  • Equivalence-Principle Wrapping: Requires gl.nondet API calls to be wrapped in gl.vm.run_nondet or gl.eq_principle.strict_eq() for validator consensus.
  • Storage Constraints: Mandates GenVM containers (TreeMap, DynArray, u256) instead of native Python dict or list for deterministic serialization.
  • Public API Contracts: Requires @gl.public.view or @gl.public.write decorators plus explicit return type annotations on all public methods.
  • Standardized Errors: Enforces gl.vm.UserError for contract logic failures instead of generic exceptions or assertions.
  • Formatting: Enforces PEP 8 style with a 120-character line limit.

Frequently Asked Questions

What happens if I use a Python dictionary instead of TreeMap in GenLayer contracts?

The linter will reject the contract with the message "Invalid storage type 'dict'; use TreeMap instead." Native Python containers like dict and list do not guarantee deterministic serialization across different Python interpreter versions, so GenLayer requires using TreeMap, DynArray, or other GenVM storage primitives that ensure consistent state across all validators.

How do I fix "Non-deterministic call outside run_nondet block" errors?

Wrap any call to gl.nondet.web, gl.nondet.exec_prompt, or similar non-deterministic APIs inside a gl.vm.run_nondet() block with an equivalence comparison function. This ensures validators can reach consensus by comparing results against the defined equivalence principle, as shown in the example contract at contracts/football_bets.py.

Why does the GenVM linter require explicit return types on public methods?

Explicit return type annotations ensure the GenVM can correctly serialize return values for cross-contract calls and consensus validation. The rule "Public method lacks return type annotation" prevents ABI mismatches and ensures consistent type handling across the distributed validator network.

Can I use assert statements for input validation in GenLayer contracts?

No. The linter flags bare assert statements and generic Exception raises. You must raise gl.vm.UserError (or a subclass) for all contract-level validation failures. This standardization allows the GenVM to distinguish between intentional contract logic failures and unexpected system errors.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →