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

> Debug failed transactions in GenLayer with direct-mode testing, gl.vm.log, and expect_revert. Learn to identify and fix contract reverts efficiently.

- Repository: [GenLayer Labs/genlayer-project-boilerplate](https://github.com/genlayerlabs/genlayer-project-boilerplate)
- Tags: how-to-guide
- Published: 2026-08-20

---

**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](https://github.com/genlayerlabs/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`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/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:

```python

# 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.

```bash
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`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/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:

```python
@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`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/football_bets.py) shows the correct pattern: wrap non-deterministic calls in `gl.eq_principle.strict_eq` to enforce leader/validator agreement:

```python
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:

```python
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:

```python
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:

```python
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

```python
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

```python
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`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) | Core contract with explicit revert points: duplicate bets, resolved bets, unfinished games |
| [`tests/direct/test_create_bet.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/tests/direct/test_create_bet.py) | Direct-mode tests demonstrating "Bet already created" error |
| [`tests/direct/test_resolve_bet.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/tests/direct/test_resolve_bet.py) | Tests for "Bet already resolved" and "Game not finished" reverts |
| [`tests/direct/conftest.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/tests/direct/conftest.py) | Fixtures: `direct_vm`, `direct_deploy`, test accounts |
| [`config/genlayer_config.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/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`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/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`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/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.