How to Debug Contract Reverts in GenLayer: A Complete Developer Guide
Use direct_vm.expect_revert("<message>") in isolated tests to reproduce and assert contract reverts, combined with temporary print() statements to inspect state before the raise clause.
GenLayer contracts are standard Python classes inheriting from gl.Contract. When a contract method raises an exception, the GenVM rolls back the transaction and returns a revert containing the exception message. This guide covers the exact workflow for debugging these reverts in the genlayerlabs/genlayer-project-boilerplate repository.
Understanding How GenLayer Reverts Work
Every GenLayer contract revert originates from a Python raise statement. The GenVM catches this exception, discards all state changes, and surfaces the message to the caller.
In contracts/football_bets.py, three distinct revert paths exist:
create_betraisesBet already createdwhen duplicate bets are attemptedresolve_betraisesBet already resolvedfor double-resolution attemptsresolve_betraisesGame not finishedwhen match data shows incomplete status
These reverts protect contract integrity by enforcing business rules at the code level.
Locating Revert Sources in Contract Code
The first debugging step is matching the revert message to its source. Search the contract for the exact string returned in the transaction receipt.
Example: Finding the "Bet Already Created" Revert
In contracts/football_bets.py, lines 73-75 implement this check:
if bet_id in self.bets[gl.message.sender_address]:
raise Exception("Bet already created")
Example: Finding the "Bet Already Resolved" Revert
Lines 91-93 in the same file handle this case:
if bet.has_resolved:
raise Exception("Bet already resolved")
Line 97-99 adds another guard:
if not is_finished:
raise Exception("Game not finished")
Use grep "Your revert message" or your IDE's search to jump directly to the triggering condition.
Reproducing Reverts with Direct-Mode Tests
Direct tests run contracts in-memory without blockchain dependencies. This isolation lets you mock external calls—web fetches, LLM prompts—and verify exact revert behavior.
The expect_revert Context Manager
The test harness provides direct_vm.expect_revert("<message>") as a context manager. Any revert with a matching message passes the test; mismatches or missing reverts fail.
In tests/direct/test_resolve_bet.py, lines 74-85 demonstrate testing double-resolution:
def test_resolve_already_resolved_fails(direct_vm, direct_deploy, direct_alice):
contract = direct_deploy("contracts/football_bets.py")
direct_vm.sender = direct_alice
# Create and resolve the bet first
contract.create_bet("2024-06-20", "Spain", "Italy", "1")
_setup_match_mocks(direct_vm, "1:0", 1)
contract.resolve_bet("2024-06-20_spain_italy")
# Second resolve must revert
with direct_vm.expect_revert("Bet already resolved"):
contract.resolve_bet("2024-06-20_spain_italy")
Setting Up Mocks for Isolated Testing
External dependencies are mocked with vm.mock_web() and vm.mock_llm(). The _setup_match_mocks helper in tests/direct/test_resolve_bet.py configures these:
def _setup_match_mocks(vm, score: str, is_finished: int):
vm.mock_web("https://www.bbc.com/...", '<html>match data</html>')
vm.mock_llm("extract score and status", f'{{"score": "{score}", "finished": {is_finished}}}')
This pattern removes network variability and lets you test edge cases deterministically.
Adding Diagnostic Output for Deep Debugging
While print() statements are suppressed in production deployments, they appear in direct test output. Add temporary logging before raise statements to expose internal state.
Example: Debugging Unexpected Reverts
Modify resolve_bet temporarily:
def resolve_bet(self, bet_id: str) -> None:
bet = self.bets[gl.message.sender_address][bet_id]
print(f"DEBUG: bet_id={bet_id}, has_resolved={bet.has_resolved}, match_time={bet.match_time}")
if bet.has_resolved:
raise Exception("Bet already resolved")
# ... remaining logic
Run the test with pytest tests/direct/test_resolve_bet.py -v -s to see debug output. Remove or comment these lines before committing.
Step-by-Step Debugging Workflow
Follow this systematic approach to resolve any GenLayer contract revert:
-
Capture the revert message from the transaction receipt or test failure
-
Locate the source with
grep -r "revert message" contracts/ -
Analyze the condition triggering the
raise—check state variables, parameters, and external data -
Create a minimal direct test that reproduces the exact state:
- Set
direct_vm.senderappropriately - Deploy with
direct_deploy("contracts/<file>.py") - Mock external calls with
vm.mock_web()andvm.mock_llm()
- Set
-
Assert the revert using
with direct_vm.expect_revert("<message>"): -
Add debug prints if the revert path is unclear, re-run, then remove them
-
Fix the logic and run
pytest tests/direct/ -vto verify no regressions
Key Files for Revert Debugging
| File | Purpose |
|---|---|
contracts/football_bets.py |
Core contract with raise statements at lines 73-75, 91-93, 97-99 |
tests/direct/test_resolve_bet.py |
Demonstrates expect_revert usage and mock setup |
tests/direct/test_create_bet.py |
Shows "Bet already created" revert testing |
Summary
- GenLayer reverts are Python exceptions caught by the GenVM, rolling back state and returning the message
- Locate reverts by searching contract files for the exact error string
- Test reverts in isolation using
direct_vm.expect_revert("<message>")with mocked dependencies - Debug state with temporary
print()statements visible only in direct test output - Verify fixes by running the full direct test suite to prevent regressions
Frequently Asked Questions
What makes a GenLayer contract revert?
A GenLayer contract reverts when any method raises a Python Exception. The GenVM intercepts this, reverts all state changes, and returns the exception message to the caller. This differs from Ethereum's require() or revert() opcodes—it's standard Python exception handling wrapped in transaction semantics.
How do I test that my contract reverts correctly?
Use with direct_vm.expect_revert("exact message"): in direct-mode tests. This context manager asserts that the enclosed code raises the specified revert. Combine it with vm.mock_web() and vm.mock_llm() to control external dependencies and test edge cases deterministically, as shown in tests/direct/test_resolve_bet.py.
Can I see print output from my contract when testing?
Yes—print() and logger output appears in direct test results but is suppressed in production. Add diagnostic prints before raise statements, run with pytest -s, then remove them before deploying. This is the recommended method for inspecting state that triggers unexpected reverts.
Why does my test fail even though the contract reverts?
expect_revert requires an exact message match. Check for typos, whitespace differences, or dynamic message construction. Also ensure you're using the context manager correctly—it must wrap the call that triggers the revert, not subsequent assertions.
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 →