Module M.04
Wrong Equivalence
The Precision Trap
GenLayer uses equivalence functions to determine if validator outputs agree. strict_eq requires exact match. For live prices, feeds, or any real-time data, validators will fetch at slightly different times and get slightly different values. strict_eq will always fail. Use custom equivalence with tolerance bands.
Side-by-side · Vulnerable vs. Patched
two contracts · proven by paired transactions
vulnerable ▸contracts/vulnerable/VulnerablePrice.py
Failed TX> consensus failed · validators diverged
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *4import json56# Module 4 (Vulnerable) -- Wrong equivalence principle.7# Live ETH spot price changes between the moment each validator fetches8# CoinGecko. Wrapping the fetch in strict_eq REQUIRES byte-for-byte9# agreement. Validators land on slightly different float values --> the10# round fails to reach consensus and the tx ends FINISHED_WITH_ERROR.1112PRICE_URL = "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd"131415class VulnerablePrice(gl.Contract):16 last_price: str1718 def __init__(self):19 self.last_price = ""2021 @gl.public.write22 def fetch_eth(self) -> None:23 def _fetch() -> str:24 resp = gl.nondet.web.request(PRICE_URL, method='GET')25 data = json.loads(resp.body.decode("utf-8"))26 return str(data["ethereum"]["usd"])2728 # BUG: strict_eq on a continuously-changing float.29 self.last_price = gl.eq_principle.strict_eq(_fetch)3031 @gl.public.view32 def get_last_price(self) -> str:33 return self.last_price
patched ▸contracts/patched/TolerantPrice.py
Success TX> consensus reached · all validators agree
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *4import json56# Module 4 (Patched) -- Tolerant equivalence.7# Same upstream feed, but the consensus principle is comparative: each8# validator returns a price; agreement is judged by an LLM-mediated check9# that the values are within a sane spread. This is the correct shape of10# eq_principle for live, slowly-drifting data.1112PRICE_URL = "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd"13TOLERANCE_PERCENT = 2.0141516class TolerantPrice(gl.Contract):17 last_price: str18 tolerance_percent: str # stored as string for cross-validator stability1920 owner: str2122 def __init__(self):23 self.owner = str(gl.message.sender_address)24 self.last_price = ""25 self.tolerance_percent = str(TOLERANCE_PERCENT)2627 @gl.public.write28 def fetch_eth(self) -> None:29 tol = self.tolerance_percent3031 def _fetch() -> str:32 resp = gl.nondet.web.request(PRICE_URL, method='GET')33 data = json.loads(resp.body.decode("utf-8"))34 return str(data["ethereum"]["usd"])3536 principle = (37 f"All values must be numeric ETH/USD prices and be within {tol}% "38 "of each other. Pick any one of them."39 )40 self.last_price = gl.eq_principle.prompt_comparative(_fetch, principle=principle)4142 @gl.public.write43 def set_tolerance(self, percent_str: str) -> None:44 """Owner can tune the equivalence tolerance on-chain -- deterministic,45 no LLM. Demonstrates that the comparative principle is parameterised,46 which is the structural fix relative to hardcoded strict_eq."""47 if str(gl.message.sender_address) != self.owner:48 raise Exception("only owner")49 self.tolerance_percent = percent_str5051 @gl.public.view52 def get_tolerance(self) -> str:53 return self.tolerance_percent5455 @gl.public.view56 def get_last_price(self) -> str:57 return self.last_price
Call invoked
fetch_eth()strict_eq on live ETH/USD -> validators fetch slightly different prices -> divergence
Call invoked
set_tolerance("2.0")owner tunes the comparative tolerance on-chain -- proves the equivalence is parameterised
On-chain receipts
Knowledge check · M.04
01 / 02
Two questions on this incident. Pick the best answer; the question locks once committed.
Question 01 / 02
Why does strict_eq fail for live price data?