Every Intelligent Contract starts with a runner comment:
# { "Depends": "py-genlayer:<hash>" }
What is not documented anywhere we could find is that GenVM parses the entire
contiguous run of leading # lines as that one JSON document. So this is a
valid contract:
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }
# Anything you want to say about the contract.
from genlayer import *
and this one is rejected by the network, purely because the blank line is gone:
# { "Depends": "py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6" }
# Anything you want to say about the contract.
from genlayer import *
A single # hello on line 2 is enough. Reproducible without spending anything,
against Bradbury (2026-09-04) — the first form returns a schema, the second
returns VMError: invalid_contract:
$ curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"gen_getContractSchema\",\"params\":[{\"code\":\"$(base64 -w0 < contract.py)\"}]}" \
https://rpc-bradbury.genlayer.com
{"jsonrpc":"2.0","result":null,"error":{"code":-32000,"message":"VMError: invalid_contract"},"id":1}
The execution trace names the cause, and the message is the confusing part:
error: { causes: [ 'trailing characters at line 1 column 84' ] }
Line 1 is 83 characters long and entirely correct. Column 84 is the newline —
the parser is reporting where line 2 got appended to it. On the attest call
against the resulting address the trace instead says
VMError(invalid_contract absent_runner_comment), which points at a runner
comment that is present and well-formed.
What makes this expensive is that nothing local catches it:
genvm-lint check passes the broken file.
- The direct-mode test suite passes the broken file.
genlayer deploy prints ✔ Contract deployed successfully. and returns a
contract address.
The first real symptom is genlayer code <address> returning contract code not found at address …, or every call failing, against an address the CLI just
told you was deployed. We burned a deployment and an attest transaction
before finding it.
Suggested fixes, in order of how much they would have helped us:
- Document the rule on the Intelligent Contracts introduction, next to the
runner comment: the runner comment must be the only leading comment, or must
be separated from any other comment by a blank line.
- Have
genvm-lint check it. It already parses the runner comment to
resolve the version; rejecting a leading block that is not valid JSON would
move this failure from a wasted deployment to a local lint error.
- Improve the error.
absent_runner_comment for a file whose first line
is a runner comment sends readers to exactly the wrong place; naming the
comment block would be enough.
Every Intelligent Contract starts with a runner comment:
# { "Depends": "py-genlayer:<hash>" }What is not documented anywhere we could find is that GenVM parses the entire
contiguous run of leading
#lines as that one JSON document. So this is avalid contract:
and this one is rejected by the network, purely because the blank line is gone:
A single
# helloon line 2 is enough. Reproducible without spending anything,against Bradbury (2026-09-04) — the first form returns a schema, the second
returns
VMError: invalid_contract:The execution trace names the cause, and the message is the confusing part:
Line 1 is 83 characters long and entirely correct. Column 84 is the newline —
the parser is reporting where line 2 got appended to it. On the
attestcallagainst the resulting address the trace instead says
VMError(invalid_contract absent_runner_comment), which points at a runnercomment that is present and well-formed.
What makes this expensive is that nothing local catches it:
genvm-lint checkpasses the broken file.genlayer deployprints✔ Contract deployed successfully.and returns acontract address.
The first real symptom is
genlayer code <address>returningcontract code not found at address …, or every call failing, against an address the CLI justtold you was deployed. We burned a deployment and an
attesttransactionbefore finding it.
Suggested fixes, in order of how much they would have helped us:
runner comment: the runner comment must be the only leading comment, or must
be separated from any other comment by a blank line.
genvm-lintcheck it. It already parses the runner comment toresolve the version; rejecting a leading block that is not valid JSON would
move this failure from a wasted deployment to a local lint error.
absent_runner_commentfor a file whose first lineis a runner comment sends readers to exactly the wrong place; naming the
comment block would be enough.