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.logcalls to trace execution and inspectgl.messagecontext fields - Wrap non-deterministic calls in
gl.eq_principle.strict_eqand handlegl.vm.UserErrorfor detailed failure diagnostics - Use
direct_vm.expect_revertto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →