How to Debug Failed Transactions in GenLayer: A Complete Guide for Smart Contract Developers

Use direct-mode testing, gl.vm.log diagnostics, and expect_revert assertions to identify and fix transaction reverts in GenVM contracts.

This guide walks through proven debugging strategies for the GenLayer project boilerplate, where contracts run on the GenVM with deterministic execution and explicit failure modes. Whether you're hitting equivalence-principle validation errors or non-deterministic call failures, these techniques will help you trace the root cause quickly.


Understanding GenLayer Transaction Failure Modes

Before debugging, identify which failure mode you're encountering. The boilerplate's contracts/football_bets.py demonstrates four common revert sources:

Failure source Typical symptom Location in codebase
Explicit raise Exception or gl.vm.UserError Transaction reverts with custom message Lines rejecting duplicate bets or already-resolved bets
Equivalence-principle validation gl.eq_principle.strict_eq throws on leader/validator mismatch _check_match helper validating web-scraped results
Non-deterministic call failures gl.nondet.* reverts on validation or external error Web rendering and LLM execution in _check_match
Contract storage errors KeyError or missing key triggers revert Accessing self.bets[gl.message.sender_address][bet_id] when absent

The revert message is your first diagnostic. The boilerplate's test suite uses direct_vm.expect_revert to assert specific errors:


# From tests/direct/test_resolve_bet.py

with direct_vm.expect_revert("Bet already resolved"):
    contract.resolve_bet(bet_id)

Step-by-Step Debugging Workflow for GenLayer Failed Transactions

1. Run Direct-Mode Tests for Fast Iteration

Direct-mode tests execute contracts in-memory without network overhead. This is the fastest way to reproduce and isolate failures.

pytest tests/direct/ -v

Insert print statements or inspect direct_vm fields (e.g., direct_vm.sender) to examine VM state mid-execution. The conftest.py file provides the direct_vm fixture that powers this workflow.

2. Add In-Contract Diagnostic Logging

Use gl.vm.log inside contract methods to trace execution paths and inspect call context:

@gl.public.write
def create_bet(self, game_date: str, team1: str, team2: str, predicted_winner: str):
    gl.vm.log(f"[DEBUG] create_bet called by {gl.message.sender_address}")
    gl.vm.log(f"[DEBUG] msg.value: {gl.message.value}, block: {gl.message.block_number}")
    # existing logic...

Available context fields include gl.message.sender_address, gl.message.value, gl.message.block_number, and gl.message.timestamp.

3. Validate Non-Deterministic Calls with Equivalence Principle

The _check_match helper in football_bets.py shows the correct pattern: wrap non-deterministic calls in gl.eq_principle.strict_eq to enforce leader/validator agreement:

result_json = json.loads(gl.eq_principle.strict_eq(get_match_result))

If leader and validator outputs differ, the SDK raises gl.vm.UserError with a descriptive message. Wrap this in try/catch to add custom logging:

try:
    result_json = json.loads(gl.eq_principle.strict_eq(get_match_result))
except gl.vm.UserError as e:
    gl.vm.log(f"[EQ-FAIL] {e}")
    raise

4. Write Explicit Revert Expectations

When you anticipate a failure, use expect_revert to document the behavior and prevent regressions:

def test_duplicate_bet(direct_vm, direct_deploy, direct_alice):
    contract = direct_deploy("contracts/football_bets.py")
    direct_vm.sender = direct_alice
    contract.create_bet("2024-09-01", "TeamA", "TeamB", "TeamA")
    
    with direct_vm.expect_revert("Bet already created"):
        contract.create_bet("2024-09-01", "TeamA", "TeamB", "TeamA")

5. Verify Storage State Persistence After Reverts

GenVM transactions are atomic—storage changes roll back on revert. Use view functions to confirm state integrity:

points_before = contract.get_player_points(alice_addr)
with direct_vm.expect_revert("Bet already created"):
    contract.create_bet(...)
points_after = contract.get_player_points(alice_addr)
assert points_before == points_after  # State unchanged

