Skip to content

Afterparty: OdraVM factory & rollback, panic hook, livenet fixes, macro names, event fields, host leak, CI - #680

Merged
kubaplas merged 44 commits into
release/2.10.0from
feature/afterparty
Oct 19, 2026
Merged

kubaplas merged 44 commits into
release/2.10.0from
feature/afterparty

Conversation

@kubaplas

@kubaplas kubaplas commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

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::new registering its contract on livenet, the remaining livenet todo!()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 rejected new_contract with "Factory is not supported for this configuration" and had no upgrade entry points.

  • Core: ContractContext::new_child_contract and upgrade_child_contract are new. Their default implementations return the old "not supported" error, so the Wasm and livenet backends are unchanged. A new odra::host::factory module holds the helpers that the generated factory entry-point caller calls for new_contract, upgrade_child_contract and batch_upgrade_child_contract. They read the call's arguments through ExecutionEnv::get_named_arg, like a generated entry point does, and return their results through odra::utils::try_serialize.
  • OdraVM behaves like Casper:
    • A child's init and upgrade see the factory's caller as their caller.
    • The factory stores its children by name.
    • Only the account that deployed the factory, or upgraded a contract into one, can upgrade its children. Anyone else gets InvalidContext.
    • Children are registered with the HostEnv, so their events are tracked.
  • OdraVM: the contract register is no longer borrowed during a call, so a contract can deploy or upgrade another one. The register hands its containers out as Rc (ContractRegister::get_shared), so this costs a refcount per call, not a copy of the entry-point table.
  • OdraVM rolls back everything a reverted call did, as Casper does. Previously only storage was rolled back:
    • Contract code: a failed upgrade (plain or factory child) left the new code running on the old storage. A failed init left the contract or child registered.
    • Deployers: OdraVM records the account that installed each contract, which on Casper holds the package's access URef and a factory's factory_group; that is what the child-upgrade check compares against, and it is rolled back too.
    • Events and native events: events emitted before the revert are dropped, including those from nested calls and from a child's init.
  • HostEnv::take_snapshot/restore_snapshot now 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. HostEnv also forgets contracts deployed by a call that failed.
  • Examples: the factory tests are no longer ignored. New tests cover reverted upgrades, a reverted child init and upgrade, snapshot restore, and events of reverted (and nested reverted) calls.

Differences from Casper

  • The child's access URef on OdraVM is a stand-in derived from the child's address.
  • The upgrade permission check compares the direct caller. Casper checks group access instead.
  • A factory loaded with HostRefLoader::load has no recorded deployer on OdraVM, so its children can't be upgraded there.

Still different from Casper after a revert

  • Plain Rust panics in a contract (not reverts) still roll back nothing.
  • The address counter isn't rolled back, so a reverted deploy uses up an address.

2. The OdraVM panic hook no longer silences panics

The OdraVM panic hook was hiding the output of other panics in the test binary.

  • It replaced the process-wide hook on every contract call. It never passed panics on to the previous hook.
  • After the first OdraVM call, any panic that wasn't a revert or an assertion printed nothing. That covers unwrap(), expect(), panic!() and todo!(), in every test thread of the binary, CasperVM and plain unit tests included. You got FAILED and an empty output.
  • A hook the user had installed was silently dropped.

What changed:

  • Installed once. The hook is set up once (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 and RUST_BACKTRACE output work again.
  • Reverts are flagged. OdraVm::revert flags 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 own panic!("Revert: …") is not.
  • Revert output is unchanged.
  • One hook for OdraVM and livenet. The marking and the chaining live in 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.
  • Side effect: a non-revert panic inside contract code on OdraVM (e.g. unwrap in an entry point) now prints the standard panic message. It is still returned as VmError::Panic.

Before / after

Each scenario makes one OdraVM contract call first. The output is from odra-vm/tests/panic_hook.rs.

unwrap() on None

Before:

child --- FAILED

After:

thread 'child' panicked at odra-vm/tests/panic_hook.rs:65:54:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
child --- FAILED

assert_eq!(1, 2)

Before (custom format, no column, no backtrace support):

💥 assertion `left == right` failed
  left: 1
 right: 2
    ↳ at odra-vm/tests/panic_hook.rs:69
child --- FAILED

After (standard output):

thread 'child' panicked at odra-vm/tests/panic_hook.rs:69:13:
assertion `left == right` failed
  left: 1
 right: 2
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
child --- FAILED

User hook set before the first call (panic!("boom"))

Before:

child --- FAILED

After:

user hook: boom
child --- FAILED

Contract revert (nested call): unchanged

Contract(ContractPackageHash(1000…0000)))::fail
💣 Boom(7)
↳ Stack trace:
  ↳ contract: "Contract"
    ↳ entry point: fail
  ↳ contract: "Contract"
    ↳ entry point: nested
  ↳ caller: Account(AccountHash(0ee6…f309))

Tests: odra-vm/tests/panic_hook.rs runs 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::revert panicked and nothing caught the panic.

  • try_* didn't work for getters. It panicked instead of returning Err, which differs from OdraVM and CasperVM.
  • The callstack frame was never popped. Anything up the stack that did catch the panic was left with a stale frame.
  • Nested getters. An Err from a nested local call panicked through unwrap().

What changed:

  • Catching the revert: revert() stores the error before it panics. The local call runs inside catch_unwind and returns that error, or VmError::Panic when the panic wasn't a revert.
  • Popping frames: frames are popped by a drop guard, so they also go on unwind.
  • Nested calls: a nested Err reverts with that error, as on CasperVM. The contract register is no longer locked while the call runs.
  • Panic hook: the revert_hook shared with OdraVM keeps expected reverts quiet; other panics print normally.
  • Also covered: #[odra(offchain)] functions and contracts opened with HostRefLoader::load take the same path, so they're fixed too.

Before, on nctl, try_peek() on an empty stack aborted the run:

thread 'main' panicked at odra-casper/livenet-env/src/livenet_contract_env.rs:152:9:
Revert: ExecutionError(User(UserError { code: 2, message: "EmptyStack" })) - Contract(…)::peek

After, it returns Err(EmptyStack).

3b. Livenet: a getter's caller() is the calling account

A getter run locally used to see the contract itself as caller(), and its call_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:

  left: (Contract(…5c3a71…), None, [Contract(…5c3a71…)])
 right: (Account(…2b263c…), None, [Account(…2b263c…), Contract(…5c3a71…)])

After, it matches OdraVM and CasperVM. I checked every getter in modules/ and examples/ that reads caller(), call_stack() or nth_caller; none breaks.

Tests:

  • livenet-env unit tests (no node): revert, nested revert, non-revert panic, nested unknown entry point, panic output in a child process, and caller and callstack after each call.
  • examples/bin/livenet_tests.rs scenario 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 as VmError::Panic, and the panic was printed three times. A nested offchain function also took caller() from the bottom of the callstack.

What changed:

  • Nested calls run on the host. A read-only call with no tokens attached, to a contract the host knows, now runs on the host one frame deeper, as on livenet. Calls with tokens attached, or to unknown contracts such as factory children, still go to the VM. Calls that change state are still rejected.
  • caller() is the frame below the current one, as on OdraVM and livenet.

Before / after on CasperVM (OdraVM was already right):

  • relay_offchain():
    • Before: (Account, None, [Account, probe]).
    • After: (relay, Some(Account), [Account, relay, probe]).
  • try_offchain_peek():
    • Before: Err(VmError(Panic)).
    • After: 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 an OdraError.

Built-in errors:

  • A hand-maintained table. It mapped Odra's built-in codes back to errors, and fell back to 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.
  • One place now. The new ExecutionError::from_code, next to the enum, replaces the table. An unknown code becomes VmError::Other with the node's message.
  • It can't drift. A test round-trips every variant through an exhaustive match, so adding a variant without updating it breaks the test.

