Repository navigation
Afterparty: OdraVM factory & rollback, panic hook, livenet fixes, macro names, event fields, host leak, CI - #680
Merged
Merged
Conversation
Run new_contract, upgrade_child_contract and batch_upgrade_child_contract natively in OdraVM instead of rejecting them: - ContractContext::new_child_contract / upgrade_child_contract (default: not supported), odra::host::factory helpers used by the generated factory entry points caller. - OdraVM deploys and upgrades children with the factory's caller as their caller, keeps children by name, allows upgrades only by the factory deployer and registers children with the HostEnv for event tracking. - OdraVM no longer holds the contract register borrowed during a call. - Un-ignore the factory example tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reverted call or HostEnv::restore_snapshot now undoes everything Casper would: contract code versions (upgrades, new contracts, factory children), factory admins, events and native events. HostEnv also restores its deployed contracts on restore_snapshot and forgets contracts deployed by a failed call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OdraVM panic hook replaced the process-wide hook on every contract call and printed only reverts and assertion failures, so after the first OdraVM call any other panic in the test binary printed nothing, and a user-installed hook was dropped. Install the hook once, keep the previous one and pass every panic that is not an Odra revert to it. Reverts are flagged by the VM right before it panics, so a nested revert is recognized and unrelated panics are not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 8, 2026
Benchmark report
|
A non-mutable call runs on the host on livenet and a revert unwinds it with a panic. Nothing caught it, so `try_*` on a reverting getter or offchain function panicked instead of returning the error, and the frame of the call stayed on the call stack for whoever caught the panic. The host catches the panic and returns the error the contract reverted with, recorded by the contract env before it panics; any other panic becomes `VmError::Panic`. Call stack frames are popped by a guard, so they are unwound with the call, also in nested getters, whose errors revert the outer call. A revert is logged instead of printed as a panic, other panics go to the previous panic hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A getter executed locally on livenet saw its own address as the caller and a call stack without the account: the host pushed no account frame and `caller()` read the bottom of the stack. The host puts the calling account under the contract, popped by the same guard as the contract frames, and `caller()` is the frame below the current one. A getter called by the host sees the account, a nested one the calling contract, like on OdraVM and CasperVM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmark report
|
An offchain function on CasperVM handed every nested call that was not another offchain function to the VM, which executed it as if the account had called it: the getter saw the account as its caller, a call stack without the offchain function's contract, and its revert came back as `VmError::Panic`. A nested offchain function took `caller()` from the bottom of the call stack, so it saw the account as well. A read-only call without tokens to a known contract runs on the host, one frame deeper, like on livenet, and `caller()` is the frame below the current one, like on OdraVM and livenet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmark report
|
A user error code was named after the first schema that had it, so with contracts reusing small codes a revert came back as another contract's error. The host passes the name of the called contract and its schema is searched first; the other schemas only if it has no such error, since the code does not tell which contract in a nested call reverted. The Odra error codes were decoded by a table that had to list every variant and panicked on any other code, crashing a client older than the contract. `ExecutionError::from_code` decodes them next to the enum, a test fails to compile when a variant is added without it, and an unknown code comes back as `VmError::Other`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deployment that failed on the node came back as
`ContractDeploymentError("Livenet execution error")`, also when the
constructor or the upgrade function reverted, while OdraVM and CasperVM
return the contract's error.
A failed execution is decoded like the error of an entry point call,
named after the schema of the deployed contract; any other failure
(gas not set, the node not reachable, a missing argument) is still a
deployment error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmark report
|
An entry point argument named `contract`, `env_rc` or `named_args` (or `exec_env` before another argument or on a payable or non-reentrant entry point), and an external contract argument named `named_args`, were shadowed by the locals of the generated code and failed with a type error pointing at the user's code. They are reported as reserved names now. Only names that never compiled are rejected; a TODO marks the proper fix (prefixed locals) for 3.0.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmark report
|
The code generated around an entry point (exec parts, the entry points caller, contract and host refs, external contracts, wasm entry points) bound its own locals next to the locals that hold the arguments, so an argument named `contract`, `env_rc`, `exec_env` or `named_args` was shadowed and failed with a type error pointing at the user's code. The locals and closure parameters of the generated code are prefixed with `__odra_` now, so any argument name works, and the check that rejected these names is removed. An `ArgNames` example with such arguments runs on OdraVM and CasperVM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated code also binds the fields of a module, an odra_type and an event to locals named like them, next to its own locals: - `Module::new(env)` clashed with a module field named `env` (which was also skipped when numbering the fields); the parameter and the hidden field are `__odra_env` now (the field was `__env`), - the FromBytes of an odra_type took `bytes`, so a struct or variant field named `bytes` did not compile, and the ToBytes of an enum shadowed a variant field named `result` with its buffer, - `derive(odra::Event)` from casper-event-standard takes `bytes` too; `#[odra::event]` now generates the same impls itself (same bytes, name and schema) with prefixed locals. Storage indices and serialized layouts are unchanged: the bytes of 138 types and events in a scratch matrix and the Module::new of 69 modules are identical before and after, and an example test compares an event with the casper-event-standard derive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmark report
|
An event field whose CLType is or contains `Any` (an odra_type struct or data-carrying enum, also in an Option, Vec, map, Result or tuple) compiled and deployed, and then every emit_event reverted with a bare `Formatting` from casper_event_standard::validate_type. - `Event::has_any` looks into compound types like casper-event-standard (`contract_def::cl_type_has_any`); `Event::validate` names the event and the fields and says what to do. - Deploying or upgrading through `Deployer` (OdraVM, CasperVM, livenet before the transaction) panics with that message: a misdefined contract is a programming error, and `OdraError` has no variant that could carry the explanation. The events come from the new `EntryPointsCallerProvider::contract_events` (default: none), which the macros implement with `HasEvents` (a factory adds its child's). - `ContractBlueprint::new`, used by the schema build, panics the same way, so `cargo odra schema` fails with the message. - On the host a failed emit prints the reason before it reverts with the unchanged `Formatting`; wasm is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Benchmark report
|
1 task done
…tEnv EntryPointsCaller held a HostEnv, and the backends keep the callers of their contracts (OdraVM and livenet registers), so HostEnv -> backend -> register -> caller -> HostEnv was a cycle: dropping the environment never freed the VM or the livenet host. The caller now keeps only the entry points and the dispatch function; the backend passes the contract environment on each call (OdraVM builds one from a weak handle to itself, livenet hands its own, nested livenet calls reach it through a weak handle like CasperVM's offchain env). Factories on OdraVM no longer need the HostEnv of the factory: EntryPointsCallerFn takes no argument, and the children are reported to the HostEnv after each operation through HostContext::take_child_contracts instead of being registered from inside the VM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
Benchmark report
|
Native events: CasperVM and the livenet client count a contract's messages
over all its package versions, oldest first, as OdraVM does, and
HostEnv::upgrade_contract no longer resets its counter. The reset replayed
every native event of the contract into the call after an upgrade on OdraVM,
and a factory child upgraded by its factory kept a stale counter on CasperVM
and livenet. upgrade_contract registers a contract the env did not know
instead of unwrapping after the upgrade went through. Example test on both
VMs, core test with the mock backend.
OdraVM: a contract upgraded into a factory records the account that upgraded
it as the admin of its children; upgrade_child_contract failed with
InvalidContext for everyone. EntryPointsCaller::has_entry_point replaces the
inline checks.
Livenet: HostContext::has_contract asks the node for the package, so a host
ref to a made-up address registers nothing instead of panicking inside new
while reading its event counts; register_contract adds no version for code
registered before under that name.
ContractRegister keeps Rc<ContractContainer>: get_shared replaces the clone
of the entry-point table on every OdraVM and livenet local call.
ODRA_MAX_THREADS set to an empty value counts as not set.
odra::host::revert_hook: one revert-marking panic hook for OdraVM and
livenet instead of two copies.
Macros: ModuleImplIR::child_module_ident replaces five strip_suffix("Factory").
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
…lize The host-side factory helpers read `contract_name` and `args` through ExecutionEnv::get_named_arg, as a generated entry point does, instead of a local wrapper over CallDef::get that reported a malformed argument as a missing one. Their results, and the results of every generated entry point caller, go through the new odra::utils::try_serialize, the fallible counterpart of utils::serialize, instead of the same inline expression in two places. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
Event::validate and assert_events_can_be_emitted run before a deploy and when the schema is generated, both host-side paths already gated on cfg(not(target_arch = "wasm32")); the functions carry the same gate now, and ContractBlueprint::new skips the check in a wasm build, so the intent is visible where the code is and the wasm build does not compile it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
The events checked at deploy belong to the contract type, not to its host ref: OdraContract gets HasEvents as a supertrait and a host-only contract_events() that defaults to Self::events(), and the deployer asks the contract. Only a factory overrides it, in its generated OdraContract impl, to add the events of the child whose code it carries. The method leaves EntryPointsCallerProvider, whose default used to turn the check off for any hand-written provider. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
host.rs models what contracts and tests do on a backend; running that work on the threads of this machine, with ODRA_MAX_THREADS and the per-thread environments, is a concern of the machine and gets a module of its own: the thread limit, ThreadEnvFactory (re-exported from host), the two methods and their tests. HostContext::thread_env_factory is host-only now, like the module. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
…dules host.rs keeps what a test or a script does on a backend: the HostRef traits and HostEnv with its core (accounts, block time, snapshots, the contract lifecycle, calls, balances, storage, gas, transfers). Out of it go: - host/deployer.rs: Deployer with InstallConfig and UpgradeConfig, the constructor and upgrader argument types, and the HostRefLoader impl; - host/context.rs: the HostContext trait a backend implements, next to ContractContext for the contract side; - host/events.rs: the event methods of HostEnv and the per-contract counters behind last_call(), as a second impl block. The tests move with their code; a MockEv mock nothing used is dropped. Public paths are unchanged through re-exports from host. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ten doc comments that rustdoc rejects under -D warnings: a link to a type that does not exist (OdraConfig, now InstallConfig), two URLs in square brackets, a public link to a private item, Option<ValidatorBid> read as an HTML tag (and naming the wrong type), and four explicit link targets the label already resolves to. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
1 similar comment
Benchmark report
|
HostRef left the prelude when the ref_helpers impls started importing the address trait they need themselves; the doc still said both traits were there. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
The map held the installer of each factory, checked by upgrade_child_contract; it is the OdraVM stand-in for Casper's factory group URef, which the installing account holds along with the package's access URef. Both belong to the installer of any package, so the VM now records the deployer of every contract it deploys (and the upgrader of a contract it only loaded), and the factory check reads as "the caller installed this factory". Behaviour is unchanged; the map is also what a direct-upgrade check, which Casper has and OdraVM lacks, would consult. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
…t they cannot check odra/tests/ui becomes odra/tests/macros: the files are the Odra macros from the user's side, built against odra, and the name says so. Twelve tests of odra-macros asserted the same error messages the fail snapshots assert with their spans; they go, and the two snapshots that covered fewer cases than the unit tests gain the missing ones (layout and factory = on, events on an impl block; factory = on on a trait). The positive checks stay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
Entries are one to three lines and say what changes for a user of the crate; related items are folded together (livenet getter fixes, RPC caching, the storage layout API, the custom-backend API changes) and repository housekeeping is left out: regenerated schemas and their CI check, the CI job layout, template extras (AGENTS.md, .env.sample), security doc notes, the Odra.toml registrations of odra-modules, the examples' build binaries, odra-wasm-client's secret handling, and the internal register, revert hook and serialization helpers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The bare `cargo install grcov` resolves grcov's dependencies afresh, and CI failed when every `zip` 8.x release, which grcov 0.10.7 requires, was yanked for a while. With `--locked` the install uses the Cargo.lock grcov ships, and a locked version stays installable when it is yanked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Benchmark report
|
Contributor
|
great |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-ups to the 2.10.0 party. This replaces #678 and #679, and adds livenet fixes (getters, error decoding), an offchain fix for CasperVM macro fixes for argument and field names that clashed with generated code, an early failure for events that can't be emitted, a fix for a memory leak between the host and its backend,
HostRef::newregistering its contract on livenet, the remaining livenettodo!()s (gas report, validator info), odra-cli commands that no longer need a livenet env, and CI/tooling updates.1. Factory modules on OdraVM
Factory modules (
#[odra::module(factory = on)]) now work on OdraVM. Before this change, the generated entry-point caller rejectednew_contractwith "Factory is not supported for this configuration" and had no upgrade entry points.ContractContext::new_child_contractandupgrade_child_contractare new. Their default implementations return the old "not supported" error, so the Wasm and livenet backends are unchanged. A newodra::host::factorymodule holds the helpers that the generated factory entry-point caller calls fornew_contract,upgrade_child_contractandbatch_upgrade_child_contract. They read the call's arguments throughExecutionEnv::get_named_arg, like a generated entry point does, and return their results throughodra::utils::try_serialize.initandupgradesee the factory's caller as their caller.InvalidContext.HostEnv, so their events are tracked.Rc(ContractRegister::get_shared), so this costs a refcount per call, not a copy of the entry-point table.upgrade(plain or factory child) left the new code running on the old storage. A failedinitleft the contract or child registered.factory_group; that is what the child-upgrade check compares against, and it is rolled back too.init.HostEnv::take_snapshot/restore_snapshotnow cover contract code and the list of deployed contracts. Before, contracts deployed or upgraded after the snapshot survived a restore, and event counts went stale.HostEnvalso forgets contracts deployed by a call that failed.initand upgrade, snapshot restore, and events of reverted (and nested reverted) calls.Differences from Casper
HostRefLoader::loadhas no recorded deployer on OdraVM, so its children can't be upgraded there.Still different from Casper after a revert
2. The OdraVM panic hook no longer silences panics
The OdraVM panic hook was hiding the output of other panics in the test binary.
unwrap(),expect(),panic!()andtodo!(), in every test thread of the binary, CasperVM and plain unit tests included. You gotFAILEDand an empty output.What changed:
std::sync::Once), keeps the previous hook, and passes every panic that is not an Odra revert to it, assertions included. The standard message, location andRUST_BACKTRACEoutput work again.OdraVm::revertflags the revert (thread-local) right before it panics. Only flagged panics get the compact Odra output. So reverts in nested calls are recognised, and a user's ownpanic!("Revert: …")is not.odra::host::revert_hook(install,mark_revert(reporter)). OdraVM passes its compact printer, livenet a reporter that prints nothing; a custom backend that unwinds reverts with a panic can use it too.unwrapin an entry point) now prints the standard panic message. It is still returned asVmError::Panic.Before / after
Each scenario makes one OdraVM contract call first. The output is from
odra-vm/tests/panic_hook.rs.unwrap()onNoneBefore:
After:
assert_eq!(1, 2)Before (custom format, no column, no backtrace support):
After (standard output):
User hook set before the first call (
panic!("boom"))Before:
After:
Contract revert (nested call): unchanged
Tests:
odra-vm/tests/panic_hook.rsruns each scenario in a child process and checks its stderr.3. Livenet: getters that revert return an error
On livenet, a getter runs locally against RPC-backed state. When it reverted,
LivenetContractEnv::revertpanicked and nothing caught the panic.try_*didn't work for getters. It panicked instead of returningErr, which differs from OdraVM and CasperVM.Errfrom a nested local call panicked throughunwrap().What changed:
revert()stores the error before it panics. The local call runs insidecatch_unwindand returns that error, orVmError::Panicwhen the panic wasn't a revert.Errreverts with that error, as on CasperVM. The contract register is no longer locked while the call runs.revert_hookshared with OdraVM keeps expected reverts quiet; other panics print normally.#[odra(offchain)]functions and contracts opened withHostRefLoader::loadtake the same path, so they're fixed too.Before, on nctl,
try_peek()on an empty stack aborted the run:After, it returns
Err(EmptyStack).3b. Livenet: a getter's
caller()is the calling accountA getter run locally used to see the contract itself as
caller(), and itscall_stack()had no account frame. The host now pushes an account frame first, as CasperVM's offchain path does.caller()reads the frame below the current one, which is OdraVM's rule: a top-level getter sees the account, and a nested one sees the calling contract.Before, on nctl,
CallStackProbe::inspect()returned this:After, it matches OdraVM and CasperVM. I checked every getter in
modules/andexamples/that readscaller(),call_stack()ornth_caller; none breaks.Tests:
examples/bin/livenet_tests.rsscenario on nctl: run before and after each fix.3c. CasperVM: offchain functions see the calling contract
On CasperVM, an
#[odra(offchain)]function that called a regular getter handed the call to the wasm VM, which ran it as if the account had called it directly. The getter saw the account as its caller, and its callstack skipped the offchain function's contract. A revert in it came back asVmError::Panic, and the panic was printed three times. A nested offchain function also tookcaller()from the bottom of the callstack.What changed:
caller()is the frame below the current one, as on OdraVM and livenet.Before / after on CasperVM (OdraVM was already right):
relay_offchain():(Account, None, [Account, probe]).(relay, Some(Account), [Account, relay, probe]).try_offchain_peek():Err(VmError(Panic)).Err(EmptyStack).4. Livenet: decoding errors
The node reports a failed transaction only as a number (
User error: N). Livenet turns that number back into anOdraError.Built-in errors:
panic!("Unknown execution error code"). A code the client didn't know, for example from a contract built with a newer Odra, crashed the client.ExecutionError::from_code, next to the enum, replaces the table. An unknown code becomesVmError::Otherwith the node's message.match, so adding a variant without updating it breaks the test.Errors your contracts define:
resources/casper_contract_schemas/with a matching code, whichever contract had reverted. Most contracts number their errors 1, 2, 3, so names often came from another contract.Before, on nctl,
TimeLockWalletreverted with code 2:The assertion on
try_function_that_reverts() == Err(SillyError)inlivenet_tests.rswas a TODO; it's re-enabled.4b. Livenet: a reverting
initorupgradereturns the contract's errornew_contractandupgrade_contractwrapped every failure asContractDeploymentError. Now a failed execution reported by the node is decoded the same way, first against the schema of the contract being deployed.ContractDeploymentErrorstays for real deployment failures, such as gas not set, RPC or network errors, a missing argument, or an engine error with no code. OdraVM and CasperVM already returned the contract's error, so all three backends now agree. This changes the error livenet deploys return (CHANGELOG → Changed).Before, on nctl:
Tests:
from_coderound-trip test.examples:reverted_install_returns_the_errorpasses on both VMs.try_deployandtry_upgrade, and the wrong-name case.CounterV3gets a committed schema, because there's none for it otherwise.5. Macros: user names no longer clash with generated code
The code generated by the macros bound the user's argument and field names as locals, next to its own locals with plain names. A user name equal to one of those shadowed it or was shadowed by it.
Arguments (
#[odra::module]entry points, external contracts):contract,env_rc,named_args: never compiled.exec_env: failed when another argument followed it, or on a payable or non-reentrant entry point.Fields:
env: didn't compile. It was also skipped when the fields were numbered.bytes: didn't compile in#[odra::odra_type]structs, enum variants and#[odra::event]s.result: compiled, butto_bytesshadowed it with its own buffer, so the value didn't round-trip (it panicked in debug).Fix: every local and parameter the macros generate that shares a scope with user names is now prefixed with
__odra_(__odra_contract,__odra_env_rc,__odra_exec_env,__odra_named_args,__odra_result,__odra_bytes,__odra_env, …).#[odra::event]no longer derivesodra::Event(casper-event-standard), whose generated code clashed onbytes. It generates the sameCLTyped,ToBytes,FromBytesandEventInstanceimpls itself.env(),address(), …), and neither were entry point names or runtime argument keys (gas,attached_value,contract_name).Compatibility:
odra_typeand event test cases that compiled before are identical before and after.Module::newfield numbering) of all 69 modules are identical.#[odra::event]against#[derive(odra::Event)]: bytes, length, name and schema are equal.execute_init(__odra_env: ContractEnv)) are not part of the API, since Rust passes arguments by position.__envis now__odra_env. It's undocumented (useself.env()), and nothing in odra, the cargo-odra templates or the docs uses it.Tests:
init,upgrade, trait impl, factory and external contract. 37 field names were tried in modules,odra_typestructs and enums, and events. Every case compiles natively and as wasm.ArgNamesandFieldNamespass on OdraVM and CasperVM.6. Events: fields without a concrete CLType are refused at deploy
Casper has no
CLTypefor a user struct or a data-carrying enum, so#[odra::odra_type]gives thoseCLType::Any. casper-event-standard forbidsAnyin events, because indexers couldn't decode them. An event with such a field, including one nested inOption,Vec,Map,Resultor a tuple, compiled and deployed fine. Every emit then reverted with a bareExecutionError::Formatting.Now deploy, upgrade and schema generation (
cargo odra schema) panic with:try_deploy_with_cfgandtry_upgrade_with_cfg, before anything is deployed. That covers OdraVM, CasperVM, livenet (before a transaction is sent) and odra-cli. It panics rather than returningErr, because a wrongly defined contract is a programming error.Event::validateandassert_events_can_be_emittedare compiled for the host only (cfg(not(target_arch = "wasm32"))), like their callers; a wasm build carries none of it.OdraContract::contract_events(), which defaults to the contract'sHasEvents::events()(HasEventsis now a supertrait ofOdraContract). A factory overrides it to add the child module's events, since the factory wasm carries the child's code.Event::has_anynow checks inside containers, as casper-event-standard does.HostRefLoader::load, so a contract that's already deployed stays usable.Formatting. In tests, the host also prints the explanation.7. The backend is freed with the last
HostEnv(memory leak)EntryPointsCallerheld theHostEnvit was built with, and backends store the callers of deployed contracts in their registers. That made a loop of strong references:HostEnv→ backend →OdraVm→ register →EntryPointsCaller→HostEnv. After a single deploy, dropping the env never freed the VM, so each OdraVM test leaked its whole state until the process exited. Livenet had the same loop. CasperVM had already worked around it.What changed:
EntryPointsCallerkeeps only the entry points and the function. The backend passes theContractEnvin:call(contract_env, call_def).ContractEnvfor each call.EntryPointsCallerFnno longer takes a&HostEnv, so building a child's caller needs no env. The VM queues new children, andHostEnvcollects them after each deploy, upgrade or call through the newHostContext::take_child_contracts(), which has a default. Children of a failed call are dropped. This replaces the "deployed before / retain" logic from section 1.Public API changes: these affect hand-written callers and custom backends only. Code using
#[odra::module],Deployer,HostReforloadis unchanged, and the macros generate the new form. The 2.10.0 migration guide has the table.EntryPointsCaller::new(env, eps, f)→new(eps, f), andcall(call_def)→call(contract_env, call_def).host_env()is removed.entry_points_caller(&env)→entry_points_caller(), andEntryPointsCallerFn = fn() -> EntryPointsCaller.ContractRegister::callandContractContainer::calltake aContractEnv.OdraVm::default()is removed; useOdraVm::new().Tests:
odra-vm/tests/no_leak.rskeeps aWeak<OdraVm>and drops the env. It covers a plain contract, a factory with a deployed and upgraded child, a snapshot restore, andload. A livenet test does the same with an offline host. All of them failed before this change.8.
XHostRef::new(address, env)works likeX::loadX::load(&env, address)registers the contract's code with the env, while the generatedXHostRef::newonly wrapped the address. On OdraVM and CasperVM that made no difference. Livenet, though, runs getters locally from its contract register. For an address wrapped withnew, which is what the factory examples do for children, transactions worked, but every getter failed with a bareVmError::InvalidContractAddress.What changed:
newregisters likeload. The generatednewcalls the newHostEnv::register_contract_ref, which does nothing for an address the env already tracks under that name. Soload, deploy and upgrade, which callnewinternally, register only once.HostEnv::register_contractno longer resets the event baseline or version of an address it already knows. On livenet a different contract name adds that code (e.g.TenCounterHostRef::newon an upgraded child), while upgrades still replace the code. Code registered before under a name, as any version, is not added again, so alternating host refs of two types for one address doesn't grow the register.HostContext::has_contract, which has a default oftrue. OdraVM checks its register, CasperVM checks that the package exists, and livenet asks the node whether a package is stored at the address (one global-state query, cached per state root hash, only for the firstnewof an address the env doesn't know). Sonewwith a made-up address doesn't make it callable, and on livenet it no longer panics insidenewwhile reading the event counts of a contract that isn't there.#[odra::external_contract]HostRef has no local code to register. A getter on it, or on any unregistered address, on livenet now logs what to do: "No contract code registered for …: … Register it withContract::load(&env, address)(orContractHostRef::new(address, env)) … A contract known only by its interface can be called only with transactions." The error is stillInvalidContractAddress.newnow has its events tracked (last_call()) and its offchain functions work, as afterload.Tests:
factory_child_by_addressscenario creates a factory child, wraps it withCounterHostRef::new, and calls a getter, a transaction and the getter again (10 → 11). Before the fix the getter failed withInvalidContractAddress. The upgrade and odra-cli factory scenarios were also re-run.newsees exactly the right events, for a factory child and for a deployed token. A secondnewor aloaddoesn't change that. Without the fix this fails on CasperVM.register_contract_refskips nonexistent addresses, registers once per name, and keeps event counts, plus the livenet error message.9. Livenet: gas report, validator info, named args
Three livenet backend methods were
todo!()or returned a fake value.gas_report()panicked, andlast_call_gas_cost()always returned 0. The client now records the gas each transaction used, taken from the node's execution result: V2consumed, or V1costfor V1 results. That's the same unit CasperVM reports. V2costisn't used, because it is the charged payment limit, not what the call used.WasmDeploy { file_name: "Name.wasm" }, and calls, direct or through the proxy, asContractCall.HostEnv::concurrentlyworker keeps its own report.get_validator_infoin a locally executed getter reads the validator's bid (Key::BidAddr(BidAddr::Validator)) from the node. It returnsNonewhen the node has no bid for the validator.None(get_named_arg_bytes→MissingArg), as on CasperVM's offchain env.On nctl:
An unknown validator gives
None, which the example contract unwraps intoExecutionError(UnwrapError), as the test expects.Tests:
gas_is_reportedinlivenet_tests, and the validator-info checks invalidators_on_livenet.10. Native events are counted over all versions of a contract
Casper keeps a message (native event) with the entity that emitted it, and an upgrade installs a new entity, so on CasperVM and livenet the count of a contract's native events restarted at every upgrade.
HostEnvworked around it by resetting its own counter inupgrade_contract:upgrade_child_contractgot no reset, so its counter went stale and its native events after the upgrade were missing fromlast_call()until the new version's count overtook the old one.What changed:
native_events_count,get_native_eventandlast_call().native_events()now carry on across an upgrade on all three backends, as CES events do.HostEnv::upgrade_contractno longer resets the counter. It also registers a contract the environment never deployed, loaded or wrapped in a host ref, instead ofunwrap()ing after the upgrade had gone through.Tests:
native_events_carry_on_across_an_upgradeinexamplesdeploysPartyContractupgradable, emits, upgrades it with the same code and emits again: one native event inlast_call(), three in total. On the old code it fails on both VMs (3 events in the last call). A core test with the mock backend covers the upgrade of an unknown address.11. odra-cli:
--help,completionsandconfigwork without a livenet envOdraCli::new()created the livenetHostEnvbefore the builder and before argument parsing. With theODRA_CASPER_LIVENET_*variables missing:--help,completionsandconfig.Before (no
ODRA_*vars, no TTY):What changed:
run()parses the arguments first, so--help(also of subcommands),--versionand usage errors are handled by clap.OnceCelland created by the first command that needs it: deploy, contract, scenario, print-events, whoami, status, inspect, storage, transfer and repl. Prompts and error messages for those are unchanged, and the REPL still creates it once per session.completionsneeds no env.confignever prompts. With an incomplete configuration it prints each variable (<not set> ($VAR)), the caller as<unavailable>and the problem, and exits 0. In--json,caller_addressisnullin that case and aproblemfield appears.Tests:
odra-cli/tests/offline_commands.rsruns the CLI as a child process with everyODRA_*var removed, stdin closed and a scratch working directory. It covers help, subcommand help, a usage error, completions,config,config --json, and checks that network commands still fail with the same message. These checks fail on the old code.12. CI and tooling
cargo-odra: CI installs it from
release/0.2.0instead ofrelease/0.1.7. That branch contains cargo-odra for Odra 2.10: dependencies, workspace inheritance, dependency-crate contracts, CI cargo-odra#103 and Fix non-semver casper-node update. #105: dependency-crate contracts, no wasm copies in workspace members, and the workspace-inheritance fix.examples/ourcoininheritslicensefrom the workspace again; the old cargo-odra needed a hard-coded value. Thegenerate-clientworkaround in the justfile stays, becausegenerate-clientrefuses single-crate projects cargo-odra#104 is still open and still reproduces with 0.2.0.Benchmark workflow:
dawidd6/action-download-artifactgoes from v4 to v27. This fixes the Dependabot artifact-poisoning alert. Withif_no_artifact_found: warn, a PR whose base branch has no benchmark report gets a notice and skips the evaluation and the comment instead of failing.just prepare-test-env:sudo apt-get update && sudo apt-get install -y wabt, so it runs unattended.odra-wasm-client/example:npm audit fixplus removing the unusedhttp-proxy-middlewaredevDependency takesnpm auditfrom 10 vulnerabilities (1 critical) to 0.npm run build) and serves its pages.unexpected_cfgswarnings: the macros wrap every item gated onodra_modulein#[allow(unexpected_cfgs)] const _: () = { … };, so a crate withoutodra-buildin itsbuild.rsno longer warnsunexpected cfg condition name: odra_module.odra-buildalso declaresodra_backend.just clippyno longer needs--allow unexpected_cfgs.Build binaries: the
_build_contract/_build_schemabinaries of examples, ourcoin, modules and benchmark build on the host, as in the templates, so a plaincargo build --workspaceworks.cargo odra buildproduces the same wasm.benchmark/build.rscallsodra_build::build().Factory
unused_variables: generated factory functions get#[allow(unused_variables)]. The file-wide allows that hid this inexamples,cep95andcep96are removed, along with a deadcallerlocal incep95::approve.odra-clino longer has a build script. It calledodra_build::build()without using the cfg, so it was recompiled for every contract duringcargo odra schema(about 7% faster schema generation inexamples).Committed schemas: 48 were out of date (toolchain, version, and real interface changes such as new entry points and arguments) and 37 were missing; all are regenerated. A new
just check-schemasregenerates them and fails on any diff. It runs in its own workflow (check-schemas.yml), only when macros, core, odra-build, odra-schema, modules, examples, the root manifest, the toolchain or the justfile change, because generation rebuilds the contract crate once per contract (about 20 minutes).Checked with cargo-odra 0.2.0 installed into a scratch root:
examples,examples/ourcoinandmoduleson both backends.new,test,test -b casper,schema, CLI build, andgenerate-clientforworkspace) for cep18, full, workspace and cep95.The benchmark workflow change has only been checked by parsing the YAML; it runs for real on this PR.
13. Macros:
#[odra(..)]errors are reported; UI testsSwallowed attribute errors. The checks for
payable,non_reentrantandoffchainusedpartition_attributes(..).unwrap_or_default(). So an unknown argument (#[odra(paybale)],#[odra(view)]) or a duplicate one (#[odra(payable, payable)]) compiled, and the function silently lost the flag. The function's other attributes were dropped too. Each#[odra(..)]is now parsed once, and problems are reported at the argument:paybale, expected one of: payable, non_reentrant, offchain"payable", also across two#[odra(..)]attributespayabletakes no value"This applies to public, private, trait-impl, factory, delegated and offchain functions, and to
#[odra::external_contract]trait functions. It can break builds of code with a wrong attribute, which never had an effect. Nothing in this repo or the templates used one.Private helpers. Private functions of a plain module impl aren't entry points, so they no longer get the entry-point name and argument checks: they may be called
new,env,address, … or takegas/attached_value. The generated code calls<X as Module>::newfully qualified, so nothing clashes. This was verified in plain and factory modules, including wasm. Their#[odra(..)]is still validated.Compile tests of the macros (trybuild) live in
odra/tests/macros/{pass,fail}with the runnerodra/tests/macros.rs: everypassfile is built as a crate againstodra, everyfailfile must fail with the diagnostics in its.stderrsnapshot (message and span). They run undercargo testin the OdraVM job, with the pinned toolchain. The macro crate's own tests keep checking what is generated; its twelve tests that only asserted an error message were dropped, since the snapshots check the same messages plus their spans (twofailcases gained thelayoutandfactory = on, eventsarguments on an impl block andfactory = onon a trait to cover everything they did).#![deny(warnings)], sounexpected_cfgsand unused-variable warnings from generated code also fail it. It covers:payable/non_reentrantoninit/upgrade;Listas aMappingkey;ref_helpersmisuse;#[odra(..)], and#[odra(..)]on a field.14. Small API additions
self.contract_address()in#[odra::ref_helpers]. A helper couldn't use the contract's own address on both refs:ContractRef::address()returns&Address, whileHostRef::contract_address()returnsAddressand needsHostRefimported.ContractAddresstrait inodra::preludegives everyContractRefacontract_address() -> Address.ref_helperssit in anonymous const blocks that import the providing trait (ContractAddressorHostRef). Helpers need no imports, and nothing changes in scope outside them.Token::own_balance()returnsself.balance_of(&self.contract_address()), tested on both VMs and in a UI test.HostEnv::concurrently.concurrently_with(max_threads, items, f)sets the limit in code; 0 is treated as 1.concurrentlyreadsODRA_MAX_THREADSon each call, with a default of 8, so a rate-limited node can be spared without code changes. A value that isn't a number panics; an empty one counts as unset.const fn u512sits next tou256.cspr!with a literal works in aconst, and that is now documented and tested.last_call()had no events there. A scenario can now overrideScenario::captures_events() -> bool, which defaults tofalse.EntryPointsCaller::has_entry_point(name)replaces the inlineentry_points().iter().any(..)checks in the backends.odra::utils::try_serialize(&value)is the fallible counterpart ofutils::serialize: the value asBytes, or the error an entry point returns. The generated entry-point callers return their results through it instead of an inlineto_bytes().map(..).map_err(..)expression, and the factory helpers use it too.Test plan
Docs: odradev/odradev.github.io#60 (branch
feature/afterparty).cargo test(workspace)release/0.2.0(cep18, full, workspace, cep95)cargo odra testand-b casperinexamples(112 passed each) andmodules(141 passed each), plusexamples/ourcoinjust test-livenetagainst nctl (makesoftware/casper-nctl:v203) at the head of this branch:livenet_tests,validators_on_livenet,upgrade_on_livenetand all 11 odra-cli commands passjust clippy(incl.--features=livenet),cargo fmt -- --checkcargo check -p odra-core --target wasm32-unknown-unknowncargo test --workspace(682 passed), the examples on CasperVM with the built wasm (113 passed),just clippy,cargo fmt --check, core on wasm32just test-livenetagainst nctl (makesoftware/casper-nctl:v203) after the review fixes:livenet_tests(factory_child_by_addressgoes through the new livenethas_contractquery),validators_on_livenet,upgrade_on_livenetand all 11 odra-cli commands pass; thenative-eventsscenario reads its counts through the per-version lookup🤖 Generated with Claude Code