Practical Code Examples for Debugging GenLayer Transactions

Debugging a Failed Resolve with Mocked Web Data

def test_resolve_unfinished_game(direct_vm, direct_deploy, direct_alice):
    contract = direct_deploy("contracts/football_bets.py")
    direct_vm.sender = direct_alice
    
    # Setup: create a bet for a future date

    contract.create_bet("2025-01-01", "TeamX", "TeamY", "TeamX")
    bet_id = "2025-01-01_teamx_teamy"
    
    # Mock web response to simulate unfinished match

    direct_vm.mock_web(
        r".*bbc.com.*", 
        {"status": 200, "body": "Match not finished yet"}
    )
    
    # Expect revert with specific message

    with direct_vm.expect_revert("Game not finished"):
        contract.resolve_bet(bet_id)
    
    # Verify no points were awarded due to revert

    assert contract.get_player_points(direct_alice) == 0

Inspecting Equivalence-Principle Failures

def _check_match(self, resolution_url: str, team1: str, team2: str) -> dict:
    def get_match_result() -> str:
        html = gl.nondet.web_render(f"{resolution_url}/football")
        # LLM extraction logic...

        return gl.nondet.llm(_extract_prompt, html)
    
    # Detailed error capture for debugging

    try:
        result_json = json.loads(gl.eq_principle.strict_eq(get_match_result))
    except gl.vm.UserError as e:
        gl.vm.log(f"[EQ-PRINCIPLE FAILED] url={resolution_url}, error={e}")
        raise gl.vm.UserError(f"Validation failed for {team1} vs {team2}: {e}")
    
    return result_json

Key Files for Debugging Failed Transactions in GenLayer

File Purpose
contracts/football_bets.py Core contract with explicit revert points: duplicate bets, resolved bets, unfinished games
tests/direct/test_create_bet.py Direct-mode tests demonstrating "Bet already created" error
tests/direct/test_resolve_bet.py Tests for "Bet already resolved" and "Game not finished" reverts
tests/direct/conftest.py Fixtures: direct_vm, direct_deploy, test accounts
config/genlayer_config.py SDK configuration for local VM execution

Summary

  • Run pytest tests/direct/ for rapid, in-memory debugging of failed transactions without network latency
  • Insert gl.vm.log calls to trace execution and inspect gl.message context fields
  • Wrap non-deterministic calls in gl.eq_principle.strict_eq and handle gl.vm.UserError for detailed failure diagnostics
  • Use direct_vm.expect_revert to document expected failures and prevent regression
  • Verify storage with view functions after reverts to confirm atomic rollback behavior

Frequently Asked Questions

Why does my GenLayer transaction revert with "Validation failed"?

This indicates an equivalence-principle mismatch. In contracts/football_bets.py, the gl.eq_principle.strict_eq wrapper requires leader and validator nodes to produce identical outputs for non-deterministic calls. Add gl.vm.log inside your wrapped function to inspect diverging results, or mock responses with direct_vm.mock_web for deterministic testing.

How do I test a specific revert message in GenLayer?

Use the direct_vm.expect_revert context manager from the test fixtures. Pass the exact expected string: with direct_vm.expect_revert("Bet already created"):. This asserts both that the transaction fails and that the error message matches, as demonstrated in tests/direct/test_create_bet.py.

Can I inspect state after a transaction reverts in GenLayer?

Yes—state is automatically rolled back. Since GenVM transactions are atomic, any storage modifications are discarded on revert. Query view functions like get_player_points or get_bets immediately after an expect_revert block to verify the pre-transaction state remains intact, as shown in the storage verification examples above.

What's the difference between raise Exception and gl.vm.UserError in GenLayer contracts?

Both trigger reverts, but gl.vm.UserError is the SDK-native exception class that integrates with GenVM's logging and debugging infrastructure. The boilerplate uses gl.vm.UserError for explicit reverts that need to surface to callers, while Python built-in exceptions may also be caught and wrapped by the VM depending on context.

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 →