How to Handle Async Contract Operations in GenLayer: A Complete Guide
GenLayer contracts cannot use Python's native async/await primitives; instead, use a two-step state-machine pattern with gl.nondet.* functions and equivalence-principle validators to handle asynchronous-like workflows.
The GenLayer platform enables intelligent smart contracts that interact with external data sources, but its deterministic execution model requires a different approach to handling operations that would traditionally be asynchronous. In genlayerlabs/genlayer-project-boilerplate, the reference implementation demonstrates how to structure contracts that fetch remote data or invoke LLM prompts across multiple transactions.
Why GenLayer Prohibits Native Async/Await
GenLayer smart contracts execute inside the GenVM, a deterministic virtual machine that guarantees consensus across all validators. This design has a critical constraint: contracts must run to completion in a single transaction without yielding control or suspending execution.
Native Python async/await introduces nondeterminism through coroutine scheduling, which would break validator consensus. The commented line in contracts/football_bets.py illustrates this limitation:
# Line 65 – This would be ideal but is NOT allowed in GenLayer:
# match_status = await self._check_match(bet.resolution_url, bet.team1, bet.team2)
Instead, all nondeterministic operations are performed synchronously through the GenLayer SDK, with results validated via equivalence principles.
The Two-Step Pattern for Async-Like Operations
To model workflows that span multiple transactions, split your logic into distinct phases:
- Initiation – Store request parameters in contract state; emit events but do not call
gl.nondet.*functions. - Resolution – In a separate transaction, execute the nondeterministic call and apply the validated result.
This pattern is fully implemented in the FootballBets contract at contracts/football_bets.py.
Step 1: Initiate Without Nondeterministic Calls
The create_bet method (lines 70–87) demonstrates proper initiation:
@gl.public.write
def create_bet(
self,
game_date: str,
team1: str,
team2: str,
predicted_winner: str
) -> str:
# Generate deterministic data for later resolution
match_resolution_url = (
f"https://www.bbc.com/sport/football/scores-fixtures/{game_date}"
)
bet_id = f"{gl.message.sender_address}_{team1}_{team2}_{game_date}"
# Store state WITHOUT making any nondet calls
self.bets[gl.message.sender_address][bet_id] = Bet(
game_date=game_date,
team1=team1,
team2=team2,
predicted_winner=predicted_winner,
resolution_url=match_resolution_url,
has_resolved=False
)
return bet_id
Key characteristics of the initiation phase:
- All operations are deterministic and pure
- External URLs or prompts are stored, not executed
- Contract state tracks the pending request
Step 2: Resolve With Synchronous Nondeterministic Calls
The resolve_bet method (lines 90–103) performs the actual fetch:
@gl.public.write
def resolve_bet(self, bet_id: str):
# Guard clauses ensure proper state
if bet_id not in self.bets[gl.message.sender_address]:
raise Exception("Invalid bet ID")
bet = self.bets[gl.message.sender_address][bet_id]
if bet.has_resolved:
raise Exception("Bet already resolved")
# SYNCHRONOUS nondeterministic call (no await)
match_status = self._check_match(
bet.resolution_url,
bet.team1,
bet.team2
)
# Apply result deterministically
bet.has_resolved = True
# ... determine winner, update balances ...
The _check_match helper (lines 30–55) encapsulates the LLM prompt and validation:
def _check_match(self, url: str, team1: str, team2: str) -> dict:
# Fetch remote HTML synchronously
page_contents = gl.nondet.web.render(url, mode="text")
# Build prompt for LLM extraction
prompt = f"""
Based on the following content from {url},
what was the result of the match between {team1} and {team2}?
Return a JSON with keys: home_score, away_score.
Content: {page_contents}
"""
# Execute LLM prompt synchronously
raw_response = gl.nondet.exec_prompt(prompt)
# Equivalence-principle validator ensures consensus
def parse_response() -> dict:
return json.loads(raw_response)
# All validators must agree on this exact output
validated_result = gl.eq_principle.strict_eq(parse_response)
return validated_result
Core GenLayer SDK Components
| Component | Purpose | Example Usage |
|---|---|---|
gl.nondet.web.render |
Fetch remote web content | gl.nondet.web.render(url, mode="text" |
gl.nondet.exec_prompt |
Execute LLM prompts | gl.nondet.exec_prompt(prompt_string) |
gl.eq_principle.strict_eq |
Exact-match validator for simple outputs | gl.eq_principle.strict_eq(lambda: result) |
gl.eq_principle.prompt_comparative |
LLM-based semantic comparison validator | For fuzzy or structured data matching |
gl.public.write |
Decorator for state-modifying functions | Marks transaction entry points |
Complete Example: Generic Async Pattern
Apply this template to your own contracts:
import json
class AsyncOperationContract:
def __init__(self):
# Track pending operations by caller
self.pending_requests: dict[str, str] = {}
self.results: dict[str, str] = {}
@gl.public.write
def start_operation(self, resource_url: str) -> str:
"""
Phase 1: Store the request parameters.
No nondeterministic calls allowed here.
"""
request_id = f"{gl.message.sender_address}_{gl.block.number}"
self.pending_requests[gl.message.sender_address] = resource_url
return request_id
@gl.public.write
def complete_operation(self) -> str:
"""
Phase 2: Execute fetch and validate result.
This runs in a separate transaction initiated later.
"""
url = self.pending_requests.get(gl.message.sender_address)
if not url:
raise Exception("No pending request for caller")
# Nondeterministic fetch (synchronous)
raw_content = gl.nondet.web.render(url, mode="text")
# Define validator function
def extract_data() -> str:
# Deterministic processing of fetched content
return raw_content[:1000] # Truncate for example
# Consensus validation
validated = gl.eq_principle.strict_eq(extract_data)
# Cleanup and store
del self.pending_requests[gl.message.sender_address]
self.results[gl.message.sender_address] = validated
return validated
Error Handling Best Practices
The resolve_bet implementation demonstrates defensive patterns:
- State validation before nondet calls: Check
has_resolved, existence of records, and authorization before consuming validator resources. - Deterministic exceptions: Use standard
raise Exception(...)for control flow—never catch nondeterministic errors. - Idempotent resolution design: Ensure multiple valid calls produce consistent final state.
From contracts/football_bets.py lines 91–99:
if bet_id not in self.bets[gl.message.sender_address]:
raise Exception("Invalid bet ID")
bet = self.bets[gl.message.sender_address][bet_id]
if bet.has_resolved:
raise Exception("Bet already resolved") # Prevent double-resolution
Testing Async Contract Patterns
The repository includes tests/direct/test_resolve_bet.py for validating the two-step flow:
# Typical test sequence
def test_full_bet_lifecycle(contract_client):
# Phase 1: Create (deterministic)
bet_id = contract_client.create_bet(
game_date="2024-01-15",
team1="Arsenal",
team2="Chelsea",
predicted_winner="Arsenal"
)
# Phase 2: Resolve (nondeterministic, validated)
result = contract_client.resolve_bet(bet_id)
assert result["has_resolved"] is True
Run tests with the commands documented in CLAUDE.md:
# Lint contract code
python -m py_contracts lint contracts/
# Run direct tests
pytest tests/direct/test_resolve_bet.py -v
Summary
- No
async/awaitin GenLayer: The GenVM requires single-transaction determinism. - Split into initiate + resolve: Store parameters first, execute
gl.nondet.*calls later. - Always validate with equivalence principles: Use
gl.eq_principle.strict_eqorgl.eq_principle.prompt_comparativeto ensure validator consensus. - Reference implementation: Study
contracts/football_bets.pyfor production patterns.
Frequently Asked Questions
Can I use Python's asyncio library in GenLayer contracts?
No. The GenVM executes contracts deterministically across all validators, and asyncio's event loop introduces scheduling nondeterminism. All I/O operations must use synchronous gl.nondet.* primitives wrapped in equivalence-principle validators, as implemented in contracts/football_bets.py.
How do I handle long-running external API calls?
Store the API parameters in contract state during an initial transaction. Trigger the actual call in a subsequent transaction when needed. The external latency is absorbed between transactions, not within one. The FootballBets contract defers match result fetching from create_bet to resolve_bet for this reason.
What happens if validators disagree on a nondeterministic result?
The equivalence-principle validator rejects the transaction. Contracts must handle this by either retrying with adjusted parameters or leaving the request in pending state for future resolution. gl.eq_principle.strict_eq requires byte-identical outputs; use gl.eq_principle.prompt_comparative for semantically equivalent but textually different responses.
Can multiple pending requests exist per user?
Yes. The storage pattern maps user addresses to nested structures (e.g., self.bets[address][bet_id]). Design your initiation function to generate unique IDs—typically combining sender address, block number, and content hash—to enable concurrent pending operations.
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 →