Errors your contracts define:

  • The wrong contract won. The decoder took the first schema in 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.
  • The called contract's schema is now searched first. The others are searched only if it has no such code, for example for a nested revert, where the code alone can't identify the inner contract. Schemas are searched in sorted order, nearest directory first.

Before, on nctl, TimeLockWallet reverted with code 2:

  left: Err(ExecutionError(User(UserError { code: 2, message: "NotAnOwner" })))
 right: Err(ExecutionError(User(UserError { code: 2, message: "CannotLockTwice" })))

The assertion on try_function_that_reverts() == Err(SillyError) in livenet_tests.rs was a TODO; it's re-enabled.

4b. Livenet: a reverting init or upgrade returns the contract's error

new_contract and upgrade_contract wrapped every failure as ContractDeploymentError. Now a failed execution reported by the node is decoded the same way, first against the schema of the contract being deployed. ContractDeploymentError stays 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:

  left: Some(ExecutionError(ContractDeploymentError("Livenet execution error")))
 right: Some(ExecutionError(User(UserError { code: 2, message: "InstallRefused" })))

Tests:

  • Core: a from_code round-trip test.
  • livenet-env (no node): two schemas that share a code, unknown codes, and how a failed deploy is classified.
  • examples: reverted_install_returns_the_error passes on both VMs.
  • nctl: the scenario covers a reverting try_deploy and try_upgrade, and the wrong-name case.
  • New schema: CounterV3 gets 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.
error[E0308]: mismatched types
6 |     pub fn set_owner(&mut self, contract: Address) { self.owner.set(contract); }
  |                                 ^^^^^^^^ expected `Address`, found `Vault`

Fields:

  • Module field env: didn't compile. It was also skipped when the fields were numbered.
  • Field bytes: didn't compile in #[odra::odra_type] structs, enum variants and #[odra::event]s.
  • Enum variant field result: compiled, but to_bytes shadowed 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 derives odra::Event (casper-event-standard), whose generated code clashed on bytes. It generates the same CLTyped, ToBytes, FromBytes and EventInstance impls itself.
  • Public interfaces are unchanged: no struct field, method or type name was renamed (env(), address(), …), and neither were entry point names or runtime argument keys (gas, attached_value, contract_name).

Compatibility:

  • On-chain data is unchanged.
    • Serialized bytes of all 138 odra_type and event test cases that compiled before are identical before and after.
    • The storage indices (Module::new field numbering) of all 69 modules are identical.
    • An example test compares an #[odra::event] against #[derive(odra::Event)]: bytes, length, name and schema are equal.
  • Source: only names that failed before now compile.
    • The renamed generated parameters (e.g. execute_init(__odra_env: ContractEnv)) are not part of the API, since Rust passes arguments by position.
    • One technically breaking rename: the hidden module field __env is now __odra_env. It's undocumented (use self.env()), and nothing in odra, the cargo-odra templates or the docs uses it.

Tests:

  • Compile matrices: 12 argument names were tried in every kind of entry point: plain, read-only, payable, non-reentrant, offchain, private, init, upgrade, trait impl, factory and external contract. 37 field names were tried in modules, odra_type structs and enums, and events. Every case compiles natively and as wasm.
  • New example contracts: ArgNames and FieldNames pass on OdraVM and CasperVM.
  • Macro snapshot tests are updated.

6. Events: fields without a concrete CLType are refused at deploy

Casper has no CLType for a user struct or a data-carrying enum, so #[odra::odra_type] gives those CLType::Any. casper-event-standard forbids Any in events, because indexers couldn't decode them. An event with such a field, including one nested in Option, Vec, Map, Result or a tuple, compiled and deployed fine. Every emit then reverted with a bare ExecutionError::Formatting.

trade:  Err(ExecutionError(Formatting))
schema: [("side", U8), ("price", Any)]

