Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/linear-issue-fixer/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ Create a clear implementation plan:
Co-Authored-By: Claude <noreply@anthropic.com>"
```


### Step 7: Run ALL Test Suites

**IMPORTANT: You MUST run ALL of the following test suites before creating a PR. Do NOT skip any.**
Expand Down
7 changes: 5 additions & 2 deletions backend/node/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,9 @@ async def _run_genvm(
perms += "ws" # write/send

start_time = time.time()
# Only enforce version restrictions for new deployments (is_init=True)
# For running existing contracts, allow debug mode for flexibility
extra_args = [] if is_init else ["--debug-mode"]
try:
result = await genvmbase.run_genvm_host(
functools.partial(
Expand All @@ -1006,8 +1009,8 @@ async def _run_genvm(
permissions=perms,
capture_output=True,
host_data=json.dumps(host_data),
extra_args=["--debug-mode"],
is_sync=is_sync,
extra_args=extra_args,
is_sync=False,
manager_uri=self.manager.url,
timeout=timeout,
code=code,
Expand Down
15 changes: 13 additions & 2 deletions backend/node/genvm/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
parse_module_error_string,
parse_ctx_from_module_error_string,
GenVMInternalError,
get_user_friendly_message,
)


Expand Down Expand Up @@ -284,8 +285,14 @@ def provide_result(
# Preserve raw error structure (causes, fatal, ctx) excluding message
raw_error = {k: v for k, v in result_decoded.items() if k != "message"}

# Get user-friendly message if available
original_message = result_decoded["message"]
friendly_message = get_user_friendly_message(
error_code, original_message, raw_error
)

result = ExecutionError(
result_decoded["message"],
friendly_message,
res.result_kind,
error_code=error_code,
raw_error=raw_error if raw_error else None,
Expand All @@ -294,8 +301,12 @@ def provide_result(
else:
# String error - try to extract error code from message
error_code = extract_error_code(str(result_decoded), res.stderr)
original_message = str(result_decoded)
friendly_message = get_user_friendly_message(
error_code, original_message
)
result = ExecutionError(
str(result_decoded),
friendly_message,
res.result_kind,
error_code=error_code,
)
Expand Down
57 changes: 57 additions & 0 deletions backend/node/genvm/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"RATE_LIMIT_STATUSES",
"extract_error_code",
"parse_module_error_string",
"get_user_friendly_message",
"parse_ctx_from_module_error_string",
)

Expand Down Expand Up @@ -80,12 +81,61 @@ class GenVMErrorCode(StrEnum):
WEB_REQUEST_FAILED = "WEB_REQUEST_FAILED"
WEB_TLD_FORBIDDEN = "WEB_TLD_FORBIDDEN"

# Contract/runner errors
INVALID_RUNNER = "INVALID_RUNNER"

# Execution errors
GENVM_TIMEOUT = "GENVM_TIMEOUT"
CONTRACT_ERROR = "CONTRACT_ERROR"
INTERNAL_ERROR = "INTERNAL_ERROR"


# User-friendly error messages for each error code
ERROR_CODE_MESSAGES: dict[str, str] = {
GenVMErrorCode.INVALID_RUNNER: (
"Invalid runner version. The 'latest' and 'test' versions are not allowed. "
"Please use a fixed version hash in your contract header, e.g.: "
'# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }'
),
GenVMErrorCode.LLM_RATE_LIMITED: "LLM provider rate limit exceeded. Please try again later.",
GenVMErrorCode.LLM_NO_PROVIDER: "No LLM provider available for the requested operation.",
GenVMErrorCode.LLM_PROVIDER_ERROR: "LLM provider returned an error.",
GenVMErrorCode.LLM_INVALID_API_KEY: "Invalid API key for the LLM provider.",
GenVMErrorCode.LLM_TIMEOUT: "LLM request timed out.",
GenVMErrorCode.WEB_REQUEST_FAILED: "Web request failed.",
GenVMErrorCode.WEB_TLD_FORBIDDEN: "Access to the requested domain is forbidden.",
GenVMErrorCode.GENVM_TIMEOUT: "Contract execution timed out.",
GenVMErrorCode.CONTRACT_ERROR: "Contract execution error.",
GenVMErrorCode.INTERNAL_ERROR: "Internal GenVM error.",
}


def get_user_friendly_message(
error_code: str | None, original_message: str, raw_error: dict | None = None
) -> str:
"""
Get a user-friendly error message based on the error code.

Args:
error_code: The standardized error code (e.g., INVALID_RUNNER)
original_message: The original error message from GenVM
raw_error: Optional raw error structure with causes

Returns:
A user-friendly error message
"""
if error_code and error_code in ERROR_CODE_MESSAGES:
return ERROR_CODE_MESSAGES[error_code]

# Fallback: check raw_error causes for invalid runner pattern
if raw_error and isinstance(raw_error.get("causes"), list):
for cause in raw_error["causes"]:
if isinstance(cause, str) and "invalid runner id" in cause.lower():
return ERROR_CODE_MESSAGES[GenVMErrorCode.INVALID_RUNNER]

return original_message


# Mapping from Lua error causes to standardized error codes
LUA_CAUSE_TO_CODE: dict[str, GenVMErrorCode] = {
"NO_PROVIDER_FOR_PROMPT": GenVMErrorCode.LLM_NO_PROVIDER,
Expand Down Expand Up @@ -311,6 +361,9 @@ def extract_error_code(
# Map Lua causes to error codes
if isinstance(causes, list):
for cause in causes:
# Check for invalid runner error (e.g., "invalid runner id: py-genlayer:latest")
if isinstance(cause, str) and "invalid runner id" in cause.lower():
return GenVMErrorCode.INVALID_RUNNER
if cause in LUA_CAUSE_TO_CODE:
# Special case: STATUS_NOT_OK might be rate limiting based on ctx
if cause == "STATUS_NOT_OK":
Expand All @@ -327,6 +380,10 @@ def _extract_from_message(message: str, stderr: str = "") -> Optional[str]:
"""Extract error code from message string or stderr."""
combined = f"{message} {stderr}".lower()

# Check for invalid runner (e.g., using :latest or :test in non-debug mode)
if "invalid runner" in combined:
return GenVMErrorCode.INVALID_RUNNER

# Check for specific error patterns
if "rate limit" in combined or "429" in combined:
return GenVMErrorCode.LLM_RATE_LIMITED
Expand Down
84 changes: 80 additions & 4 deletions backend/protocol_rpc/contract_linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,40 @@
Provides validation and linting for GenLayer smart contracts.
"""

from typing import Dict, Any
from backend.protocol_rpc.exceptions import JSONRPCError
import re
from typing import Dict, List, Any, Optional, Tuple
from flask_jsonrpc.exceptions import JSONRPCError


# Pattern to match the Depends header with "latest" or "test" versions
# Matches: # { "Depends": "py-genlayer:latest" } or # { "Depends": "py-genlayer:test" }
INVALID_VERSION_PATTERN = re.compile(
r'#\s*\{\s*"Depends"\s*:\s*"py-genlayer:(latest|test)"\s*\}'
)

# User-friendly error message for invalid runner versions
# Note: Curly braces in the example are escaped for .format() compatibility
INVALID_VERSION_ERROR_MESSAGE = (
'Invalid runner version "{version}". The "latest" and "test" versions are not allowed. '
"Please use a fixed version hash in your contract header, e.g.: "
'# {{ "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }}'
)


def check_invalid_runner_version(source_code: str) -> Tuple[bool, Optional[str]]:
"""
Check if source code contains an invalid runner version (latest or test).

Args:
source_code: Python source code to check

Returns:
Tuple of (has_invalid_version, version_name or None)
"""
match = INVALID_VERSION_PATTERN.search(source_code)
if match:
return True, match.group(1)
return False, None


class ContractLinter:
Expand All @@ -13,6 +45,40 @@ class ContractLinter:
def __init__(self):
"""Initialize the ContractLinter."""

def _check_invalid_runner_version(
self, source_code: str, filename: str
) -> List[Dict[str, Any]]:
"""
Check for invalid runner versions (latest, test) in the contract header.

Args:
source_code: Python source code to check
filename: Filename for error reporting

Returns:
List of linting issues found
"""
issues = []
lines = source_code.split("\n")

for line_num, line in enumerate(lines, start=1):
match = INVALID_VERSION_PATTERN.search(line)
if match:
version = match.group(1)
issues.append(
{
"rule_id": "INVALID_RUNNER_VERSION",
"message": f'The runner version "{version}" is not allowed. Use a fixed version hash instead.',
"severity": "error",
"line": line_num,
"column": match.start() + 1,
"filename": filename,
"suggestion": 'Use a fixed version hash, e.g.: # { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }',
}
)

return issues

def lint_contract(
self, source_code: str, filename: str = "contract.py"
) -> Dict[str, Any]:
Expand Down Expand Up @@ -40,7 +106,7 @@ def lint_contract(

linter = GenVMLinter()
results = linter.lint_source(source_code, filename)
print(f"[LINTER] Found {len(results)} issues")
print(f"[LINTER] Found {len(results)} issues from genvm_linter")

# Convert results to JSON-serializable format
results_json = []
Expand All @@ -61,9 +127,19 @@ def lint_contract(
}
)

# Add custom linting rules
custom_issues = self._check_invalid_runner_version(source_code, filename)
for issue in custom_issues:
severity_counts[issue["severity"]] += 1
results_json.append(issue)

print(
f"[LINTER] Total issues: {len(results_json)} ({len(custom_issues)} from custom rules)"
)

return {
"results": results_json,
"summary": {"total": len(results), "by_severity": severity_counts},
"summary": {"total": len(results_json), "by_severity": severity_counts},
}
except ImportError as e:
print(f"[LINTER] Import error: {e}")
Expand Down
14 changes: 14 additions & 0 deletions backend/protocol_rpc/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,20 @@ def admin_upgrade_contract_code(
data={},
)

# Validate runner version is not 'latest' or 'test'
from backend.protocol_rpc.contract_linter import (
check_invalid_runner_version,
INVALID_VERSION_ERROR_MESSAGE,
)

has_invalid_version, version = check_invalid_runner_version(new_code)
if has_invalid_version:
raise JSONRPCError(
code=-32602,
message=INVALID_VERSION_ERROR_MESSAGE.format(version=version),
data={"invalid_version": version},
)

# Validate contract exists and is deployed
contract = session.query(CurrentState).filter_by(id=contract_address).one_or_none()
if not contract or not contract.data or not contract.data.get("state"):
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/icontracts/contracts/company_naming.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

import json
from genlayer import *
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# v0.1.0
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# v0.1.0
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *
import json
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# v0.1.0
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# v0.1.0
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

import json
from enum import Enum
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# v0.1.0
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# v0.1.0
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *


Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"Depends": "py-genlayer-multi:test"
"Depends": "py-genlayer-multi:06zyvrlivjga0d5jlpdbprksc0pa6jmllxvp8s20hq1l512vh5yk"
}
2 changes: 1 addition & 1 deletion tests/integration/icontracts/contracts/multi_read_erc20.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# v0.1.0
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# v0.1.0
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *

Expand Down
2 changes: 1 addition & 1 deletion tests/integration/icontracts/contracts/read_erc20.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# v0.1.0
# { "Depends": "py-genlayer:test" }
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }

from genlayer import *

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Storage contracts for different py-genlayer versions
# Used for retrocompatibility testing
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# v0.1.0
# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }

from genlayer import *


class Storage(gl.Contract):
storage: str

def __init__(self, initial_storage: str):
self.storage = initial_storage

@gl.public.view
def get_storage(self) -> str:
return self.storage

@gl.public.write
def update_storage(self, new_storage: str) -> None:
self.storage = new_storage
Loading
Loading