Repository navigation
gl.get_contract_at(...).view() dispatches to __handle_undefined_method__ despite target method being correctly registered in schema #420
Description
Activity
Traced this as far as I could with static analysis of the pinned SDK (genlayer-py 0.19.0rc2, matching your report) — the client-side call construction looks correct, which narrows where the actual bug has to live.
What I checked
gl.get_contract_at(addr).view().read_it()resolves like this ingenlayer/contract/__init__.py:.view()returns_ContractAtGetter(_ContractAtViewMethod, address, state, catch_vm_error)._ContractAtGetter.__getattr__('read_it')correctly captures the accessed name and constructs_ContractAtViewMethod('read_it', address, state, catch_vm_error)— the method name is captured correctly here, no typo/mangling.- Calling that instance sends a
CallContractmessage viagl_call_generic, withcalldata: _make_calldata_obj('read_it', args, kwargs), which produces{'': 'read_it', ...}— the target method name is encoded under the empty-string key.
I couldn't find anywhere in this package where the decision to fall through to
__handle_undefined_method__gets made — that method is defined as an abstract fallback on the baseContractclass (Contract.__handle_undefined_method__.__isabstractmethod__ = True), but nothing ingenlayer.contractitself resolves an incoming calldata's''key against the target contract's registered methods. That resolution has to happen in the GenVM host (Rust), which I don't have matching source access to for this pinned build — I can't verify further without either building/running it locally (not something I could do reliably) or someone withgenvm-executor/genvm-manageraccess checking the actual method-lookup path for cross-contractCallContractdispatch.What this rules in/out
The SDK's call-construction side looks correct end-to-end — the method name isn't getting lost, mangled, or mis-encoded before it leaves the caller. If this is a real dispatch bug (not e.g. schema-cache staleness specific to Studio Next), it's most likely in how the GenVM host resolves the
''calldata key against the target contract's method table forCallContract-triggered calls specifically (as opposed to the initial externalis_init/top-level call, which presumably works fine or every contract call would fail, not just cross-contract ones).Not a fix, but hopefully narrows where to look.
Reacted by Snehal707Thanks for tracing this, really helpful to have independent confirmation that the client-side call construction is clean. Matches what we found from our side.
Given it looks like GenVM host-level resolution of the CallContract dispatch for the ''-keyed calldata, we don't have access to chase this further either — happy to wait for anyone with genvm-executor/genvm-manager visibility to pick it up from here.
One tangentially related data point, in case it's useful: while separately working through #421 (Studio Next deployment), we found that a stale/mismatched runner ID caused a related-sounding failure (invalid_contract runner malformed) — the fix there was pulling the currently-correct runner ID directly from Studio Next's live served assets rather than trusting documented examples. Not claiming that's the same root cause here, but flagging in case a runner/build mismatch is worth ruling in or out for this dispatch issue too.
Will keep this open and update if we find anything further from our side.That runner-ID-mismatch lead is worth ruling in — I actually have first-party evidence of exactly that instability from my own testing today, on the same "py-genlayer:latest" tag.
Two separate runs of the identical
direct_deploycommand, a few minutes apart, in the same session, resolved "latest" to two different runner releases:Run 1: Downloading https://github.com/genlayerlabs/genvm-manager/releases/download/v0.6.0-rc3/genvm-runners-all.tar.xz Run 2: Downloading https://github.com/genlayerlabs/genvm-manager/releases/download/v0.6.0-rc5/genvm-runners-all.tar.xz(Separately, both of my local runs also logged
Warning: could not resolve latest GenVM version (HTTP Error 403: Forbidden); checking the local cachebefore falling back — so "latest" resolution is itself flaky, on top of whatever it resolves to when it does succeed.)So: within a single testing session, "latest" moved across an RC boundary. If ICStoreProbe and Caller in the original repro were deployed far enough apart in time (or against different environments/caches) to land on different actual runner builds, that's a very plausible way to get exactly this symptom — two contracts that look schema-compatible from the outside but were compiled/dispatched against subtly different runner internals for cross-contract calls specifically.
Doesn't confirm the mechanism, but it's a second independent signal (yours from #421's runner-ID fix, mine from directly observing "latest" drift) pointing at the same category of cause. Might be worth re-running the original #420 repro with both contracts pinned to the exact same explicit runner hash (not "latest") to see if that alone makes the dispatch succeed — that would isolate this cleanly.
Completed the three follow-ups.
- Original store's pin verified — retrieved deployed source directly, both original store and caller declare the identical runner pin (
py-genlayer:1jb45aa8ynh2a9c9xn3b7qqh8sm5q93hwfp7jqmwsfhh8jpz09h6). Runner-pin mismatch is ruled out as the explanation for the original failure — they were never mismatched. - Write path reproduced — submitted
capture_targetagainst a same-pinned setup on the matching endpoint. Transaction0xe8277ac15468c8228aab09249bb7b10f816fdb12e65042537349c4e758f8fedf: executionSUCCESS,captured()changed from empty string to the expected value. - Network confirmed as a genuine match, not assumed — decoded the original failed transaction's signed chain ID directly rather than trusting the issue text alone. It's
61999, matching today's endpoint.
Both original contracts' retrieved sources declare the same runner pin, and today's same-pin control successfully executes
capture_targeton the matching endpoint and chain. The failure did not reproduce in this control. We haven't established why the outcomes differ; host/runtime changes remain one possibility, not a confirmed cause.- Original store's pin verified — retrieved deployed source directly, both original store and caller declare the identical runner pin (
Summary
Calling a correctly-defined, correctly-decorated
@gl.public.viewmethod on a separately deployed Intelligent Contract throughgl.get_contract_at(address).view().method_name()fails withcall to private method Contract.__handle_undefined_method__/call to private method Contract.__receive__, even thoughgen_getContractSchemaconfirms that the target method is registered and visible.Environment
https://studio.genlayer.com/api)genlayer-test0.30.0rc2genlayer-py0.19.0rc2Reproduction
Target contract:
value = "probe-value"; deployment finalizes before the call.gen_getContractSchemafor the deployed target. It returns:{ "read_it": { "params": [], "kwparams": {}, "readonly": true, "ret": "string" } }gen_call failed (code=-32000): execution failed.Ruled out
@gl.contract_interfacevariant also failed.0x2a9b9962b5fa117bbefee0dea26cd73fc8a9545db8927a41b5851634eccdc041reachedFINALIZED, but its leader/validatorexecution_resultwasERROR.The detailed GenVM stderr from the dynamic/write attempt was:
Conclusion
The target method is correctly defined and schema-visible, yet the calling runtime dispatches to the undefined-method fallback path. This appears to be a dispatch/resolution bug in the IC-to-IC call mechanism rather than a contract-authoring error.
Question
Is
gl.get_contract_at(...).view()known to work in the current StudioNet build? Is there a required registration/configuration step beyond correct method decoration, or is this a runtime bug?