Now deploy, upgrade and schema generation (cargo odra schema) panic with:

Contract `Exchange`: event `Traded`: field `price` has no concrete CLType (it is or contains an
#[odra::odra_type] struct or data-carrying enum, whose CLType is `Any`), so emitting it fails with
`Formatting`; casper-event-standard events need plain types: flatten the fields into the event or
use a unit enum
  • Where the check runs: try_deploy_with_cfg and try_upgrade_with_cfg, before anything is deployed. That covers OdraVM, CasperVM, livenet (before a transaction is sent) and odra-cli. It panics rather than returning Err, because a wrongly defined contract is a programming error. Event::validate and assert_events_can_be_emitted are compiled for the host only (cfg(not(target_arch = "wasm32"))), like their callers; a wasm build carries none of it.
  • What's checked: the events come from a new OdraContract::contract_events(), which defaults to the contract's HasEvents::events() (HasEvents is now a supertrait of OdraContract). A factory overrides it to add the child module's events, since the factory wasm carries the child's code. Event::has_any now checks inside containers, as casper-event-standard does.
  • Not checked: HostRefLoader::load, so a contract that's already deployed stays usable.
  • Unchanged: the wasm error code at emit time is still Formatting. In tests, the host also prints the explanation.
  • Behaviour change: a contract that registers such an event but never emits it used to deploy, and now fails. The event could never be emitted anyway. This is noted in the 2.10.0 migration guide.

7. The backend is freed with the last HostEnv (memory leak)

EntryPointsCaller held the HostEnv it 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:

  • The caller is now plain data. EntryPointsCaller keeps only the entry points and the function. The backend passes the ContractEnv in: call(contract_env, call_def).
    • OdraVM keeps a weak reference to itself and builds a ContractEnv for each call.
    • Livenet passes its own env, and uses a weak reference for nested local calls, as CasperVM's offchain path does.
  • Factory children on OdraVM. EntryPointsCallerFn no longer takes a &HostEnv, so building a child's caller needs no env. The VM queues new children, and HostEnv collects them after each deploy, upgrade or call through the new HostContext::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, HostRef or load is 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), and call(call_def) → call(contract_env, call_def). host_env() is removed.
  • entry_points_caller(&env) → entry_points_caller(), and EntryPointsCallerFn = fn() -> EntryPointsCaller.
  • ContractRegister::call and ContractContainer::call take a ContractEnv.
  • OdraVm::default() is removed; use OdraVm::new().

Tests: odra-vm/tests/no_leak.rs keeps a Weak<OdraVm> and drops the env. It covers a plain contract, a factory with a deployed and upgraded child, a snapshot restore, and load. A livenet test does the same with an offline host. All of them failed before this change.

8. XHostRef::new(address, env) works like X::load

X::load(&env, address) registers the contract's code with the env, while the generated XHostRef::new only wrapped the address. On OdraVM and CasperVM that made no difference. Livenet, though, runs getters locally from its contract register. For an address wrapped with new, which is what the factory examples do for children, transactions worked, but every getter failed with a bare VmError::InvalidContractAddress.

What changed:

  • new registers like load. The generated new calls the new HostEnv::register_contract_ref, which does nothing for an address the env already tracks under that name. So load, deploy and upgrade, which call new internally, register only once.
  • Registering again is safe. HostEnv::register_contract no 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::new on 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.
  • Nonexistent addresses are handled by a new HostContext::has_contract, which has a default of true. 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 first new of an address the env doesn't know). So new with a made-up address doesn't make it callable, and on livenet it no longer panics inside new while reading the event counts of a contract that isn't there.
  • External contracts: an #[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 with Contract::load(&env, address) (or ContractHostRef::new(address, env)) … A contract known only by its interface can be called only with transactions." The error is still InvalidContractAddress.
  • Side effect on CasperVM: a factory child reached through new now has its events tracked (last_call()) and its offchain functions work, as after load.

Tests:

  • nctl: a new factory_child_by_address scenario creates a factory child, wraps it with CounterHostRef::new, and calls a getter, a transaction and the getter again (10 → 11). Before the fix the getter failed with InvalidContractAddress. The upgrade and odra-cli factory scenarios were also re-run.
  • Examples: a HostRef made with new sees exactly the right events, for a factory child and for a deployed token. A second new or a load doesn't change that. Without the fix this fails on CasperVM.
  • Core and offline livenet unit tests: register_contract_ref skips 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. gas_report() panicked, and last_call_gas_cost() always returned 0. The client now records the gas each transaction used, taken from the node's execution result: V2 consumed, or V1 cost for V1 results. That's the same unit CasperVM reports. V2 cost isn't used, because it is the charged payment limit, not what the call used.
    • Installs and upgrades are recorded as WasmDeploy { file_name: "Name.wasm" }, and calls, direct or through the proxy, as ContractCall.
    • Failed transactions are recorded too. Transfers update the last-call gas without adding a report entry, as on CasperVM.
    • Getters run locally and send nothing, so they don't change the last-call gas.
    • Each HostEnv::concurrently worker keeps its own report.
  • get_validator_info in a locally executed getter reads the validator's bid (Key::BidAddr(BidAddr::Validator)) from the node. It returns None when the node has no bid for the validator.
  • A named arg read while the current frame is an account returns None (get_named_arg_bytes → MissingArg), as on CasperVM's offchain env.
  • Snapshots still panic on livenet, deliberately: a real chain can't be rolled back.

On nctl:

Gas report:
Wasm deploy: Erc20.wasm - 259.888293669 CSPR
Contract call: transfer - 0.50742048 CSPR

Last call gas: 507420480
Minimum delegation amount: 0

An unknown validator gives None, which the example contract unwraps into ExecutionError(UnwrapError), as the test expects.

Tests:

  • Offline (rpc-client and livenet-env): deploy, call, failed-call and transfer gas bookkeeping; a fresh host with an empty report instead of a panic; a named arg read on an account frame.
  • nctl: gas_is_reported in livenet_tests, and the validator-info checks in validators_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. HostEnv worked around it by resetting its own counter in upgrade_contract:

  • On OdraVM the count never restarts (events are kept per address), so the reset replayed every native event of the contract into the call after the upgrade.
  • A factory child upgraded through upgrade_child_contract got no reset, so its counter went stale and its native events after the upgrade were missing from last_call() until the new version's count overtook the old one.

What changed:

  • CasperVM reads the package's versions and concatenates the messages of every entity, oldest first. Livenet does the same with the events the client recorded, over the entities of every package version (Casper 2.0 and legacy packages). native_events_count, get_native_event and last_call().native_events() now carry on across an upgrade on all three backends, as CES events do.
  • HostEnv::upgrade_contract no longer resets the counter. It also registers a contract the environment never deployed, loaded or wrapped in a host ref, instead of unwrap()ing after the upgrade had gone through.

Tests: native_events_carry_on_across_an_upgrade in examples deploys PartyContract upgradable, emits, upgrades it with the same code and emits again: one native event in last_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, completions and config work without a livenet env

OdraCli::new() created the livenet HostEnv before the builder and before argument parsing. With the ODRA_CASPER_LIVENET_* variables missing:

  • Without a terminal, every invocation exited 1, including --help, completions and config.
  • In a terminal, it prompted for the node address and secret key just to show help.

Before (no ODRA_* vars, no TTY):

$ odra_cli --help
ERROR : Livenet env misconfigured! ODRA_CASPER_LIVENET_NODE_ADDRESS env var is missing.
...
ERROR : No interactive terminal to prompt on. Set the variable(s) above and re-run.
(exit 1)

What changed:

  • Parse first. run() parses the arguments first, so --help (also of subcommands), --version and usage errors are handled by clap.
  • The env is created on first use. It is held in a private OnceCell and 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.
  • completions needs no env.
  • config never 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_address is null in that case and a problem field appears.
  • No public API change.

Tests: odra-cli/tests/offline_commands.rs runs the CLI as a child process with every ODRA_* 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.0 instead of release/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/ourcoin inherits license from the workspace again; the old cargo-odra needed a hard-coded value. The generate-client workaround in the justfile stays, because generate-client refuses single-crate projects cargo-odra#104 is still open and still reproduces with 0.2.0.

  • Benchmark workflow: dawidd6/action-download-artifact goes from v4 to v27. This fixes the Dependabot artifact-poisoning alert. With if_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 fix plus removing the unused http-proxy-middleware devDependency takes npm audit from 10 vulnerabilities (1 critical) to 0.
    • This clears the 13 npm Dependabot alerts.
    • The example still builds (npm run build) and serves its pages.
  • unexpected_cfgs warnings: the macros wrap every item gated on odra_module in #[allow(unexpected_cfgs)] const _: () = { … };, so a crate without odra-build in its build.rs no longer warns unexpected cfg condition name: odra_module. odra-build also declares odra_backend. just clippy no longer needs --allow unexpected_cfgs.

  • Build binaries: the _build_contract/_build_schema binaries of examples, ourcoin, modules and benchmark build on the host, as in the templates, so a plain cargo build --workspace works. cargo odra build produces the same wasm. benchmark/build.rs calls odra_build::build().

  • Factory unused_variables: generated factory functions get #[allow(unused_variables)]. The file-wide allows that hid this in examples, cep95 and cep96 are removed, along with a dead caller local in cep95::approve.

  • odra-cli no longer has a build script. It called odra_build::build() without using the cfg, so it was recompiled for every contract during cargo odra schema (about 7% faster schema generation in examples).

  • 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-schemas regenerates 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/ourcoin and modules on both backends.
  • The template flow (new, test, test -b casper, schema, CLI build, and generate-client for workspace) 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 tests

Swallowed attribute errors. The checks for payable, non_reentrant and offchain used partition_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:

  • "unknown odra attribute paybale, expected one of: payable, non_reentrant, offchain"
  • "duplicate odra attribute payable", also across two #[odra(..)] attributes
  • "odra attribute payable takes no value"
  • "malformed odra attribute …" and "empty odra attribute …"

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 take gas/attached_value. The generated code calls <X as Module>::new fully 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 runner odra/tests/macros.rs: every pass file is built as a crate against odra, every fail file must fail with the diagnostics in its .stderr snapshot (message and span). They run under cargo test in 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 (two fail cases gained the layout and factory = on, events arguments on an impl block and factory = on on a trait to cover everything they did).

  • Must compile: every pass test has #![deny(warnings)], so unexpected_cfgs and unused-variable warnings from generated code also fail it. It covers:
    • all the argument and field names from section 5;
    • private helpers with reserved names.
  • Must fail, with error snapshots:
    • reserved argument and entry-point names;
    • payable/non_reentrant on init/upgrade;
    • offchain misuse, including in factory modules;
    • module arguments on an impl or trait;
    • List as a Mapping key;
    • wrong item shapes;
    • ref_helpers misuse;
    • unknown, duplicate and malformed #[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, while HostRef::contract_address() returns Address and needs HostRef imported.
    • A new ContractAddress trait in odra::prelude gives every ContractRef a contract_address() -> Address.
    • The impls generated by ref_helpers sit in anonymous const blocks that import the providing trait (ContractAddress or HostRef). Helpers need no imports, and nothing changes in scope outside them.
    • Example: Token::own_balance() returns self.balance_of(&self.contract_address()), tested on both VMs and in a UI test.
  • Thread limit for HostEnv::concurrently.
    • concurrently_with(max_threads, items, f) sets the limit in code; 0 is treated as 1.
    • concurrently reads ODRA_MAX_THREADS on 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 u512 sits next to u256. cspr! with a literal works in a const, and that is now documented and tested.
  • odra-cli scenarios can opt in to events. Event capture is off in scenarios (it costs node requests per call on livenet), so last_call() had no events there. A scenario can now override Scenario::captures_events() -> bool, which defaults to false.
  • EntryPointsCaller::has_entry_point(name) replaces the inline entry_points().iter().any(..) checks in the backends.
  • odra::utils::try_serialize(&value) is the fallible counterpart of utils::serialize: the value as Bytes, or the error an entry point returns. The generated entry-point callers return their results through it instead of an inline to_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)
  • Template flow with cargo-odra release/0.2.0 (cep18, full, workspace, cep95)
  • cargo odra test and -b casper in examples (112 passed each) and modules (141 passed each), plus examples/ourcoin
  • Full just test-livenet against nctl (makesoftware/casper-nctl:v203) at the head of this branch: livenet_tests, validators_on_livenet, upgrade_on_livenet and all 11 odra-cli commands pass
  • just clippy (incl. --features=livenet), cargo fmt -- --check
  • cargo check -p odra-core --target wasm32-unknown-unknown
  • After the review fixes (sections 1, 2, 8, 10): cargo test --workspace (682 passed), the examples on CasperVM with the built wasm (113 passed), just clippy, cargo fmt --check, core on wasm32
  • just test-livenet against nctl (makesoftware/casper-nctl:v203) after the review fixes: livenet_tests (factory_child_by_address goes through the new livenet has_contract query), validators_on_livenet, upgrade_on_livenet and all 11 odra-cli commands pass; the native-events scenario reads its counts through the per-version lookup

