From ce785fca85a42913b0ad6fadd77852431eab3539 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 9 Jul 2026 16:55:02 +0900 Subject: [PATCH 1/4] fix(gltest): resolve runners from local tree when GENVM_PREBUILT_DIR set GenVM v0.6 dropped the monolithic runner bundle the direct/glsim loader downloads from the (retired) genlayerlabs/genvm releases, so under v0.6 every load failed with `No GenVM runner bundle for `. When GENVM_PREBUILT_DIR points at a prebuilt GenVM tree, extract the runner straight from its content-addressed tar (globbed under any *runners* dir, so runners/ and executor//legacy-runners/ both resolve) and skip the download. Download path is untouched and stays the default when the env var is unset. --- gltest/direct/sdk_loader.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/gltest/direct/sdk_loader.py b/gltest/direct/sdk_loader.py index d1fa7ab..cef54d3 100644 --- a/gltest/direct/sdk_loader.py +++ b/gltest/direct/sdk_loader.py @@ -158,6 +158,28 @@ def download_artifacts(version: str) -> Path: ) from last_error +def _extract_local_runner( + root: Path, runner_type: str, runner_hash: Optional[str] +) -> Path: + """Extract a runner from a local prebuilt GenVM tree (GENVM_PREBUILT_DIR); globs + any *runners* dir so runners/ and executor//legacy-runners/ both match.""" + sub = ( + f"{runner_hash[:2]}/{runner_hash[2:]}.tar" + if runner_hash and runner_hash.lower() != "latest" + else "*/*.tar" + ) + hits = sorted(root.glob(f"**/*runners*/{runner_type}/{sub}")) + if not hits: + raise FileNotFoundError(f"runner {runner_type}:{runner_hash} not under {root}") + tar = hits[-1] + dest = CACHE_DIR / "extracted" / "local" / runner_type / (tar.parent.name + tar.stem) + if not dest.exists(): + dest.mkdir(parents=True, exist_ok=True) + with tarfile.open(tar, "r:") as inner: + inner.extractall(dest, filter="data") + return dest + + def extract_runner( tarball_path: Path, runner_type: str, @@ -165,6 +187,9 @@ def extract_runner( version: Optional[str] = None, ) -> Path: """Extract a runner from the tarball.""" + prebuilt = os.environ.get("GENVM_PREBUILT_DIR") + if prebuilt: + return _extract_local_runner(Path(prebuilt), runner_type, runner_hash) if version is None: match = re.search(r"genvm-universal-(.+)\.tar\.xz", tarball_path.name) version = match.group(1) if match else "unknown" @@ -256,10 +281,11 @@ def setup_sdk_paths( if contract_path and contract_path.exists(): contract_deps = parse_contract_header(contract_path) - if version is None: + prebuilt = os.environ.get("GENVM_PREBUILT_DIR") + if version is None and not prebuilt: version = resolve_version() - tarball = download_artifacts(version) + tarball = None if prebuilt else download_artifacts(version) runner_hash = contract_deps.get(RUNNER_TYPE) runner_dir = extract_runner(tarball, RUNNER_TYPE, runner_hash, version) From 243f2f9d9a7c0c7945f0c725a19bb85705995b10 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 9 Jul 2026 17:56:17 +0900 Subject: [PATCH 2/4] test(fixtures): migrate example/glsim contracts to genvm v0.3 SDK The gltest/glsim test fixtures still used old-idiom markers that break under GenVM v0.6: gl.DynArray/TreeMap -> gl.storage.*, gl.Contract -> gl.contract.Contract, and callable gl.u256(x) -> plain x. Migrate all fixtures under tests/ (examples/, glsim/, gltest/artifact/); Depends/runner-hash header lines are untouched. Package source (gltest/, glsim/) is not changed. --- tests/examples/contracts/football_prediction_market.py | 2 +- tests/examples/contracts/intelligent_oracle.py | 8 ++++---- tests/examples/contracts/intelligent_oracle_factory.py | 2 +- tests/examples/contracts/llm_erc20.py | 4 ++-- tests/examples/contracts/log_indexer.py | 4 ++-- tests/examples/contracts/multi_file_contract/__init__.py | 4 ++-- tests/examples/contracts/multi_read_erc20.py | 2 +- tests/examples/contracts/multi_tenant_storage.py | 6 +++--- tests/examples/contracts/user_storage.py | 2 +- tests/glsim/deterministic_factory_contract.py | 2 +- tests/gltest/artifact/contracts/current_style_contract.py | 2 +- 11 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/examples/contracts/football_prediction_market.py b/tests/examples/contracts/football_prediction_market.py index 2e33279..dfcb5f5 100644 --- a/tests/examples/contracts/football_prediction_market.py +++ b/tests/examples/contracts/football_prediction_market.py @@ -37,7 +37,7 @@ def __init__(self, game_date: str, team1: str, team2: str): ) self.team1 = team1 self.team2 = team2 - self.winner = gl.u256(0) + self.winner = 0 self.score = "" @gl.public.write diff --git a/tests/examples/contracts/intelligent_oracle.py b/tests/examples/contracts/intelligent_oracle.py index 8e9cd06..5bc0a13 100644 --- a/tests/examples/contracts/intelligent_oracle.py +++ b/tests/examples/contracts/intelligent_oracle.py @@ -19,10 +19,10 @@ class IntelligentOracle(gl.contract.Contract): prediction_market_id: str title: str description: str - potential_outcomes: gl.DynArray[str] - rules: gl.DynArray[str] - data_source_domains: gl.DynArray[str] - resolution_urls: gl.DynArray[str] + potential_outcomes: gl.storage.DynArray[str] + rules: gl.storage.DynArray[str] + data_source_domains: gl.storage.DynArray[str] + resolution_urls: gl.storage.DynArray[str] earliest_resolution_date: str # Store as ISO format string status: str # Store as string since Enum isn't supported analysis: str # Store analysis results diff --git a/tests/examples/contracts/intelligent_oracle_factory.py b/tests/examples/contracts/intelligent_oracle_factory.py index 0d147b8..c49e335 100644 --- a/tests/examples/contracts/intelligent_oracle_factory.py +++ b/tests/examples/contracts/intelligent_oracle_factory.py @@ -6,7 +6,7 @@ class Registry(gl.contract.Contract): # Declare persistent storage fields - contract_addresses: gl.DynArray[str] + contract_addresses: gl.storage.DynArray[str] intelligent_oracle_code: str def __init__(self, intelligent_oracle_code: str): diff --git a/tests/examples/contracts/llm_erc20.py b/tests/examples/contracts/llm_erc20.py index 85561e4..9b150ac 100644 --- a/tests/examples/contracts/llm_erc20.py +++ b/tests/examples/contracts/llm_erc20.py @@ -7,10 +7,10 @@ class LlmErc20(gl.contract.Contract): - balances: gl.TreeMap[gl.Address, gl.u256] + balances: gl.storage.TreeMap[gl.Address, gl.u256] def __init__(self, total_supply: int) -> None: - self.balances[gl.message.sender_address] = gl.u256(total_supply) + self.balances[gl.message.sender_address] = total_supply @gl.public.write def transfer(self, amount: int, to_address: str) -> None: diff --git a/tests/examples/contracts/log_indexer.py b/tests/examples/contracts/log_indexer.py index a3eed4f..5637eb7 100644 --- a/tests/examples/contracts/log_indexer.py +++ b/tests/examples/contracts/log_indexer.py @@ -53,14 +53,14 @@ def get_closest_vector(self, text: str) -> dict | None: @gl.public.write def add_log(self, log: str, log_id: int) -> None: emb = self.get_embedding(log) - self.vector_store.insert(emb, StoreValue(text=log, log_id=gl.u256(log_id))) + self.vector_store.insert(emb, StoreValue(text=log, log_id=log_id)) @gl.public.write def update_log(self, log_id: int, log: str) -> None: emb = self.get_embedding(log) for elem in self.vector_store.knn(emb, 2): if elem.value.text == log: - elem.value.log_id = gl.u256(log_id) + elem.value.log_id = log_id @gl.public.write def remove_log(self, id: int) -> None: diff --git a/tests/examples/contracts/multi_file_contract/__init__.py b/tests/examples/contracts/multi_file_contract/__init__.py index a2c75cd..8927ab5 100644 --- a/tests/examples/contracts/multi_file_contract/__init__.py +++ b/tests/examples/contracts/multi_file_contract/__init__.py @@ -10,8 +10,8 @@ def __init__(self): self.other_addr = gl.contract.deploy( code=text.encode("utf-8"), args=["123"], - salt_nonce=gl.u256(1), - value=gl.u256(0), + salt_nonce=1, + value=0, on="accepted", ) diff --git a/tests/examples/contracts/multi_read_erc20.py b/tests/examples/contracts/multi_read_erc20.py index 3bdf346..8ad807f 100644 --- a/tests/examples/contracts/multi_read_erc20.py +++ b/tests/examples/contracts/multi_read_erc20.py @@ -5,7 +5,7 @@ class multi_read_erc20(gl.contract.Contract): - balances: gl.TreeMap[gl.Address, gl.TreeMap[gl.Address, gl.u256]] + balances: gl.storage.TreeMap[gl.Address, gl.storage.TreeMap[gl.Address, gl.u256]] def __init__(self): pass diff --git a/tests/examples/contracts/multi_tenant_storage.py b/tests/examples/contracts/multi_tenant_storage.py index 6aa37e9..2bcd15d 100644 --- a/tests/examples/contracts/multi_tenant_storage.py +++ b/tests/examples/contracts/multi_tenant_storage.py @@ -12,9 +12,9 @@ class MultiTentantStorage(gl.contract.Contract): This is done to test contract calls between different contracts. """ - all_storage_contracts: gl.DynArray[gl.Address] - available_storage_contracts: gl.DynArray[gl.Address] - mappings: gl.TreeMap[ + all_storage_contracts: gl.storage.DynArray[gl.Address] + available_storage_contracts: gl.storage.DynArray[gl.Address] + mappings: gl.storage.TreeMap[ gl.Address, gl.Address ] # mapping of user address to storage contract address diff --git a/tests/examples/contracts/user_storage.py b/tests/examples/contracts/user_storage.py index a37a902..8f02bf9 100644 --- a/tests/examples/contracts/user_storage.py +++ b/tests/examples/contracts/user_storage.py @@ -5,7 +5,7 @@ class UserStorage(gl.contract.Contract): - storage: gl.TreeMap[gl.Address, str] + storage: gl.storage.TreeMap[gl.Address, str] # constructor def __init__(self): diff --git a/tests/glsim/deterministic_factory_contract.py b/tests/glsim/deterministic_factory_contract.py index 3b65b96..ef51fcf 100644 --- a/tests/glsim/deterministic_factory_contract.py +++ b/tests/glsim/deterministic_factory_contract.py @@ -24,7 +24,7 @@ def deploy_child(self, salt: int) -> str: code=CHILD_CODE.encode("utf-8"), args=[], kwargs={}, - salt_nonce=gl.u256(salt), + salt_nonce=salt, on="accepted", ) self.child_address = child_address.as_hex diff --git a/tests/gltest/artifact/contracts/current_style_contract.py b/tests/gltest/artifact/contracts/current_style_contract.py index caa7b20..ce6020a 100644 --- a/tests/gltest/artifact/contracts/current_style_contract.py +++ b/tests/gltest/artifact/contracts/current_style_contract.py @@ -3,7 +3,7 @@ import genlayer as gl -class CurrentStyleContract(gl.Contract): +class CurrentStyleContract(gl.contract.Contract): storage: str def __init__(self, initial_storage: str): From 38fcaab3bb00d72648d3bff5b783808c1207549c Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 9 Jul 2026 20:02:55 +0900 Subject: [PATCH 3/4] fix(gltest): point genvm download at the genvm-manager repo The genvm release bundle moved from the retired genlayerlabs/genvm repo to genlayerlabs/genvm-manager. Update both the download and release-discovery URLs so the sdk_loader download fallback resolves against the live repo. --- gltest/direct/sdk_loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gltest/direct/sdk_loader.py b/gltest/direct/sdk_loader.py index cef54d3..d07a7bb 100644 --- a/gltest/direct/sdk_loader.py +++ b/gltest/direct/sdk_loader.py @@ -17,8 +17,8 @@ from typing import Optional, Dict, List CACHE_DIR = Path.home() / ".cache" / "gltest-direct" -GITHUB_RELEASES_URL = "https://github.com/genlayerlabs/genvm/releases" -GITHUB_API_RELEASES = "https://api.github.com/repos/genlayerlabs/genvm/releases" +GITHUB_RELEASES_URL = "https://github.com/genlayerlabs/genvm-manager/releases" +GITHUB_API_RELEASES = "https://api.github.com/repos/genlayerlabs/genvm-manager/releases" # GenVM 0.3.0 renamed this bundle from genvm-universal.tar.xz; newest name first. RUNNER_BUNDLE_ASSETS = ("genvm-runners-all.tar.xz", "genvm-universal.tar.xz") From 63d338cd5812722851974d15536b3b64bf883461 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 9 Jul 2026 20:23:57 +0900 Subject: [PATCH 4/4] fix(gltest): resolve runners from the genvm-manager whole-tree release genvm-manager v0.6 ships a per-platform whole-tree tarball (genvm--.tar.xz) instead of the retired runner-only bundle, so the download fallback now selects the host asset and unpacks the whole tree once (atomically, cached) into a GENVM_PREBUILT_DIR-shaped dir, then resolves runners through the same glob path as the prebuilt tree. This collapses the download and prebuilt paths onto one extraction routine and handles both the current runners/ and the legacy-runners/ overlay. --- gltest/direct/sdk_loader.py | 106 ++++++++++++------------- tests/gltest_direct/test_sdk_loader.py | 5 +- 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/gltest/direct/sdk_loader.py b/gltest/direct/sdk_loader.py index d07a7bb..7335a98 100644 --- a/gltest/direct/sdk_loader.py +++ b/gltest/direct/sdk_loader.py @@ -9,7 +9,9 @@ import re import sys import json +import shutil import tarfile +import platform import tempfile import urllib.error import urllib.request @@ -20,8 +22,23 @@ GITHUB_RELEASES_URL = "https://github.com/genlayerlabs/genvm-manager/releases" GITHUB_API_RELEASES = "https://api.github.com/repos/genlayerlabs/genvm-manager/releases" -# GenVM 0.3.0 renamed this bundle from genvm-universal.tar.xz; newest name first. -RUNNER_BUNDLE_ASSETS = ("genvm-runners-all.tar.xz", "genvm-universal.tar.xz") + +def _host_release_asset() -> str: + """genvm-manager (v0.6+) ships a whole-tree tarball per platform, named + genvm--.tar.xz — pick the one matching this host.""" + machine = platform.machine().lower() + arch = "arm64" if machine in ("aarch64", "arm64") else "amd64" + os_name = "macos" if platform.system().lower() == "darwin" else "linux" + return f"genvm-{arch}-{os_name}.tar.xz" + + +# Download candidates, newest-scheme first: the genvm-manager per-platform whole +# tree, then the pre-v0.6 runner-only bundles (genvm-runners-all → genvm-universal). +RUNNER_BUNDLE_ASSETS = ( + _host_release_asset(), + "genvm-runners-all.tar.xz", + "genvm-universal.tar.xz", +) GENVM_VERSION_ENV = "GENVM_VERSION" FALLBACK_VERSION = "v0.2.16" @@ -180,71 +197,50 @@ def _extract_local_runner( return dest +def _extract_release_tree(tarball_path: Path, version: str) -> Path: + """Unpack a downloaded GenVM release tarball once (cached) into a local tree. + + genvm-manager ships the whole tree (bin/ lib/ runners/ executor/…), not a + runner-only bundle, so we unpack it to a directory that looks exactly like a + GENVM_PREBUILT_DIR and then resolve runners through the same globbing path. + """ + tree = CACHE_DIR / "trees" / version + if (tree / ".extracted").exists(): + return tree + trees = CACHE_DIR / "trees" + trees.mkdir(parents=True, exist_ok=True) + # Extract into a process-unique dir, mark it complete, then publish with an + # atomic rename. Concurrent cold-cache extractions each use their own tmp and + # race only on the final rename; the loser sees the winner's finished tree. + tmp = Path(tempfile.mkdtemp(dir=trees, prefix=f".{version}.")) + try: + with tarfile.open(tarball_path, "r:xz") as outer: + outer.extractall(tmp, filter="data") + (tmp / ".extracted").touch() + os.replace(tmp, tree) + except OSError: + shutil.rmtree(tmp, ignore_errors=True) + if (tree / ".extracted").exists(): + return tree # another extraction published first + raise + return tree + + def extract_runner( tarball_path: Path, runner_type: str, runner_hash: Optional[str] = None, version: Optional[str] = None, ) -> Path: - """Extract a runner from the tarball.""" + """Resolve a runner dir from a local prebuilt tree or a downloaded release.""" prebuilt = os.environ.get("GENVM_PREBUILT_DIR") if prebuilt: return _extract_local_runner(Path(prebuilt), runner_type, runner_hash) if version is None: match = re.search(r"genvm-universal-(.+)\.tar\.xz", tarball_path.name) version = match.group(1) if match else "unknown" - - extract_base = CACHE_DIR / "extracted" / version / runner_type - - # Fast path: if hash specified and already extracted, skip tarball entirely - if runner_hash and runner_hash.lower() != "latest": - extract_dir = extract_base / runner_hash - if extract_dir.exists(): - return extract_dir - - # Check if any version already extracted (for "latest" case) - if extract_base.exists(): - existing = sorted(extract_base.iterdir(), reverse=True) - if existing and (not runner_hash or runner_hash.lower() == "latest"): - return existing[0] - - # Need to open tarball - this is slow (~13s for xz) - with tarfile.open(tarball_path, "r:xz") as outer_tar: - prefix = f"runners/{runner_type}/" - runner_tars = [ - m.name for m in outer_tar.getmembers() - if m.name.startswith(prefix) and m.name.endswith(".tar") - ] - - if not runner_tars: - raise ValueError(f"No {runner_type} runners found in tarball") - - # Treat "latest" as no specific hash - if runner_hash and runner_hash.lower() != "latest": - target = f"runners/{runner_type}/{runner_hash[:2]}/{runner_hash[2:]}.tar" - if target not in runner_tars: - raise ValueError(f"Runner hash {runner_hash} not found") - runner_tar_name = target - extract_dir = extract_base / runner_hash - else: - runner_tar_name = sorted(runner_tars)[-1] - parts = runner_tar_name.split("/") - runner_hash = parts[-2] + parts[-1].replace(".tar", "") - extract_dir = extract_base / runner_hash - - if extract_dir.exists(): - return extract_dir - - inner_tar_file = outer_tar.extractfile(runner_tar_name) - if inner_tar_file is None: - raise ValueError(f"Failed to read {runner_tar_name}") - - extract_dir.mkdir(parents=True, exist_ok=True) - - with tarfile.open(fileobj=inner_tar_file, mode="r:") as inner_tar: - inner_tar.extractall(extract_dir, filter='data') - - return extract_dir + tree = _extract_release_tree(tarball_path, version) + return _extract_local_runner(tree, runner_type, runner_hash) def parse_runner_manifest(runner_dir: Path) -> Dict[str, str]: diff --git a/tests/gltest_direct/test_sdk_loader.py b/tests/gltest_direct/test_sdk_loader.py index 7a7cbd6..dd28db5 100644 --- a/tests/gltest_direct/test_sdk_loader.py +++ b/tests/gltest_direct/test_sdk_loader.py @@ -162,7 +162,7 @@ def _download(url, dest): assert result == tmp_path / "genvm-universal-v0.3.0.tar.xz" assert result.read_bytes() == b"bundle" assert tried == [ - f"{sdk_loader.GITHUB_RELEASES_URL}/download/v0.3.0/genvm-runners-all.tar.xz" + f"{sdk_loader.GITHUB_RELEASES_URL}/download/v0.3.0/{sdk_loader.RUNNER_BUNDLE_ASSETS[0]}" ] def test_falls_back_to_old_asset_on_404(self, monkeypatch, tmp_path): @@ -171,7 +171,8 @@ def test_falls_back_to_old_asset_on_404(self, monkeypatch, tmp_path): def _download(url, dest): tried.append(url) - if url.endswith("genvm-runners-all.tar.xz"): + # 404 every asset except the last so the loop walks the full list. + if not url.endswith(sdk_loader.RUNNER_BUNDLE_ASSETS[-1]): raise self._http_404(url) dest.write_bytes(b"bundle")