🤖 Generated with Claude Code

kubaplas and others added 3 commits October 7, 2026 15:48
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>
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.194700503 CSPR (0.44%)

kubaplas and others added 2 commits October 8, 2026 11:28
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>
@kubaplas kubaplas changed the title Afterparty: factory modules on OdraVM, revert rollback, panic hook Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.194700503 CSPR (0.44%)

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>
@kubaplas kubaplas changed the title Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters Afterparty: factory on OdraVM, revert rollback, panic hook, livenet & offchain getters Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.194700503 CSPR (0.44%)

kubaplas and others added 2 commits October 8, 2026 12:55
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>
@kubaplas kubaplas changed the title Afterparty: factory on OdraVM, revert rollback, panic hook, livenet & offchain getters Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters & errors Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.194700503 CSPR (0.44%)

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>
@kubaplas kubaplas changed the title Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters & errors Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters & errors, arg names Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.194700503 CSPR (0.44%)

kubaplas and others added 2 commits October 8, 2026 14:02
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>
@kubaplas kubaplas changed the title Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters & errors, arg names Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters & errors, macro name clashes Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.194700503 CSPR (0.44%)

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>
@kubaplas kubaplas changed the title Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters & errors, macro name clashes Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters & errors, macro names, event fields Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.194700503 CSPR (0.44%)

…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>
@kubaplas kubaplas changed the title Afterparty: factory on OdraVM, revert rollback, panic hook, livenet getters & errors, macro names, event fields Afterparty: factory on OdraVM, revert rollback, panic hook, livenet, macro names, event fields, host leak Oct 9, 2026
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.194700503 CSPR (0.44%)

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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

…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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

kubaplas and others added 2 commits October 15, 2026 11:50
…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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

1 similar comment
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

…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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

kubaplas and others added 2 commits October 16, 2026 13:21
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>
@github-actions

Copy link
Copy Markdown

Benchmark report

Action Details Gas diff
Wasm Deploy Filename: Benchmark.wasm 🔴 +1.309811964 CSPR (0.48%)

@kubaplas
kubaplas merged commit 72f4dc0 into release/2.10.0 Oct 19, 2026
9 checks passed
@zie1ony

zie1ony commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

great

@kubaplas
kubaplas deleted the feature/afterparty branch November 13, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants