diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index d61c7860..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "outputStyle": "Brief Style" -} diff --git a/.genvm-tool.py b/.genvm-tool.py index 7b4ca8ed..2c9a6057 100644 --- a/.genvm-tool.py +++ b/.genvm-tool.py @@ -285,3 +285,8 @@ def collect_parse_version(ctx: genvm_tool.tests.stage.collection.Context): ctx.collect_dir('tests/system/parse_version') ctx.add_collector(collect_parse_version) + + def collect_make_zip(ctx: genvm_tool.tests.stage.collection.Context): + ctx.collect_dir('tests/system/make_zip') + + ctx.add_collector(collect_make_zip) diff --git a/.gitignore b/.gitignore index eab50f70..771520e7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /.pre-commit-config.yaml .claude/hooks +.claude/settings.local.json .idea .envrc *.log diff --git a/crates/modules-interfaces/codegen/data/host-fns.json b/crates/modules-interfaces/codegen/data/host-fns.json index e9d9e0e5..028e1682 100644 --- a/crates/modules-interfaces/codegen/data/host-fns.json +++ b/crates/modules-interfaces/codegen/data/host-fns.json @@ -5,13 +5,13 @@ "name": "methods", "values": { "storage_read": 0, - "consume_fuel": 1, - "eth_call": 2, - "get_balance": 3, - "remaining_fuel_as_gen": 4, + "consume_time_fee_gen_wei": 1, + "external_call": 2, + "get_balance_gen_wei": 3, + "get_remaining_time_fee_gen_wei": 4, "notify_nondet_disagreement": 5, "consume_result": 6, - "resolve_callcontract_executor": 7, + "resolve_call_contract_executor": 7, "run_nested": 8 } }, diff --git a/crates/modules-interfaces/src/abi_stub.rs b/crates/modules-interfaces/src/abi_stub.rs index c0dd0892..6f5010af 100644 --- a/crates/modules-interfaces/src/abi_stub.rs +++ b/crates/modules-interfaces/src/abi_stub.rs @@ -15,9 +15,9 @@ pub enum On { #[serde(rename = "finalized")] #[calldata(rename = "finalized")] Finalized, - #[serde(rename = "accepted")] - #[calldata(rename = "accepted")] - Accepted, + #[serde(rename = "decided")] + #[calldata(rename = "decided")] + Decided, } #[derive( @@ -57,21 +57,21 @@ pub struct MessageData { pub value: num_bigint::BigInt, pub is_init: bool, /// Transaction timestamp - #[serde(default = "default_datetime")] + #[serde(default = "default_transaction_timestamp")] #[calldata( - serialize_with = encode_datetime_rfc3339, - deserialize_with = decode_datetime_rfc3339 + serialize_with = encode_transaction_timestamp_rfc3339, + deserialize_with = decode_transaction_timestamp_rfc3339 )] - #[calldata(default = default_datetime)] - pub datetime: chrono::DateTime, + #[calldata(default = default_transaction_timestamp)] + pub transaction_timestamp: chrono::DateTime, } -fn decode_datetime_rfc3339( +fn decode_transaction_timestamp_rfc3339( val: genlayer_calldata::Value, ) -> Result, genlayer_calldata::codec::DecodeError> { let genlayer_calldata::Value::Str(s) = val else { return Err(genlayer_calldata::codec::DecodeError::Unexpected( - "expected string for datetime", + "expected string for transaction_timestamp", )); }; chrono::DateTime::parse_from_rfc3339(&s) @@ -79,7 +79,7 @@ fn decode_datetime_rfc3339( .map_err(|e| genlayer_calldata::codec::DecodeError::UserError(Box::new(e))) } -fn encode_datetime_rfc3339( +fn encode_transaction_timestamp_rfc3339( dt: &chrono::DateTime, enc: &mut genlayer_calldata::Encoder, ) -> Result<(), W::Error> { @@ -88,7 +88,7 @@ fn encode_datetime_rfc3339( enc.push_str(&s) } -fn default_datetime() -> chrono::DateTime { +fn default_transaction_timestamp() -> chrono::DateTime { chrono::DateTime::parse_from_rfc3339("2024-11-26T06:42:42.424242Z") .unwrap() .to_utc() diff --git a/crates/modules-interfaces/src/domain.rs b/crates/modules-interfaces/src/domain.rs index ca16e8fd..94b47423 100644 --- a/crates/modules-interfaces/src/domain.rs +++ b/crates/modules-interfaces/src/domain.rs @@ -91,7 +91,7 @@ pub struct NestedExecutionData { #[derive(Debug, Clone, PartialEq, Eq, genlayer_calldata::Encode, genlayer_calldata::Decode)] #[calldata(tag = "type")] pub enum ExecutionEmission { - EthSend { + ExternalMessage { address: genlayer_calldata::Address, calldata: Bytes, value: U256, @@ -100,7 +100,7 @@ pub enum ExecutionEmission { fee_params: fees::ExternalMessageParams, }, - PostMessage { + InternalMessage { call_key: crate::CallKey, address: genlayer_calldata::Address, calldata: genlayer_calldata::codec::Maybe, @@ -115,7 +115,7 @@ pub enum ExecutionEmission { /// balance rather than the sender's prefunded message-fee pool. use_balance: bool, }, - DeployContract { + InternalDeployMessage { calldata: genlayer_calldata::codec::Maybe, code: Bytes, value: U256, @@ -126,10 +126,10 @@ pub enum ExecutionEmission { fee_params: fees::InternalMessageParams, subtree: bytes::Bytes, - /// Chain `useBalance`; see `ExecutionEmission::PostMessage::use_balance`. + /// Chain `useBalance`; see `ExecutionEmission::InternalMessage::use_balance`. use_balance: bool, }, - EmitEvent { + Event { topics: Vec, blob: genlayer_calldata::codec::Maybe>, storage_fee: U256, @@ -403,7 +403,7 @@ pub struct ReportedResult { pub data: genlayer_calldata::unparsed::Maybe, pub backtrace: Option, pub wasm_store_hashes: WasmStoreHashes, - pub storage_changes: Vec, + pub storage_deltas: Vec, pub emissions: Vec, @@ -413,5 +413,5 @@ pub struct ReportedResult { pub data_fees_remaining: Vec, pub data_fees_consumed: BucketsConsumed, - pub llm_consumption: primitive_types::U256, + pub llm_consumed_gen_wei: primitive_types::U256, } diff --git a/crates/modules-interfaces/src/domain/fees/abi.rs b/crates/modules-interfaces/src/domain/fees/abi.rs index 93f293e7..89d938c8 100644 --- a/crates/modules-interfaces/src/domain/fees/abi.rs +++ b/crates/modules-interfaces/src/domain/fees/abi.rs @@ -109,7 +109,7 @@ fn encode_node(node: &MessageAllocationNode, parent_index: U256) -> Vec { let (message_type, on_acceptance, fee_params) = match &node.fee_params { MessageAllocationNodeParams::Internal(params) => ( MESSAGE_TYPE_INTERNAL, - matches!(node.on, On::Accepted), + matches!(node.on, On::Decided), encode_internal_params(params), ), // External messages have no acceptance/finalize lifecycle. diff --git a/crates/modules-interfaces/src/host_fns.rs b/crates/modules-interfaces/src/host_fns.rs index ae6be202..91121027 100644 --- a/crates/modules-interfaces/src/host_fns.rs +++ b/crates/modules-interfaces/src/host_fns.rs @@ -17,13 +17,13 @@ use serde::{Deserialize, Serialize}; #[repr(u8)] pub enum Methods { StorageRead = 0, - ConsumeFuel = 1, - EthCall = 2, - GetBalance = 3, - RemainingFuelAsGen = 4, + ConsumeTimeFeeGenWei = 1, + ExternalCall = 2, + GetBalanceGenWei = 3, + GetRemainingTimeFeeGenWei = 4, NotifyNondetDisagreement = 5, ConsumeResult = 6, - ResolveCallcontractExecutor = 7, + ResolveCallContractExecutor = 7, RunNested = 8, } @@ -32,26 +32,26 @@ impl Methods { pub fn value(self) -> u8 { match self { Methods::StorageRead => 0, - Methods::ConsumeFuel => 1, - Methods::EthCall => 2, - Methods::GetBalance => 3, - Methods::RemainingFuelAsGen => 4, + Methods::ConsumeTimeFeeGenWei => 1, + Methods::ExternalCall => 2, + Methods::GetBalanceGenWei => 3, + Methods::GetRemainingTimeFeeGenWei => 4, Methods::NotifyNondetDisagreement => 5, Methods::ConsumeResult => 6, - Methods::ResolveCallcontractExecutor => 7, + Methods::ResolveCallContractExecutor => 7, Methods::RunNested => 8, } } pub fn str_snake_case(self) -> &'static str { match self { Methods::StorageRead => "storage_read", - Methods::ConsumeFuel => "consume_fuel", - Methods::EthCall => "eth_call", - Methods::GetBalance => "get_balance", - Methods::RemainingFuelAsGen => "remaining_fuel_as_gen", + Methods::ConsumeTimeFeeGenWei => "consume_time_fee_gen_wei", + Methods::ExternalCall => "external_call", + Methods::GetBalanceGenWei => "get_balance_gen_wei", + Methods::GetRemainingTimeFeeGenWei => "get_remaining_time_fee_gen_wei", Methods::NotifyNondetDisagreement => "notify_nondet_disagreement", Methods::ConsumeResult => "consume_result", - Methods::ResolveCallcontractExecutor => "resolve_callcontract_executor", + Methods::ResolveCallContractExecutor => "resolve_call_contract_executor", Methods::RunNested => "run_nested", } } @@ -63,13 +63,13 @@ impl TryFrom for Methods { fn try_from(value: u8) -> Result { match value { 0 => Ok(Methods::StorageRead), - 1 => Ok(Methods::ConsumeFuel), - 2 => Ok(Methods::EthCall), - 3 => Ok(Methods::GetBalance), - 4 => Ok(Methods::RemainingFuelAsGen), + 1 => Ok(Methods::ConsumeTimeFeeGenWei), + 2 => Ok(Methods::ExternalCall), + 3 => Ok(Methods::GetBalanceGenWei), + 4 => Ok(Methods::GetRemainingTimeFeeGenWei), 5 => Ok(Methods::NotifyNondetDisagreement), 6 => Ok(Methods::ConsumeResult), - 7 => Ok(Methods::ResolveCallcontractExecutor), + 7 => Ok(Methods::ResolveCallContractExecutor), 8 => Ok(Methods::RunNested), _ => Err(()), } diff --git a/crates/modules-interfaces/src/lib.rs b/crates/modules-interfaces/src/lib.rs index 9217ad97..d4b364c0 100644 --- a/crates/modules-interfaces/src/lib.rs +++ b/crates/modules-interfaces/src/lib.rs @@ -537,11 +537,11 @@ pub mod llm { pub enum Message { Prompt { payload: PromptPayload, - remaining_fuel_as_gen: primitive_types::U256, + remaining_time_fee_gen_wei: primitive_types::U256, }, PromptTemplate { payload: PromptTemplatePayload, - remaining_fuel_as_gen: primitive_types::U256, + remaining_time_fee_gen_wei: primitive_types::U256, }, } diff --git a/crates/modules-interfaces/src/nested.rs b/crates/modules-interfaces/src/nested.rs index 94577089..0c518ff2 100644 --- a/crates/modules-interfaces/src/nested.rs +++ b/crates/modules-interfaces/src/nested.rs @@ -72,8 +72,8 @@ pub enum ExecutorSelector { #[serde(rename_all = "snake_case")] pub enum NestedStorageType { Default, - LatestFinal, - LatestNonFinal, + LatestFinalized, + LatestDecided, } /// Permission bits carried across an executor boundary. diff --git a/docs/adr/011. runner ids and runtime registration.md b/docs/adr/011. runner ids and runtime registration.md index 1d7bb3b2..2c7bf4ae 100644 --- a/docs/adr/011. runner ids and runtime registration.md +++ b/docs/adr/011. runner ids and runtime registration.md @@ -55,6 +55,12 @@ returns the resulting runner id. It is allowed only in deterministic mode and on holds the new `register_runners` permission (permission char `u`). A friendly `genlayer.vm.register_runner(code) -> str` wrapper is exposed in the Python SDK. +> **Superseded.** The `register_runners` permission was removed from v0.3: no +> `u` char was ever parsed, the root VM always held the bit, and containment +> (a `custom:` id resolves only from the registering VM's loaded set, which dies +> with it) already carries what the permission was meant to buy. +> `RegisterRunner` now requires deterministic mode alone. + Add a companion `gl_call` `MapFile { runner, path_in_runner, path_in_vfs }` that maps a file (or, when `path_in_runner` ends with `/`, a directory subtree) from any runner into the VM filesystem at runtime, sharing the exact logic of the `MapFile` diff --git a/docs/adr/013. pre-validating untrusted decoded inputs.md b/docs/adr/013. pre-validating untrusted decoded inputs.md index af14d049..532f0d21 100644 --- a/docs/adr/013. pre-validating untrusted decoded inputs.md +++ b/docs/adr/013. pre-validating untrusted decoded inputs.md @@ -130,10 +130,10 @@ bucket that raises it needs a permission the child lacks); the child also can't reach the derived namespace (it has `spawn_nondet: false`). Both invariants must be re-checked if the nondet child's permissions change — a non-trie code trips the `debug_assert!` in debug and, in release, is an honest-node hash split. -Consequence: `leaders_result` is always detail-free while a validator's own +Consequence: `leader_result` is always detail-free while a validator's own error keeps its detail; the default `compare_vm_errors` compares `public_code` only, so it is unaffected — custom comparators must not read a detail from -`leaders_result`. +`leader_result`. ### Rule 2 — message payload shape @@ -150,12 +150,13 @@ untrusted entry calldata is decoded: - **Top-level host entry** — decoded in `run_with_impl` before permissions are read or any runner loads; failure (or a `""` present on `is_init`) is a - `malformed_entry` VM error **result**. `PostMessage`/`DeployContract` emit - messages that later execute as top-level entries, so they pass here too. -- **Every `gl_call` message** — `CallContract`, `PostMessage` and - `DeployContract` carry the calldata as a typed field, so a malformed payload - fails the `gl_call::Message` decode and answers **`Errno::Inval`**, like the - other argument checks; the caller can recover. + `malformed_entry` VM error **result**. + `EmitInternalMessage`/`EmitInternalDeployMessage` emit messages that later + execute as top-level entries, so they pass here too. +- **Every `gl_call` message** — `CallContract`, `EmitInternalMessage` and + `EmitInternalDeployMessage` carry the calldata as a typed field, so a + malformed payload fails the `gl_call::Message` decode and answers + **`Errno::Inval`**, like the other argument checks; the caller can recover. ### Run modes @@ -218,7 +219,7 @@ calldata, extra keys or observable nondet details. - **Re-encode the `Return` payload instead of rejecting** — silently normalizes malformed consensus input; the validator's bytes would differ from the proposal while claiming agreement. -- **Validate lazily when the contract reads `leaders_result`** — too late, the +- **Validate lazily when the contract reads `leader_result`** — too late, the payload is already in the hash. - **Enforce the message shape in each SDK** — once per language, differently, and a runner can't reject a call before it starts. diff --git a/docs/adr/015. cross-major contract calls.md b/docs/adr/015. cross-major contract calls.md index 08c75fc1..d1ed78ca 100644 --- a/docs/adr/015. cross-major contract calls.md +++ b/docs/adr/015. cross-major contract calls.md @@ -63,7 +63,7 @@ observed mismatch, asks the host which executor should run the callee. The resolution and the execution are separate calls, to different peers, because they answer to different authorities. -1. **`resolve_callcontract_executor`**, on host connection 0 — the host that +1. **`resolve_call_contract_executor`**, on host connection 0 — the host that initiated the transaction — when the run request sets `hook_cross_contract_calls`. Otherwise it goes to the manager on host 1, which answers null; see the implementation notes. @@ -135,7 +135,7 @@ computed by the caller and carried on the wire. The table is complete against | `ExecutionData` field | Nested value | | --- | --- | | `calldata` | Derived `CallContract` calldata. The child is always `Main`; v0.2 rewrites its legacy `method` key to the current empty key at the boundary. | -| `message` | Derived child message: target contract, inherited sender/origin/signer, chain and datetime, `value = 0`, `is_init = false`. v0.2's contract-facing message has no signer field, so its executor carries the signer beside the message and forwards it unchanged; contracts on that line still cannot observe it. | +| `message` | Derived child message: target contract, inherited sender/origin/signer, chain and `transaction_timestamp`, `value = 0`, `is_init = false`. Each executor maps `transaction_timestamp` back to the contract-facing `datetime`; v0.2's contract-facing message has no signer field, so its executor carries the signer beside the message and forwards it unchanged; contracts on that line still cannot observe it. | | `code` | `None`; the child reads the target code through host 0 storage. | | `host_data`, `gas_data` | Copied by the manager from the outer request, not carried in the envelope. | | `method_hosts` | Constructed by the manager: host 0 by default, `consume_result` and `run_nested` on host 1. | @@ -286,7 +286,7 @@ Infrastructure faults are internal errors; contract-produced outcomes stay contract-visible. The dividing line is whether a contract can reach the condition. -Internal: a host that cannot answer `resolve_callcontract_executor` (the host is +Internal: a host that cannot answer `resolve_call_contract_executor` (the host is the authority on which line owns an address); a nested callee reporting an internal error, which propagates unchanged; a callee reporting any effect, which the manager refuses outright since a `CallContract` child holds none of the @@ -355,7 +355,7 @@ determinism hazard named above. updates Rust, Python and RST — see [genvm-tool.md](../contributing/howto/genvm-tool.md). - **Resolution is opt-in.** A run request carries `hook_cross_contract_calls`, - default false. False routes `resolve_callcontract_executor` to host 1, where + default false. False routes `resolve_call_contract_executor` to host 1, where the manager answers null; true routes it to host 0 so the node decides. The default is "manager answers null" because a node that does not route across majors should pay neither the round-trip nor the cost of implementing a host diff --git a/docs/contributing/howto/committing/runners.md b/docs/contributing/howto/committing/runners.md index 55a298ab..927cece0 100644 --- a/docs/contributing/howto/committing/runners.md +++ b/docs/contributing/howto/committing/runners.md @@ -12,14 +12,16 @@ Before committing: 1. Set `dev-mode.nix` back to `false` 2. Set every `"test"` hash in `current.nix` to `null` -3. Run `runners/support/versions/hash-updater.py`, from anywhere inside the - repo. It builds the umbrella's `#runners-all` with `--keep-going`, writes - every reported `got:` value back into `current.nix`, and repeats until a - fixed point. Hashes are content-addressed and depend on resolved dependency - uids, so they must be discovered bottom-up, which the script handles. It - needs the executor nested under the manager umbrella; a standalone checkout - cannot build `runners-all` -4. Commit once every hash is a real `sha256-…` value +3. Stage the runner changes and run pre-commit; fix and restage them before + hash discovery so the hooks and Nix inspect the same source tree +4. Run `runners/support/versions/hash-updater.py`, from anywhere inside the + repo. It builds the umbrella's `#runners-all` with `--keep-going`, writes + every reported `got:` value back into `current.nix`, and repeats until a + fixed point. Hashes are content-addressed and depend on resolved dependency + uids, so they must be discovered bottom-up, which the script handles. It + needs the executor nested under the manager umbrella; a standalone checkout + cannot build `runners-all` +5. Stage `current.nix` and commit once every hash is a real `sha256-…` value **Never restore an old hash by hand.** It becomes wrong the moment any source or dependency changes, and only `hash-updater.py` produces a correct one diff --git a/docs/contributing/howto/committing/submodules.md b/docs/contributing/howto/committing/submodules.md index fb364d77..8a89f141 100644 --- a/docs/contributing/howto/committing/submodules.md +++ b/docs/contributing/howto/committing/submodules.md @@ -79,7 +79,7 @@ are normally ahead-only. Runner hash hygiene first: [runners.md](runners.md) ## Why There Are Two Runner Trees -Forward-rolling lines share the top-level `runners///.tar` tree, -named by Crockford base32 of `sha256(tar)`; frozen v0.2.x keeps -`executor//legacy-runners/` with Nix base32 hashes. `flake.nix` splits +Forward-rolling lines share the top-level `runners///.zip` tree, +named by Crockford base32 of `sha256(zip)`; frozen v0.2.x keeps +`executor//legacy-runners/` as ustar tars with Nix base32 hashes. `flake.nix` splits the runner list per line, and `genvm check` verifies each with its own scheme diff --git a/docs/contributing/howto/extending/modify-runner.md b/docs/contributing/howto/extending/modify-runner.md index da7f39f5..25ee5ba5 100644 --- a/docs/contributing/howto/extending/modify-runner.md +++ b/docs/contributing/howto/extending/modify-runner.md @@ -3,13 +3,15 @@ Paths are relative to `executors/v0.3.x/`, the line that owns runner sources 1. Set `runners/support/versions/dev-mode.nix` to `true` and the runner's hash - in `runners/support/versions/current.nix` to `"test"`, then develop and test. - Hashes move, so integration tests need `--ignore-hash` - ([integration.md](../testing/integration.md)) + in `runners/support/versions/current.nix` to `"test"`, then develop and test + Hashes move, so integration tests need `--ignore-hash` + ([integration.md](../testing/integration.md)) 2. Set dev-mode back to `false`, and the changed runner's hash, plus its - dependents', to `null` -3. Refresh the hashes with `runners/support/versions/hash-updater.py` -4. Rebuild to confirm no mismatch remains + dependents', to `null` +3. Stage the runner changes and run pre-commit; fix and restage them before + hash discovery so the hooks and Nix inspect the same source tree +4. Refresh the hashes with `runners/support/versions/hash-updater.py` +5. Stage `current.nix` and rebuild to confirm no mismatch remains -Step 3 in detail, and why a hash is never restored by hand: +Step 4 in detail, and why a hash is never restored by hand: [committing/runners.md](../committing/runners.md) diff --git a/docs/contributing/howto/testing/fuzzing.md b/docs/contributing/howto/testing/fuzzing.md index 621697e6..2ef8e999 100644 --- a/docs/contributing/howto/testing/fuzzing.md +++ b/docs/contributing/howto/testing/fuzzing.md @@ -53,6 +53,12 @@ named as a corpus entry) as soon as it is reported, so every later run replays it — the next run wipes the crash dir it was saved in. Commit it along with the fix; put it in `-curated` instead if a corpus update must never drop it +Startup replays every committed entry, and AFL skips the ones it cannot use +rather than aborting the run: a `-t` ceiling of 5 s (it auto-scales below that) +keeps a seed that grew slower than AFL's 1 s default from taking the target down. +A seed that *crashes* is skipped by the same switch, so the case reads it back out +of the instance logs and fails with the entry's name under `crashing_seeds` + Findings of both languages live under `build/test-artifacts/fuzz//`. Reruns resume from that dir (`AFL_AUTORESUME`) and therefore ignore `-i`: remove it to pick up a corpus that changed. Corpus updates `cmin` the fleet's queues diff --git a/docs/contributing/howto/testing/integration.md b/docs/contributing/howto/testing/integration.md index 4c8c3b65..cdff315f 100644 --- a/docs/contributing/howto/testing/integration.md +++ b/docs/contributing/howto/testing/integration.md @@ -11,7 +11,7 @@ runs its own cases against its own built executor, via `build/info.json` A step may declare `executor_routes: {'
': }` to send a `CallContract` on that address to another executor line instead of running it -in-process — the mock host answers `resolve_callcontract_executor` with that +in-process — the mock host answers `resolve_call_contract_executor` with that route, and the manager spawns the nested run. A route is a major (an integer, resolved by the manifest's rules) or a version string naming the line outright, `re:`-prefixed to match manifest keys rather than name a directory. Prefer a diff --git a/docs/schemas/default-config.json b/docs/schemas/default-config.json index 692e39fe..ccce9e0c 100644 --- a/docs/schemas/default-config.json +++ b/docs/schemas/default-config.json @@ -244,7 +244,7 @@ }, "max_wait_after_loaded": { "$ref": "#/definitions/timeout", - "description": "upper bound for `wait_after_loaded` requested by contracts; larger values are clamped (with a warning). Defaults to 60s" + "description": "upper bound for `post_load_wait` requested by contracts; larger values are clamped (with a warning). Defaults to 60s" }, "meta": { "type": ["object", "null"], diff --git a/docs/schemas/runners.json b/docs/schemas/runners.json index 22a543f0..dcb452da 100644 --- a/docs/schemas/runners.json +++ b/docs/schemas/runners.json @@ -9,7 +9,7 @@ "definitions": { "WasmMode": { - "enum": ["det", "nondet"] + "enum": ["det", "!det"] }, "runner-id": { "type": "string", diff --git a/docs/website/src/impl-spec/01-core-architecture/04-executor.rst b/docs/website/src/impl-spec/01-core-architecture/04-executor.rst index a5fb2519..0fa4cf6a 100644 --- a/docs/website/src/impl-spec/01-core-architecture/04-executor.rst +++ b/docs/website/src/impl-spec/01-core-architecture/04-executor.rst @@ -73,7 +73,7 @@ The levels are ordered; each *adds* to the previous one: - The ``:latest`` / ``:test`` runner ids may be resolved. **Unsafe across machines**: different nodes may resolve different code and diverge consensus, though a single node stays deterministic. * - ``unsafe-tracing`` - ``unbounded`` - - Real wall-clock time is exposed to the contract in deterministic mode (``RuntimeMicroSec`` returns actual elapsed time instead of ``0``; non-deterministic mode already returns real time regardless of debug level). **Can break determinism on a single machine.** Local debugging only. + - Real wall-clock time is exposed to the contract in deterministic mode (``RuntimeMicroseconds`` returns actual elapsed time instead of ``0``; non-deterministic mode already returns real time regardless of debug level). **Can break determinism on a single machine.** Local debugging only. Only ``unsafe`` and ``unsafe-tracing`` can affect determinism (across machines and on a single machine respectively); ``safe`` and ``safe-unbounded`` are fully diff --git a/docs/website/src/impl-spec/02-vm/02-version-management.rst b/docs/website/src/impl-spec/02-vm/02-version-management.rst index 06ce487d..b426907e 100644 --- a/docs/website/src/impl-spec/02-vm/02-version-management.rst +++ b/docs/website/src/impl-spec/02-vm/02-version-management.rst @@ -18,8 +18,9 @@ There are two independent notions of "version" in play: Detection from contract bytes ----------------------------- -``executor/src/runners/parse.rs::detect_version_from_wasm`` walks the WASM custom sections -and returns the contents of the ``genvm.version`` section as the runner version string. +``executor/src/runners/parse.rs::describe_wasm`` walks the WASM custom sections in one +pass and returns the contents of the ``genvm.version`` section as the runner version +string, alongside the optional ``genvm.runner.json`` section. When the contract is not a raw WASM but a zip-packaged archive the version is read from the ``version`` file in the archive. For single-file text contracts (Python source) the first comment line is inspected: if it starts with ``v`` it is taken as the version diff --git a/docs/website/src/impl-spec/04-fees.rst b/docs/website/src/impl-spec/04-fees.rst index 6b07f959..0aa0d5a8 100644 --- a/docs/website/src/impl-spec/04-fees.rst +++ b/docs/website/src/impl-spec/04-fees.rst @@ -259,10 +259,11 @@ Message-Fee Allocation Matching The ``a.matchedFeeParams`` above is selected per outbound message from the call's allocation list (``accumulator.message_fee_allocation``). A node (``domain/fees.rs``, ``matches_internal`` / ``matches_external``) matches on kind -(``External`` for ``EthSend``, else ``Internal``), ``on`` (internal only), and +(``External`` for ``EmitExternalMessage``, else ``Internal``), ``on`` (internal +only), and ``recipient`` / ``call_key`` (each a wildcard ``None`` or an exact match). Selection is **first-match-wins** in list order (``find_map``); no match yields the -``fee no_matching_node`` VM error. +``fee no_matching_allocation`` VM error. Wildcards are not reordered, so a wildcard node shadows every more-specific node after it: producers are advised to sort more-specific nodes ahead of wildcard ones. @@ -275,13 +276,14 @@ An outgoing internal message is funded one of two ways: - **Allocation-matched (default).** The fee is matched against the allocation tree as above, capped by the matched node's ``budget``, and consumes both the ``message_fee`` and ``message_receipt`` buckets. -- **Balance-funded (``use_balance``).** When a ``PostMessage`` / ``DeployContract`` +- **Balance-funded (``use_balance``).** When an ``EmitInternalMessage`` / + ``EmitInternalDeployMessage`` sets ``use_balance`` (the chain's ``useBalance``, gated on :ref:`gvm-perm-use-balance-for-message-fees`), allocation matching is skipped entirely. The fee is metered from the guest-supplied ``fee_params`` and that metered amount is the child's ``declaredBudget``, reserved from the emitting contract's balance (jointly with ``value``; insufficient balance yields - ``Inbalance``). The ``message_fee`` bucket is **not** consumed (the message is + ``InsufficientBalance``). The ``message_fee`` bucket is **not** consumed (the message is excluded from the sender pool on-chain); only ``message_receipt`` is. The emitted allocation subtree is empty, so nested child messages must each fund themselves. diff --git a/docs/website/src/impl-spec/appendix/host-loop.rst b/docs/website/src/impl-spec/appendix/host-loop.rst index d79258aa..54c6ef2a 100644 --- a/docs/website/src/impl-spec/appendix/host-loop.rst +++ b/docs/website/src/impl-spec/appendix/host-loop.rst @@ -35,9 +35,9 @@ independently runs the protocol described below. In the manager deployment, host 0 is the node's host loop and host 1 is a socketpair to the manager. The manager serves ``consume_result``, ``run_nested`` -and -- by default -- ``resolve_callcontract_executor`` on host 1. +and -- by default -- ``resolve_call_contract_executor`` on host 1. -``resolve_callcontract_executor`` moves to host 0 only when the run request sets +``resolve_call_contract_executor`` moves to host 0 only when the run request sets ``hook_cross_contract_calls`` (see :doc:`manager-socket`). Otherwise the manager answers it with a null reply, which keeps every ``CallContract`` in-process. A host that does not route calls across major boundaries therefore need not @@ -84,12 +84,12 @@ result, clean-finish vs crash) is reported by the manager's terminal event method_id := read_byte match method_id json/methods/storage_read: - read_type := read_byte as json/storage_type + read_type := read_byte as json/storage_view address := read_bytes(ACCOUNT_ADDR_SIZE) slot := read_bytes(SLOT_ID_SIZE) - index := read_u32_le + offset := read_u32_le len := read_u32_le - data, err := host_storage_read(read_type, address, slot, index, len) + data, err := host_storage_read(read_type, address, slot, offset, len) if err != json/errors/ok: write_byte err else: @@ -101,35 +101,35 @@ result, clean-finish vs crash) is reported by the manager's terminal event # this is needed to ensure that genvm doesn't close socket before all data is read write_byte 0x00 - json/methods/consume_fuel: - gas := read_u64_le - host_consume_fuel(gas) + json/methods/consume_time_fee_gen_wei: + time_fee_gen_wei := read_u256_le + host_consume_time_fee_gen_wei(time_fee_gen_wei) # note: this method doesn't send any response - json/methods/eth_call: + json/methods/external_call: address := read_bytes(ACCOUNT_ADDR_SIZE) calldata := read_slice() - result, err := host_eth_call(address, calldata) + result, err := host_external_call(address, calldata) if err != json/errors/ok: write_byte err else: write_byte json/errors/ok write_byte_slice result - json/methods/get_balance: + json/methods/get_balance_gen_wei: address := read_bytes(ACCOUNT_ADDR_SIZE) - balance, err := host_get_balance(address) + balance, err := host_get_balance_gen_wei(address) if err != json/errors/ok: write_byte err else: write_byte json/errors/ok write_bytes balance.to_le_bytes(32) # 256-bit integer - json/methods/resolve_callcontract_executor: + json/methods/resolve_call_contract_executor: address := read_bytes(ACCOUNT_ADDR_SIZE) - state := read_byte as json/storage_type + state := read_byte as json/storage_view advisory_major := read_byte - payload, err := host_resolve_callcontract_executor( + payload, err := host_resolve_call_contract_executor( address, state, advisory_major) if err != json/errors/ok: write_byte err @@ -158,13 +158,13 @@ result, clean-finish vs crash) is reported by the manager's terminal event } write_byte_slice calldata_encode(reply) - json/methods/remaining_fuel_as_gen: - fuel, err := host_remaining_fuel_as_gen() + json/methods/get_remaining_time_fee_gen_wei: + time_fee_gen_wei, err := host_get_remaining_time_fee_gen_wei() if err != json/errors/ok: write_byte err else: write_byte json/errors/ok - write_bytes fuel.to_le_bytes(32) # 256-bit unsigned, little-endian, always 32 bytes + write_bytes time_fee_gen_wei.to_le_bytes(32) # 256-bit unsigned, little-endian, always 32 bytes json/methods/notify_nondet_disagreement: call_no := read_u32_le diff --git a/docs/website/src/impl-spec/appendix/manager-api.yaml b/docs/website/src/impl-spec/appendix/manager-api.yaml index e2a25f08..aa0ee8c9 100644 --- a/docs/website/src/impl-spec/appendix/manager-api.yaml +++ b/docs/website/src/impl-spec/appendix/manager-api.yaml @@ -760,7 +760,7 @@ components: is_init: type: boolean description: Whether this is a contract initialization call - datetime: + transaction_timestamp: type: string format: date-time description: Transaction timestamp @@ -920,7 +920,7 @@ components: type: boolean default: false description: | - Whether the host wants to answer `resolve_callcontract_executor`. When + Whether the host wants to answer `resolve_call_contract_executor`. When false the manager answers it with a null reply and every `CallContract` stays in-process. unsafe_overrides: diff --git a/docs/website/src/impl-spec/appendix/manager-socket.rst b/docs/website/src/impl-spec/appendix/manager-socket.rst index 5f22661a..60a759c1 100644 --- a/docs/website/src/impl-spec/appendix/manager-socket.rst +++ b/docs/website/src/impl-spec/appendix/manager-socket.rst @@ -103,7 +103,7 @@ Request payload: the same logical structure as the deprecated (currently index 1, the ``consume_result`` socketpair). - ``hook_cross_contract_calls`` (bool, optional, default ``false``) -- whether this host wants to be asked where a ``CallContract`` runs. When false the - manager answers ``resolve_callcontract_executor`` itself with a null reply, + manager answers ``resolve_call_contract_executor`` itself with a null reply, so every call stays in-process and the host need not implement that method. When true the question is routed to host 0 and the host may send the caller across a major boundary (see :doc:`host-loop`). A nested run inherits the diff --git a/docs/website/src/spec/02-execution-environment/02-wasip1.rst b/docs/website/src/spec/02-execution-environment/02-wasip1.rst index 51e2bdf4..8ef9f26a 100644 --- a/docs/website/src/spec/02-execution-environment/02-wasip1.rst +++ b/docs/website/src/spec/02-execution-environment/02-wasip1.rst @@ -181,36 +181,47 @@ File Metadata The :ref:`gvm-def-vfs` stores no metadata, so every ``Filestat`` reports ``dev``, ``ino``, ``atim``, ``mtim`` and ``ctim`` as ``0``; ``size`` is the -file's length in octets and ``0`` for a directory. Timestamps are **not** -derived from the transaction timestamp — a file has no modification time at -all. +file's length in octets and ``0`` for a directory, and ``nlink`` is ``1``. +Timestamps are **not** derived from the transaction timestamp — a file has no +modification time at all. + +Operations represented in a target type's supported base mask require the +corresponding right on a descriptor returned by ``path_open`` and fail with +``Notcapable`` when it is absent. Supported masks are defined by +:ref:`gvm-def-wasi-descriptor-rights`. ``path_open`` Function ~~~~~~~~~~~~~~~~~~~~~~ #. The path is resolved as in `Path Resolution`_. -#. ``oflags``, ``fs_rights_base``, ``fs_rights_inheriting`` and ``fdflags`` are - ignored. The :ref:`gvm-def-vfs` is read-only, so ``creat``, ``excl`` and +#. The directory descriptor requires the ``path_open`` base right, otherwise + the call fails with ``Notcapable``. +#. If ``oflags::directory`` targets a regular file, the call fails with + ``Notdir``. The :ref:`gvm-def-vfs` is read-only, so ``creat``, ``excl`` and ``trunc`` never take effect — opening a non-existent path fails with - ``Noent`` rather than creating it — and ``directory`` does not restrict the - result. + ``Noent`` rather than creating it. ``fdflags`` are ignored. +#. The new descriptor's base and inheriting rights are limited by the + corresponding requested rights and the parent descriptor's inheriting + rights. Unsupported target-type rights are then removed as defined in + :ref:`gvm-def-wasi-descriptor-rights`. #. On success a fresh descriptor is allocated (see :ref:`gvm-def-fd-allocation`) that refers to the resolved file or directory. A file descriptor starts at offset ``0``. -#. The descriptor is released again if the call fails, so a failed open leaks - neither a descriptor nor its :ref:`gvm-def-ram-consumption`. +#. A failed call allocates no descriptor and consumes no descriptor + :ref:`gvm-def-ram-consumption`. ``path_filestat_get`` Function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Resolves the path as in `Path Resolution`_ and returns the ``Filestat`` described in `File Metadata`_ with ``filetype`` ``regular_file`` or -``directory`` and ``nlink`` ``0``. +``directory``. ``fd_readdir`` Function ~~~~~~~~~~~~~~~~~~~~~~~ -A descriptor that is not a directory fails with ``Badf``. +After the rights check, a descriptor that is not a directory fails with +``Badf``. The entry sequence of a directory is fixed and does not depend on how the :ref:`gvm-def-vfs` was populated: @@ -241,12 +252,12 @@ Returns the descriptor's current offset. ``stdout``/``stderr`` fail with ``fd_datasync`` Function ~~~~~~~~~~~~~~~~~~~~~~~~ -Does nothing and always returns success. +Does nothing and returns success after the rights check. ``fd_sync`` Function ~~~~~~~~~~~~~~~~~~~~ -Does nothing and always returns success. +Does nothing and returns success after the rights check. ``fd_seek`` Function ~~~~~~~~~~~~~~~~~~~~ @@ -262,27 +273,29 @@ Moves a file descriptor's offset and returns the new value. ``fd_prestat_dir_name`` Function ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Writes the preopened directory's name — the last component of its path, or ``/`` -for the root. ``fd_prestat_get`` returns the ``Prestat`` whose ``pr_name_len`` -determines the name's length. A buffer shorter than that name fails with -``Overflow``; any non-directory descriptor fails with ``Badf``. +The only preopened directory is the VFS root. This function writes its +guest-visible name, ``/``. ``fd_prestat_get`` returns the ``Prestat`` whose +``pr_name_len`` is therefore ``1``. A buffer shorter than that fails with +``Overflow``; every descriptor other than the root preopen, including a +directory returned by ``path_open``, fails with ``Badf``. ``fd_prestat_get`` Function ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -#. If the descriptor refers to a preopened directory, returns its ``Prestat`` - (a ``Dir`` whose ``pr_name_len`` is the length of the directory's last path - component). This is required for libc preopen discovery. +#. For the root preopen, returns a ``Dir`` whose ``pr_name_len`` is ``1``, the + length of its guest-visible name ``/``. This is required for libc preopen + discovery. #. For any other descriptor (``stdin``/``stdout``/``stderr``, a regular file, or a - non-existent descriptor) returns ``Badf``. + directory returned by ``path_open``, or a non-existent descriptor) returns + ``Badf``. ``fd_write`` Function ~~~~~~~~~~~~~~~~~~~~~ -Only ``stdout`` and ``stderr`` are writable; a file or directory descriptor -fails with ``Rofs``. The written octets are diagnostic output: they are not part -of the :ref:`gvm-def-vm-result` and are not covered by the -:ref:`gvm-def-execution-hash`. +After the rights check, only ``stdout`` and ``stderr`` are writable; a file or +directory descriptor fails with ``Rofs``. The written octets are diagnostic +output: they are not part of the :ref:`gvm-def-vm-result` and are not covered +by the :ref:`gvm-def-execution-hash`. The call reports every supplied octet as written and never fails on the underlying stream, so a contract cannot observe whether the :term:`Host` @@ -312,18 +325,24 @@ Returns the ``Filestat`` described in `File Metadata`_ with ``nlink`` ``1`` and - ``directory`` for a directory descriptor - ``character_device`` for ``stdout``/``stderr`` +.. _gvm-def-wasi-descriptor-rights: + ``fd_fdstat_get`` Function ~~~~~~~~~~~~~~~~~~~~~~~~~~ ``fs_flags`` is always empty — no descriptor is appending, non-blocking or -synchronous — and ``fs_rights_inheriting`` always equals ``fs_rights_base``. -The rights are fixed per descriptor kind and never include a mutating right: +synchronous. The maximum read-only rights are: + +- A regular-file descriptor has the read, seek, tell, advise, sync, datasync + and fd-filestat-get base rights, and no inheriting rights +- A directory descriptor has the ``path_open``, readdir, path-filestat-get and + fd-filestat-get base rights; its supported inheriting rights are the union of + the regular-file and directory base rights +- ``stdout``/``stderr`` have ``unknown`` filetype and the write right in both + masks -- a file descriptor: ``regular_file`` with the read, seek, tell, advise, sync, - datasync, readdir, readlink, ``path_open`` and filestat-get rights -- a directory descriptor: ``directory`` with the read, ``path_open``, readdir, - readlink and filestat-get rights -- ``stdout``/``stderr``: ``unknown`` filetype with the write right only +The root preopen receives the full directory masks. For a descriptor returned +by ``path_open``, both masks are further limited as described in that function. ``fd_close`` Function ~~~~~~~~~~~~~~~~~~~~~ @@ -335,7 +354,7 @@ descriptor fails with ``Badf``. ``fd_advise`` Function ~~~~~~~~~~~~~~~~~~~~~~ -Does nothing and always returns success. +Does nothing and returns success after the rights check. ``clock_time_get`` Function ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/01-functions.rst b/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/01-functions.rst index 8b3077fa..d8cf25f3 100644 --- a/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/01-functions.rst +++ b/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/01-functions.rst @@ -59,6 +59,6 @@ Returns - ``error_success`` on success - ``error_inval`` for invalid requests - ``error_forbidden`` for permission violations -- ``error_inbalance`` for insufficient balance +- ``error_insufficient_balance`` for insufficient balance See :doc:`02-gl_call` for the list of available messages. diff --git a/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst b/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst index 026faa5d..2710685e 100644 --- a/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst +++ b/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/02-gl_call.rst @@ -1,8 +1,8 @@ ``gl_call`` Messages ==================== -``EthSend`` Message -------------------- +``EmitExternalMessage`` Message +------------------------------- Sends transaction to Ethereum address with optional value transfer. @@ -12,7 +12,7 @@ Payload .. code-block:: { - "EthSend": { + "EmitExternalMessage": { "address": Address, // 20-byte target address "calldata": Bytes, // EVM calldata "value": U256 // Wei to transfer @@ -26,8 +26,8 @@ Requirements #. :ref:`gvm-perm-send-messages` #. Sufficient contract balance for value transfer -``EthCall`` Message -------------------- +``ExternalCall`` Message +------------------------ Calls Ethereum contract method (read-only operation). @@ -37,7 +37,7 @@ Payload .. code-block:: { - "EthCall": { + "ExternalCall": { "address": Address, // 20-byte target contract address "calldata": Bytes // EVM calldata } @@ -65,7 +65,8 @@ Payload "CallContract": { "address": Address, // 20-byte target contract address "calldata": Calldata, // Method call in calldata format - "state": Number // Storage type: 0=default, 1=latest_final, 2=latest_non_final + "storage_view": Number, // Storage view: 0=default, 1=latest_finalized, 2=latest_decided + "catch_vm_error": Bool // optional (default false): take a VM error as the result } } @@ -78,6 +79,11 @@ Requirements Creates a :term:`sub-VM`. See :ref:`gvm-meta-property-derivation`. +A :term:`sub-VM` that ends in a :ref:`gvm-def-vm-error` normally ends its caller +too. With *param* ``catch_vm_error`` set, the caller reads that error as the +call's result instead. A fatal :ref:`gvm-def-vm-error` is never caught: the flag +does not apply to it, and the caller ends regardless. + Before every call, :term:`GenVM` asks the :term:`host` whether to delegate the callee to another executor. A null answer preserves the local path, including :ref:`gvm-def-str-trie-value-vm-error-invalid-contract-major-mismatch`. A @@ -92,10 +98,10 @@ answered with ``Errno::Inval`` like the other argument checks and the caller can recover — unlike a top-level entry, where the same violation is the execution's result (:ref:`gvm-vm-startup-entry-validation`). -.. _gvm-def-post-message: +.. _gvm-def-emit-internal-message: -``PostMessage`` Message ------------------------ +``EmitInternalMessage`` Message +------------------------------- Posts message to GenLayer contract for later execution. @@ -110,11 +116,11 @@ Payload .. code-block:: { - "PostMessage": { + "EmitInternalMessage": { "address": Address, // 20-byte target contract address "calldata": Calldata, // Method call in calldata format "value": U256, // Wei to transfer - "on": String, // "finalized" or "accepted" + "on": String, // "finalized" or "decided" "use_balance": Bool, // optional (default false), see below "fee_params": FeeParams // optional (default absent), required iff use_balance } @@ -144,8 +150,8 @@ transaction's fee configuration and mirrors the chain's .. code-block:: FeeParams { - "leader_timeunits_allocation": U256, // per-round leader time units - "validator_timeunits_allocation": U256, // per-round validator time units + "leader_time_units_allocation": U256, // per-round leader time units + "validator_time_units_allocation": U256, // per-round validator time units "execution_budget_per_round": U256, // unified budget per leader round "rotations": [U256], // per-round rotations; non-empty. // rotations[0] is the initial round, @@ -166,7 +172,7 @@ Semantics: - The message is excluded from allocation matching, so no matching node is required (and none is consulted). - The contract must be able to cover ``value + metered_fee`` from its balance; - otherwise the call fails with ``Inbalance``. + otherwise the call fails with ``InsufficientBalance``. - The emitted allocation subtree is **empty**: nesting is fail-closed, so a child message must itself set ``use_balance`` or it fails to fund. @@ -181,8 +187,8 @@ Semantics: - Out-of-bounds magnitudes: prices and budgets (``max_price_gen_per_time_unit``, ``storage_fee_max_gas_price``, ``receipt_fee_max_gas_price``, ``execution_budget_per_round``) must be below - 2\ :sup:`96`; counts (``leader_timeunits_allocation``, - ``validator_timeunits_allocation``, each ``rotations`` entry) below + 2\ :sup:`96`; counts (``leader_time_units_allocation``, + ``validator_time_units_allocation``, each ``rotations`` entry) below 2\ :sup:`32`. These bounds keep the metered floor within ``U256``. Metering additionally enforces node-configured floors, surfaced as ``VMError``\ s: @@ -193,8 +199,8 @@ Metering additionally enforces node-configured floors, surfaced as ``VMError``\ - ``fee too_many_rounds`` — ``rotations`` implies more consensus rounds than the node's validator table supports (on-chain ``MAX_ROUNDS``). -``DeployContract`` Message --------------------------- +``EmitInternalDeployMessage`` Message +------------------------------------ Deploys new intelligent contract to blockchain. @@ -204,11 +210,11 @@ Payload .. code-block:: { - "DeployContract": { + "EmitInternalDeployMessage": { "calldata": Calldata, // Constructor arguments in calldata format "code": Bytes, // Contract bytecode "value": U256, // Wei to transfer - "on": String, // "finalized" or "accepted" + "on": String, // "finalized" or "decided" "salt_nonce": U256, // Salt for CREATE2-style deterministic addressing "use_balance": Bool, // optional (default false) "fee_params": FeeParams // optional (default absent), required iff use_balance @@ -246,7 +252,8 @@ Payload "data_leader": Bytes, // Code/data for leader execution "data_validator": Bytes, // Code/data for validator execution "runner": String, // optional (default "contract"): runner to execute - "custom_runners": [String] // optional (default absent): custom runners to grant + "custom_runners": [String], // optional (default absent): custom runners to grant + "catch_vm_error": Bool // optional (default false): take a VM error as the result } } @@ -262,6 +269,11 @@ Creates a non-deterministic :term:`sub-VM`. Derivation of its meta-properties, including the *param* ``runner`` and *param* ``custom_runners`` semantics, is specified in :ref:`gvm-meta-property-derivation`. +A :term:`sub-VM` that ends in a :ref:`gvm-def-vm-error` normally ends its caller +too. With *param* ``catch_vm_error`` set, the caller reads that error as the +call's result instead. A fatal :ref:`gvm-def-vm-error` is never caught: the flag +does not apply to it, and the caller ends regardless. + .. _gvm-def-gl-call-sandbox: ``Sandbox`` Message @@ -281,8 +293,8 @@ Payload "runner": String, // runner to execute; becomes the child's "contract" "allow_write_storage": Bool, // Whether to allow storage writes "allow_send_messages": Bool, // Whether to allow sending messages - "allow_register_runners": Bool, // Whether to allow registering runners - "custom_runners": [String] // optional (default absent): custom runners to grant + "custom_runners": [String], // optional (default absent): custom runners to grant + "changes_on_error": String // fate of the child's changes on a non-return } } @@ -295,10 +307,14 @@ meta-properties, including the *param* ``runner`` and *param* :ref:`gvm-meta-property-derivation`. The caller receives the sandbox result (:ref:`gvm-def-subvm-result-encoding`) -and may handle both :ref:`gvm-def-vm-error` and :ref:`gvm-def-user-error`; -storage writes performed by the sandbox are not reverted on error. If the -sandbox terminates with :ref:`gvm-def-fatal-vm-error`, the caller terminates -with the same fatal VM error instead. +and may handle both :ref:`gvm-def-vm-error` and :ref:`gvm-def-user-error`. +If the sandbox terminates with :ref:`gvm-def-fatal-vm-error`, the caller +terminates with the same fatal VM error instead. + +*param* ``changes_on_error`` says what becomes of the storage writes and +emissions of a sandbox that does not :ref:`gvm-def-return`. ``"inherit"`` is +its only accepted value: the caller keeps them, exactly as it keeps the ones it +made itself. Any other value is a malformed message .. _gvm-def-gl-call-register-runner: @@ -317,7 +333,7 @@ Payload { "RegisterRunner": { - "code": Bytes // runner archive (ustar/zip or commented text) + "code": Bytes // runner archive (zip, raw wasm or commented text) } } @@ -325,7 +341,6 @@ Requirements ~~~~~~~~~~~~ #. :ref:`gvm-perm-deterministic` -#. :ref:`gvm-perm-register-runners` Semantics ~~~~~~~~~ @@ -339,8 +354,8 @@ enters the caller's loaded set. The outcomes, in check order, are: -#. Missing :ref:`gvm-def-det-mode` or :ref:`gvm-perm-register-runners`: the call - fails with ``Forbidden``. Nothing is charged and no state changes. +#. Missing :ref:`gvm-def-det-mode`: the call fails with ``Forbidden``. Nothing + is charged and no state changes. #. Insufficient memory for the charge: the :term:`sub-VM` exits with an out-of-memory :ref:`gvm-def-vm-error`. Nothing is charged and the runner is not registered. @@ -393,7 +408,7 @@ Payload "WebRender": { "mode": String, // "text", "html", or "screenshot" "url": String, // URL to render - "wait_after_loaded": String // Wait duration, e.g. "5s" or "500ms" + "post_load_wait": String // Wait duration, e.g. "5s" or "500ms" } } @@ -623,11 +638,14 @@ Requirements .. _tracing-runtime-microsec: -``Trace.RuntimeMicroSec`` Sub-Message -------------------------------------- +``Trace.RuntimeMicroseconds`` Sub-Message +----------------------------------------- In :ref:`gvm-def-non-det-mode` returns the elapsed execution time in microseconds since VM start. -In :ref:`gvm-def-det-mode`, it returns ``0`` — exposing real elapsed time there would break determinism. The sole exception is the ``unsafe-tracing`` debug level (see the executor "Debug modes" section), which returns real elapsed time even in deterministic mode; that level is for local debugging only and must never be used on a consensus network. +In :ref:`gvm-def-det-mode`, it returns ``0`` — exposing real elapsed time there +would break determinism. An implementation MAY support a debug mode that +returns real elapsed time instead; such a mode is for local debugging only and +MUST NOT be used on a consensus network. Payload ~~~~~~~ @@ -635,13 +653,14 @@ Payload .. code-block:: { - "Trace": "RuntimeMicroSec" + "Trace": "RuntimeMicroseconds" } .. note:: The returned value is not at an implementation's discretion — in - :ref:`gvm-def-det-mode` it is exactly ``0``, and the call never fails (see + :ref:`gvm-def-det-mode` it is exactly ``0`` unless an implementation's debug + mode permits real elapsed time, and the call never fails (see :ref:`gvm-def-gl-call-observable-discretion`). Requirements diff --git a/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/03-schemas.rst b/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/03-schemas.rst index 2aeff786..0a5a0243 100644 --- a/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/03-schemas.rst +++ b/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/03-schemas.rst @@ -38,9 +38,9 @@ Validity Of A Proposed Result A result the executor did not compute itself — the leader-proposed non-deterministic result consumed in :ref:`gvm-def-sync-mode` and :ref:`gvm-def-validator-mode` — is accepted only if it satisfies all of the -following. A rejected proposal never traps and never bypasses the comparison -stage; it is replaced by a derived :ref:`gvm-def-vm-error` and handed to the -comparison as if it had been proposed. +following. A rejected proposal is replaced by a derived :ref:`gvm-def-vm-error`. +Sync mode returns that replacement without a vote; validator mode records a +disagreement without running the comparison stage #. The buffer is non-empty. An absent or empty proposal yields :ref:`gvm-def-str-trie-value-vm-error-leader-fault-nondet-output-absent`. diff --git a/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/index.rst b/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/index.rst index aa014ffe..c6b731d8 100644 --- a/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/index.rst +++ b/docs/website/src/spec/02-execution-environment/03-wasi_genlayer_sdk/index.rst @@ -34,7 +34,7 @@ Interface Definition static const uint32_t error_io = 5 static const uint32_t error_forbidden = 6 - static const uint32_t error_inbalance = 7 + static const uint32_t error_insufficient_balance = 7 __attribute__((import_module("genlayer_sdk"))) uint32_t storage_read(char const* slot, uint32_t index, char* buf, uint32_t buf_len); diff --git a/docs/website/src/spec/02-execution-environment/04-runners.rst b/docs/website/src/spec/02-execution-environment/04-runners.rst index b45f1edf..725ea338 100644 --- a/docs/website/src/spec/02-execution-environment/04-runners.rst +++ b/docs/website/src/spec/02-execution-environment/04-runners.rst @@ -17,11 +17,11 @@ forms (see the ``runner-id`` definition in the runner.json schema): - ``:`` — a packaged runner. ``human-readable-id`` is provided for convenience; ``hash`` is a hash of its contents (see `Hash Format`_). - ``contract`` — the runner of the contract currently being executed. -- ``chain:
[:[:]]`` — a runner code blob read from a storage - slot of an arbitrary contract (``a`` = accepted, ``f`` = finalized). ``
`` +- ``chain:
[:[:]]`` — a runner code blob read from a storage + slot of an arbitrary contract (``d`` = decided, ``f`` = finalized). ``
`` is a ``0x`` 20 byte hex address and ```` is a :term:`SlotID` encoded with - :doc:`../04-contract-interface/06-gvm32`. Both ```` and ```` are - optional: ```` defaults to ``a`` and ```` to the target contract's + :doc:`../04-contract-interface/06-gvm32`. Both ```` and ```` are + optional: ```` defaults to ``d`` and ```` to the target contract's root code slot. - ``custom:`` — a runner registered at runtime via the ``RegisterRunner`` ``gl_call``, looked up by its hash. @@ -41,8 +41,8 @@ A ``chain:`` id is code, but its resolution is an ordinary by consensus, not against whatever a node's chain tip happens to be. Every validator of the transaction therefore reads the same octets, whichever :term:`Host` serves them. -#. ``f`` (finalized) reads the state at the last finalized block; ``a`` - (accepted, the default) reads the accepted state — the same view a +#. ``f`` (finalized) reads the state at the last finalized block; ``d`` + (decided, the default) reads the decided state — the same view a read-only :ref:`gvm-def-gl-call-call-contract` child observes by default (see :ref:`contract-execution-flow`). Neither view includes the executing transaction's own uncommitted writes, so a contract cannot deploy code and @@ -111,6 +111,11 @@ Creates a minimal :term:`runner` configuration runner.json = { "StartWasm": "file" } file = # source bytes +Both defaults can be overridden by the module itself, through custom sections: +``genvm.version`` supplies the version string and ``genvm.runner.json`` the +whole ``runner.json``. This is how a single wasm file declares dependencies +without being repackaged as a ZIP + 3. Text-based ~~~~~~~~~~~~~ @@ -320,7 +325,7 @@ Conditionally executes an action based on WebAssembly execution mode. Properties ^^^^^^^^^^ -- ``cond``: WebAssembly mode, either ``det`` (deterministic) or ``nondet`` (non-deterministic) +- ``cond``: WebAssembly mode, either ``det`` (deterministic) or ``!det`` (non-deterministic) - ``action``: Action to execute when condition is met Example diff --git a/docs/website/src/spec/03-vm/01-startup.rst b/docs/website/src/spec/03-vm/01-startup.rst index b580d192..358bf604 100644 --- a/docs/website/src/spec/03-vm/01-startup.rst +++ b/docs/website/src/spec/03-vm/01-startup.rst @@ -43,7 +43,8 @@ runner loads. A payload that violates the convention produces the :ref:`gvm-def-str-trie-value-vm-error-malformed-entry` as the execution result, like any other failure at this point. -Messages emitted by :ref:`gvm-def-post-message` and ``DeployContract`` are +Messages emitted by :ref:`gvm-def-emit-internal-message` and +``EmitInternalDeployMessage`` are executed later as top-level entries, so they are validated here too. .. _gvm-vm-startup-message: @@ -139,7 +140,7 @@ Initial Entry the contract's root slot. - :ref:`gvm_vm_field_state_mode` is the default storage view. - :ref:`gvm_vm_field_topmost_runner_id` is the deployment runner or the - contract's accepted code runner. + contract's decided code runner. - :ref:`gvm_vm_field_det_subvm_hashes` and :ref:`gvm_vm_field_granted_custom` are empty. @@ -160,7 +161,7 @@ The child is read-only. keeps the parent's value (the plain copy rule), so by default the callee observes a view at least as recent as its caller's. Because the child cannot write, its :ref:`gvm-def-enum-value-storage-type-default` view is - the accepted state: it never includes the calling transaction's uncommitted + the decided state: it never includes the calling transaction's uncommitted writes (see :ref:`contract-execution-flow`). - :ref:`gvm_vm_field_topmost_runner_id` is the callee's contract runner. - :ref:`gvm_vm_field_granted_custom` is the caller's entire loaded @@ -195,8 +196,6 @@ inherited permission, never add one. - :ref:`gvm-perm-use-balance-for-message-fees` is cleared unless *param* ``allow_send_messages`` is set: balance-funded fees only affect message emission, so they are gated by the same flag - - :ref:`gvm-perm-register-runners` is cleared unless *param* - ``allow_register_runners`` is set - :ref:`gvm_vm_field_topmost_runner_id` is the *param* ``runner``, resolved in the caller's scope. diff --git a/docs/website/src/spec/03-vm/02-meta-properties.rst b/docs/website/src/spec/03-vm/02-meta-properties.rst index d66e3bf1..50d7a774 100644 --- a/docs/website/src/spec/03-vm/02-meta-properties.rst +++ b/docs/website/src/spec/03-vm/02-meta-properties.rst @@ -45,7 +45,7 @@ Internal field containing the permission meta-properties granted to the ~~~~~~~~~~~~~~ Internal field selecting the storage view used by reads -(a :ref:`gvm-def-enum-storage-type`). +(a :ref:`gvm-def-enum-storage-view`). .. _gvm_vm_field_topmost_runner_id: @@ -102,8 +102,8 @@ iff it can write storage. ``send_messages`` ~~~~~~~~~~~~~~~~~ -Allows sending messages to other addresses. Required by ``EthSend``, -``PostMessage``, and ``DeployContract``. Requires +Allows sending messages to other addresses. Required by ``EmitExternalMessage``, +``EmitInternalMessage``, and ``EmitInternalDeployMessage``. Requires :ref:`gvm-perm-deterministic`. .. _gvm-perm-call-others: @@ -111,7 +111,7 @@ Allows sending messages to other addresses. Required by ``EthSend``, ``call_others`` ~~~~~~~~~~~~~~~ -Allows calling other contracts. Required by ``EthCall`` and +Allows calling other contracts. Required by ``ExternalCall`` and :ref:`gvm-def-gl-call-call-contract`. Requires :ref:`gvm-perm-deterministic`. @@ -128,22 +128,14 @@ A :term:`sub-VM` that a host delegated to another executor (see caller holds. Such a :term:`sub-VM` has no connection to the non-deterministic modules, so the permission cannot be served there. -.. _gvm-perm-register-runners: - -``register_runners`` -~~~~~~~~~~~~~~~~~~~~ - -Allows registering runner archives at runtime via -:ref:`gvm-def-gl-call-register-runner`. Requires :ref:`gvm-perm-deterministic`. - .. _gvm-perm-use-balance-for-message-fees: ``can_use_balance_for_message_fees`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allows the contract to draw on its own balance to pay fees for outgoing -internal messages (``PostMessage``, ``DeployContract``). The flag has no effect -on external ``EthSend`` messages. +internal messages (``EmitInternalMessage``, ``EmitInternalDeployMessage``). The +flag has no effect on ``EmitExternalMessage``. Unlike the meta-properties above, this one is stored as a bit in the root slot's inline ``permissions`` bitfield and read before execution begins. The bit offset diff --git a/docs/website/src/spec/03-vm/04-determinism-mode-switching.rst b/docs/website/src/spec/03-vm/04-determinism-mode-switching.rst index 2d295c77..5554a0ba 100644 --- a/docs/website/src/spec/03-vm/04-determinism-mode-switching.rst +++ b/docs/website/src/spec/03-vm/04-determinism-mode-switching.rst @@ -37,16 +37,14 @@ so the resulting :ref:`gvm-def-vm-result` is simply the call's result. Validator Mode -------------- -The proposed result is accepted or replaced per -:ref:`gvm-def-proposed-result-validity`, then handed to the :term:`sub-VM` for -comparison. That :term:`sub-VM` must :ref:`gvm-def-return` a ``bool`` value: -whether the validator accepts the leader's result. Any other result has the same -effect as producing ``bool(false)``. - -A replaced proposal takes this same path — the derived code is compared like any -other, so the vote stays contract-controlled and no branch bypasses the -comparison. The replacement is a pure function of the proposed bytes, so every -honest validator derives the same value and votes identically. +An accepted proposal is handed to the :term:`sub-VM` for comparison. That +:term:`sub-VM` must :ref:`gvm-def-return` a ``bool`` value: whether the validator +accepts the leader's result. Any other result has the same effect as producing +``bool(false)`` + +A rejected proposal records a disagreement without running the comparison +stage. The contract cannot vote away bytes that no honest leader could have +produced Returns the accepted or replaced result. diff --git a/docs/website/src/spec/03-vm/05-result.rst b/docs/website/src/spec/03-vm/05-result.rst index 50cda1d1..0e0c0659 100644 --- a/docs/website/src/spec/03-vm/05-result.rst +++ b/docs/website/src/spec/03-vm/05-result.rst @@ -89,7 +89,7 @@ Represents a user-produced error in utf-8 format. Only a :ref:`gvm-def-return` carries effects. A topmost run that ends in :ref:`gvm-def-user-error` or :ref:`gvm-def-vm-error` reports no -``storage_changes`` and no ``emissions``, whatever it wrote or emitted before +``storage_deltas`` and no ``emissions``, whatever it wrote or emitted before failing, and its :ref:`gvm-def-execution-hash` covers those empty fields. .. _gvm-def-internal-error: @@ -178,7 +178,7 @@ with the following keys (in this order): #. ``data_fees_remaining`` #. ``emissions`` — emitted messages and events, in emission order #. ``kind`` — the :ref:`gvm-def-vm-result` result code -#. ``storage_changes`` +#. ``storage_deltas`` #. ``subvm_hashes`` — see :ref:`gvm-def-subvm-hash` #. ``wasm_store_hashes`` diff --git a/docs/website/src/spec/04-contract-interface/02-abi.rst b/docs/website/src/spec/04-contract-interface/02-abi.rst index 83c06ef3..1b16a3f2 100644 --- a/docs/website/src/spec/04-contract-interface/02-abi.rst +++ b/docs/website/src/spec/04-contract-interface/02-abi.rst @@ -60,7 +60,7 @@ and :ref:`gvm-def-gl-call-call-contract`. Call Key -------- -Every emitted message (see :ref:`gvm-def-post-message`) carries a ``call_key``: +Every emitted message (see :ref:`gvm-def-emit-internal-message`) carries a ``call_key``: a 256-bit unsigned integer that identifies which method the message targets. It serves the same role as a function selector in EVM, but is derived differently and is not truncated. diff --git a/docs/website/src/spec/04-contract-interface/03-storage.rst b/docs/website/src/spec/04-contract-interface/03-storage.rst index 61a6b130..7685c50e 100644 --- a/docs/website/src/spec/04-contract-interface/03-storage.rst +++ b/docs/website/src/spec/04-contract-interface/03-storage.rst @@ -59,7 +59,7 @@ the following data: - ``code_slot``: (offset 5) A raw 32-byte :term:`SlotID`. If it is all-zero (the default), the contract code is read from the ``code`` slot (offset 2); otherwise the code is read from the slot it points to (same 4-byte-length-prefixed layout). This lets a contract serve its - code from an arbitrary slot, including one shared via a ``chain:
::`` runner id. + code from an arbitrary slot, including one shared via a ``chain:
::`` runner id. - ``permissions``: (offset 37) A 32-byte (``u256``) little-endian permission bitfield read by the executor at the start of every load. Bit ``n`` corresponds to the permission whose value is ``n`` (currently only bit ``0``, ``can_use_balance_for_message_fees``). It is not reserved: diff --git a/docs/website/src/spec/04-contract-interface/06-gvm32.rst b/docs/website/src/spec/04-contract-interface/06-gvm32.rst index 8b208434..cf063919 100644 --- a/docs/website/src/spec/04-contract-interface/06-gvm32.rst +++ b/docs/website/src/spec/04-contract-interface/06-gvm32.rst @@ -67,4 +67,4 @@ Usage - **Custom runner ids** — ``custom:`` where ```` is the GVM32 encoding of the SHA3-256 of the registered code. - **Slot ids** — a :term:`SlotID` is rendered in GVM32, including the ```` - component of a ``chain:
::`` runner id. + component of a ``chain:
::`` runner id. diff --git a/docs/website/src/spec/appendix/constants.rst b/docs/website/src/spec/appendix/constants.rst index d744f684..0e7ad72d 100644 --- a/docs/website/src/spec/appendix/constants.rst +++ b/docs/website/src/spec/appendix/constants.rst @@ -29,31 +29,31 @@ vm_error Value: ``2`` -.. _gvm-def-enum-storage-type: +.. _gvm-def-enum-storage-view: -storage_type +storage_view ------------ Type: u8 -.. _gvm-def-enum-value-storage-type-default: +.. _gvm-def-enum-value-storage-view-default: default ~~~~~~~ Value: ``0`` -.. _gvm-def-enum-value-storage-type-latest-final: +.. _gvm-def-enum-value-storage-view-latest-finalized: -latest_final -~~~~~~~~~~~~ +latest_finalized +~~~~~~~~~~~~~~~~ Value: ``1`` -.. _gvm-def-enum-value-storage-type-latest-non-final: +.. _gvm-def-enum-value-storage-view-latest-decided: -latest_non_final -~~~~~~~~~~~~~~~~ +latest_decided +~~~~~~~~~~~~~~ Value: ``2`` @@ -344,15 +344,15 @@ Param: i32 ``out_of message_fee total`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. _gvm-def-str-trie-value-vm-error-out-of-message-fee-node: +.. _gvm-def-str-trie-value-vm-error-out-of-message-fee-allocation-budget: -``out_of message_fee node`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``out_of message_fee allocation_budget`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. _gvm-def-str-trie-value-vm-error-out-of-vm-recursion: +.. _gvm-def-str-trie-value-vm-error-out-of-subvm-recursion: -``out_of vm_recursion`` -~~~~~~~~~~~~~~~~~~~~~~~ +``out_of subvm_recursion`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. _gvm-def-str-trie-value-vm-error-out-of-nondet-blocks: @@ -374,10 +374,10 @@ Param: i32 ``out_of fds`` ~~~~~~~~~~~~~~ -.. _gvm-def-str-trie-value-vm-error-fee-no-matching-node: +.. _gvm-def-str-trie-value-vm-error-fee-no-matching-allocation: -``fee no_matching_node`` -~~~~~~~~~~~~~~~~~~~~~~~~ +``fee no_matching_allocation`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. _gvm-def-str-trie-value-vm-error-fee-below-minimum: @@ -441,15 +441,40 @@ This error remains terminal for top-level and runner loads. During :ref:`gvm-def ``invalid_contract wasm entrypoint`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. _gvm-def-str-trie-detail-vm-error-internal: +.. _gvm-def-str-trie-detail-vm-error-out-of-receipt-message-internal: -``# internal`` -~~~~~~~~~~~~~~ +``out_of receipt message # internal`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. _gvm-def-str-trie-detail-vm-error-external: +.. _gvm-def-str-trie-detail-vm-error-out-of-message-fee-total-internal: -``# external`` -~~~~~~~~~~~~~~ +``out_of message_fee total # internal`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _gvm-def-str-trie-detail-vm-error-out-of-message-fee-total-external: + +``out_of message_fee total # external`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _gvm-def-str-trie-detail-vm-error-out-of-message-fee-allocation-budget-internal: + +``out_of message_fee allocation_budget # internal`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _gvm-def-str-trie-detail-vm-error-out-of-message-fee-allocation-budget-external: + +``out_of message_fee allocation_budget # external`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _gvm-def-str-trie-detail-vm-error-fee-no-matching-allocation-internal: + +``fee no_matching_allocation # internal`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _gvm-def-str-trie-detail-vm-error-fee-no-matching-allocation-external: + +``fee no_matching_allocation # external`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. _gvm-def-const-event-max-topics: diff --git a/docs/website/src/spec/appendix/internal-constants.rst b/docs/website/src/spec/appendix/internal-constants.rst index 44b72f37..2323d901 100644 --- a/docs/website/src/spec/appendix/internal-constants.rst +++ b/docs/website/src/spec/appendix/internal-constants.rst @@ -127,6 +127,13 @@ wasm_stack_value_slots Value: ``65535`` +.. _gvm-def-consts-value-top-limits-vfs-path-components: + +vfs_path_components +~~~~~~~~~~~~~~~~~~~ + +Value: ``128`` + .. _gvm-def-consts-runner-limits: runner_limits diff --git a/docs/website/src/spec/changelog.rst b/docs/website/src/spec/changelog.rst index 2751adcb..7af5d709 100644 --- a/docs/website/src/spec/changelog.rst +++ b/docs/website/src/spec/changelog.rst @@ -13,6 +13,22 @@ v0.3 Breaking ~~~~~~~~ +#. The pre-finalization state is spelled *decided* everywhere it is named. + :ref:`gvm-def-enum-storage-view` reads ``latest_finalized`` (1) and + ``latest_decided`` (2) instead of ``latest_final`` and ``latest_non_final``, + the ``on`` field of the ``EmitInternalMessage`` and + ``EmitInternalDeployMessage`` ``gl_call`` + payloads (:doc:`02-execution-environment/03-wasi_genlayer_sdk/02-gl_call`) + takes ``"decided"`` instead of ``"accepted"``, and a ``chain:`` runner id + selects it with ``d`` rather than ``a`` (see + :ref:`gvm-def-chain-runner-state`). Numeric enum values are unchanged; none + of the old spellings is accepted +#. A ``chain:`` runner id resolved while a contract is being deployed + canonicalizes to ``i``, which previously spelled the deploy state as ``d`` +#. The ``When`` action's ``cond`` field spells the non-deterministic mode + ``!det``; the previous ``nondet`` spelling is rejected, with no + back-compat alias. See the ``When`` action in + :doc:`02-execution-environment/04-runners` #. ``runner.json`` rejects unknown top-level and nested fields; a runner that relied on an extra field being silently ignored fails to load. The single top-level ``$schema`` string annotation is still accepted. See diff --git a/executors/v0.2.x b/executors/v0.2.x index 9da582c9..2a32a840 160000 --- a/executors/v0.2.x +++ b/executors/v0.2.x @@ -1 +1 @@ -Subproject commit 9da582c9eb7b8f801cc00a4fd8a20f5dedd33da3 +Subproject commit 2a32a84066012eaa887a1e62d345a92e8b37c562 diff --git a/executors/v0.3.x b/executors/v0.3.x index 55b13368..79e89ad5 160000 --- a/executors/v0.3.x +++ b/executors/v0.3.x @@ -1 +1 @@ -Subproject commit 55b133688edd8950b8873aad9ce5c2f1040a8c98 +Subproject commit 79e89ad57188fd0c6ae8d939fc0056461d5d0d21 diff --git a/flake.nix b/flake.nix index dee0485b..de34f94d 100644 --- a/flake.nix +++ b/flake.nix @@ -525,7 +525,7 @@ done ''; - # Merge a { uid -> `//.tar` tree } set into one tree. + # Merge a { uid -> `//.zip` tree } set into one tree. merge-runner-trees = name: uni: pkgs.runCommand name { srcs = builtins.attrValues uni; } '' diff --git a/implementation/src/llm/handler.rs b/implementation/src/llm/handler.rs index 30b01c4a..76b843ab 100644 --- a/implementation/src/llm/handler.rs +++ b/implementation/src/llm/handler.rs @@ -89,7 +89,7 @@ impl crate::common::MessageHandler match message { llm_iface::Message::Prompt { payload, - remaining_fuel_as_gen, + remaining_time_fee_gen_wei, } => { if payload.images.len() > 2 { return Err(ModuleError { @@ -121,15 +121,15 @@ impl crate::common::MessageHandler } } self.0 - .exec_prompt(self.0.clone(), payload, remaining_fuel_as_gen) + .exec_prompt(self.0.clone(), payload, remaining_time_fee_gen_wei) .await } llm_iface::Message::PromptTemplate { payload, - remaining_fuel_as_gen, + remaining_time_fee_gen_wei, } => { self.0 - .exec_prompt_template(self.0.clone(), payload, remaining_fuel_as_gen) + .exec_prompt_template(self.0.clone(), payload, remaining_time_fee_gen_wei) .await } } @@ -208,7 +208,7 @@ impl Inner { &self, _zelf: Arc, payload: llm_iface::PromptPayload, - remaining_fuel_as_gen: primitive_types::U256, + remaining_time_fee_gen_wei: primitive_types::U256, ) -> ModuleResult { log_debug_into!(&LoggerWithId, payload:serde = payload, genvm_id:id = self.genvm_id.0; "exec_prompt start"); @@ -216,13 +216,13 @@ impl Inner { .user_vm .vm .to_value_with(&payload, scripting::DEFAULT_LUA_SER_OPTIONS)?; - let fuel = self.u256_to_lua_rat(remaining_fuel_as_gen)?; + let time_fee_gen_wei = self.u256_to_lua_rat(remaining_time_fee_gen_wei)?; let res: Result = self .user_vm .call_fn( &self.user_vm.data.exec_prompt, - (self.ctx_val.clone(), payload, fuel), + (self.ctx_val.clone(), payload, time_fee_gen_wei), ) .await; @@ -240,7 +240,7 @@ impl Inner { &self, _zelf: Arc, payload: llm_iface::PromptTemplatePayload, - remaining_fuel_as_gen: primitive_types::U256, + remaining_time_fee_gen_wei: primitive_types::U256, ) -> ModuleResult { log_debug_into!(&LoggerWithId, payload:serde = payload, genvm_id:id = self.genvm_id.0; "exec_prompt_template start"); @@ -248,13 +248,13 @@ impl Inner { .user_vm .vm .to_value_with(&payload, scripting::DEFAULT_LUA_SER_OPTIONS)?; - let fuel = self.u256_to_lua_rat(remaining_fuel_as_gen)?; + let time_fee_gen_wei = self.u256_to_lua_rat(remaining_time_fee_gen_wei)?; let res: Result = self .user_vm .call_fn( &self.user_vm.data.exec_prompt_template, - (self.ctx_val.clone(), payload, fuel), + (self.ctx_val.clone(), payload, time_fee_gen_wei), ) .await; diff --git a/implementation/src/manager/run.rs b/implementation/src/manager/run.rs index 90cd7399..de98dd67 100644 --- a/implementation/src/manager/run.rs +++ b/implementation/src/manager/run.rs @@ -1028,7 +1028,7 @@ pub struct Request { #[calldata(default = default_host_hello_data)] pub host_hello_data: Vec, /// Whether the host wants to be asked where a `CallContract` should run. - /// When false the manager answers `resolve_callcontract_executor` itself + /// When false the manager answers `resolve_call_contract_executor` itself /// with a null reply, so every call stays in-process and the host never /// has to implement that method. #[serde(default)] @@ -1509,7 +1509,7 @@ fn nested_effect(reported: &genvm_modules_interfaces::ReportedResult) -> Option< wasm_store_hashes: _, // A remaining budget is a report, not a consumption. data_fees_remaining: _, - storage_changes, + storage_deltas, emissions, nondet_disagreement, nondet_results, @@ -1521,11 +1521,11 @@ fn nested_effect(reported: &genvm_modules_interfaces::ReportedResult) -> Option< message_fee, event, }, - llm_consumption, + llm_consumed_gen_wei, } = reported; - if !storage_changes.is_empty() { - return Some("storage_changes"); + if !storage_deltas.is_empty() { + return Some("storage_deltas"); } if !emissions.is_empty() { return Some("emissions"); @@ -1536,8 +1536,8 @@ fn nested_effect(reported: &genvm_modules_interfaces::ReportedResult) -> Option< if !nondet_results.is_empty() { return Some("nondet_results"); } - if !llm_consumption.is_zero() { - return Some("llm_consumption"); + if !llm_consumed_gen_wei.is_zero() { + return Some("llm_consumed_gen_wei"); } for (bucket, name) in [ @@ -1678,23 +1678,23 @@ fn read_manager_host_stream( log_error_into!(&LoggerWithId, genvm_id:id = genvm_id.0, error:ah = &e; "failed to send run_nested reply"); } } - Ok(host_fns::Methods::ResolveCallcontractExecutor) => { + Ok(host_fns::Methods::ResolveCallContractExecutor) => { // The do-nothing route: a request that did not opt into // `hook_cross_contract_calls` gets this arm instead of the // node's host, and a null reply means "stay in-process". let mut request = [0u8; calldata::ADDRESS_SIZE + 2]; if let Err(e) = reader.read_exact(&mut request).await { - log_error_into!(&LoggerWithId, genvm_id:id = genvm_id.0, error:err = e; "failed to read resolve_callcontract_executor request"); + log_error_into!(&LoggerWithId, genvm_id:id = genvm_id.0, error:err = e; "failed to read resolve_call_contract_executor request"); return; } let encoded = calldata::encode_obj(&calldata::Value::Null); let mut writer = writer.lock().await; if let Err(e) = writer.write_all(&[host_fns::Errors::Ok as u8]).await { - log_error_into!(&LoggerWithId, genvm_id:id = genvm_id.0, error:err = e; "failed to send resolve_callcontract_executor status"); + log_error_into!(&LoggerWithId, genvm_id:id = genvm_id.0, error:err = e; "failed to send resolve_call_contract_executor status"); return; } if let Err(e) = write_length_prefixed(&mut *writer, &encoded).await { - log_error_into!(&LoggerWithId, genvm_id:id = genvm_id.0, error:ah = &e; "failed to send resolve_callcontract_executor reply"); + log_error_into!(&LoggerWithId, genvm_id:id = genvm_id.0, error:ah = &e; "failed to send resolve_call_contract_executor reply"); return; } } @@ -1936,7 +1936,7 @@ fn execution_data_from_request(req: &Request) -> genvm_modules_interfaces::Execu method_hosts[host_fns::Methods::ConsumeResult as usize] = 1; method_hosts[host_fns::Methods::RunNested as usize] = 1; if !req.hook_cross_contract_calls { - method_hosts[host_fns::Methods::ResolveCallcontractExecutor as usize] = 1; + method_hosts[host_fns::Methods::ResolveCallContractExecutor as usize] = 1; } genvm_modules_interfaces::ExecutionData { @@ -2157,7 +2157,7 @@ impl Ctx { max_execution_minutes: parent_req.max_execution_minutes, bucket_totals: Vec::new(), host_data: parent_req.host_data.clone(), - timestamp: envelope.message.datetime, + timestamp: envelope.message.transaction_timestamp, host: parent_req.host.clone(), extra_args: Vec::new(), calldata: envelope.calldata.clone(), @@ -2434,7 +2434,7 @@ async fn run_genvm_process( let mut proc = build_genvm_command(command_path, &req, genvm_id, &write_fd); // Setup manager host socketpair (host id=1 for consume_result, run_nested - // and -- unless the request hooks them -- resolve_callcontract_executor) + // and -- unless the request hooks them -- resolve_call_contract_executor) let (manager_parent, manager_child) = FdWrapper::socketpair()?; manager_parent.set_cloexec(true)?; manager_child.set_cloexec(false)?; diff --git a/implementation/src/manager/run_test.rs b/implementation/src/manager/run_test.rs index 098d36b9..7f8ffae1 100644 --- a/implementation/src/manager/run_test.rs +++ b/implementation/src/manager/run_test.rs @@ -550,13 +550,13 @@ fn clean_reported() -> genvm_modules_interfaces::ReportedResult { data: calldata::Value::Null.into(), backtrace: None, wasm_store_hashes: Default::default(), - storage_changes: Vec::new(), + storage_deltas: Vec::new(), emissions: Vec::new(), nondet_disagreement: None, nondet_results: Vec::new(), data_fees_remaining: Vec::new(), data_fees_consumed: Default::default(), - llm_consumption: primitive_types::U256::zero(), + llm_consumed_gen_wei: primitive_types::U256::zero(), } } @@ -640,7 +640,7 @@ fn some_storage_delta() -> genvm_modules_interfaces::StorageDelta { } fn some_emission() -> genvm_modules_interfaces::ExecutionEmission { - genvm_modules_interfaces::ExecutionEmission::EmitEvent { + genvm_modules_interfaces::ExecutionEmission::Event { topics: Vec::new(), blob: calldata::Map::new().into(), storage_fee: primitive_types::U256::zero(), @@ -660,16 +660,16 @@ fn nested_reply_refuses_every_reported_effect() { // end to end -- the boundary derivation clears the permission behind each of // them -- so this is the only place the refusal is exercised. let mutate: [(&str, fn(&mut genvm_modules_interfaces::ReportedResult)); 10] = [ - ("storage_changes", |r| { - r.storage_changes.push(some_storage_delta()) + ("storage_deltas", |r| { + r.storage_deltas.push(some_storage_delta()) }), ("emissions", |r| r.emissions.push(some_emission())), ("nondet_disagreement", |r| r.nondet_disagreement = Some(0)), ("nondet_results", |r| { r.nondet_results.push(bytes::Bytes::from_static(&[1])) }), - ("llm_consumption", |r| { - r.llm_consumption = primitive_types::U256::one() + ("llm_consumed_gen_wei", |r| { + r.llm_consumed_gen_wei = primitive_types::U256::one() }), ("storage", |r| { r.data_fees_consumed.storage = primitive_types::U256::one() @@ -892,7 +892,7 @@ fn message_schema_matches_the_rust_struct() { chain_id, value, is_init, - datetime, + transaction_timestamp, } = message; }; @@ -904,7 +904,7 @@ fn message_schema_matches_the_rust_struct() { "chain_id", "value", "is_init", - "datetime", + "transaction_timestamp", ] .map(str::to_owned) .to_vec(); diff --git a/runners/views/universal.nix b/runners/views/universal.nix index 22f09e29..edf49ae8 100644 --- a/runners/views/universal.nix +++ b/runners/views/universal.nix @@ -1,4 +1,4 @@ -# Builds a runner list into a `runners///.tar` tree, one derivation +# Builds a runner list into a `runners///.zip` tree, one derivation # per uid (so identical runners shared across executor lines de-duplicate). { pkgs, @@ -24,9 +24,9 @@ builtins.foldl' ( throw "invalid runner uid `${x.uid}`; expected `:`" else builtins.elemAt uidMatch 1; - # `//.tar` — consumers place this tree *under* their own + # `//.zip` — consumers place this tree *under* their own # `runners/` dir, so do NOT prefix another `runners/` here. - result-path = "${x.id}/${builtins.substring 0 2 hash32}/${builtins.substring 2 50 hash32}.tar"; + result-path = "${x.id}/${builtins.substring 0 2 hash32}/${builtins.substring 2 50 hash32}.zip"; in '' mkdir -p $out/$(dirname -- ${result-path}) diff --git a/support/tools/genvm-tool/genvm_tool/codegen/go.py b/support/tools/genvm-tool/genvm_tool/codegen/go.py index 2561fde7..5feab6f2 100644 --- a/support/tools/genvm-tool/genvm_tool/codegen/go.py +++ b/support/tools/genvm-tool/genvm_tool/codegen/go.py @@ -42,6 +42,11 @@ def _trie_inner(node: TrieNode, root_camel: str, buf: list[str]) -> None: # Walk children in source order: unlike the struct-based backends, go emits a # flat function per terminal/param exactly where it appears in the JSON. prefix = root_camel + node.suffix + for detail in node.details: + val = f'{" ".join(node.parts)} # {detail}' + buf.append( + f'func {prefix}{to_camel(detail)}() {root_camel} {{ return {_dump(val)} }}\n' + ) for kind, head, payload in node.order: if kind == 'leaf': val = ' '.join(payload) @@ -74,10 +79,7 @@ def render(defs: list[Definition], *, go_package: str = 'genvm', **_opts) -> str buf.append(f'package {go_package}\n') needs_fmt = any(isinstance(d, StrTrie) and _has_param(d.root) for d in defs) - needs_strings = any(isinstance(d, StrTrie) and d.suffix is not None for d in defs) - imports = [ - n for n, needed in (('fmt', needs_fmt), ('strings', needs_strings)) if needed - ] + imports = [n for n, needed in (('fmt', needs_fmt),) if needed] if len(imports) == 1: buf.append(f'\nimport "{imports[0]}"\n') elif imports: @@ -109,14 +111,6 @@ def render(defs: list[Definition], *, go_package: str = 'genvm', **_opts) -> str root_camel = to_camel(d.name) buf.append(f'\ntype {root_camel} string\n\n') _trie_inner(d.root, root_camel, buf) - for name, _parts in d.suffix.leaves if d.suffix else []: - # Mirrors the rust/python guard: a value carries at most one detail. - buf.append( - f'func (v {root_camel}) {to_camel(name)}() {root_camel} {{ ' - f'if strings.Contains(string(v), " # ") {{ ' - f'panic("a value carries at most one detail") }}; ' - f'return v + {_dump(f" # {name}")} }}\n' - ) return ''.join(buf) diff --git a/support/tools/genvm-tool/genvm_tool/codegen/model.py b/support/tools/genvm-tool/genvm_tool/codegen/model.py index e9f319c0..80f9ae63 100644 --- a/support/tools/genvm-tool/genvm_tool/codegen/model.py +++ b/support/tools/genvm-tool/genvm_tool/codegen/model.py @@ -56,7 +56,8 @@ class TrieNode: ``terminal`` is set does ``' '.join(parts)`` name a leaf. A node is either a *param* node (``param`` set: consumes one typed argument) or a regular node carrying ``leaves`` (terminal strings) and ``methods`` (child nodes), mirroring - the original ruby generators. + the original ruby generators. ``details`` contains the ``#`` child's terminal + values when that child exists. """ suffix: str @@ -65,6 +66,7 @@ class TrieNode: param: tuple[str, list[str]] | None leaves: list[tuple[str, list[str]]] methods: list[tuple[str, 'TrieNode']] + details: list[str] # Children in original JSON order, tagged ``('leaf', head, parts)`` or # ``('method', head, child)``. ``leaves``/``methods`` are the grouped views the # struct-based backends (rust, python) want; the go backend, which emits flat @@ -78,10 +80,6 @@ class StrTrie: root: TrieNode entries: list # unfolded entries, kept for flat path enumeration (rst) docs: dict[str, str] - # Details reachable from any built value, spelled `` # ``. A - # root-level ``#`` entry declares them; they are not paths, so they stay out - # of ``root``/``entries`` and out of the generated validity check. - suffix: TrieNode | None = None Definition = Enum | Const | Consts | StrTrie @@ -117,8 +115,14 @@ def _build(entries, prefix_parts: list[str], suffix: str, terminal: bool) -> Tri param=None, leaves=[], methods=[], + details=[], order=[], ) + entries, node.details = _split_details(entries) + if node.details and not terminal: + raise ValueError( + f'{" ".join(prefix_parts) or "root"}: details require a public path' + ) for entry in entries: if entry is None: continue @@ -135,15 +139,17 @@ def _build(entries, prefix_parts: list[str], suffix: str, terminal: bool) -> Tri param=(param_type, current), leaves=[], methods=[], + details=[], order=[], ) node.methods.append((head, child)) node.order.append(('method', head, child)) elif isinstance(tail, list): non_null = [e for e in tail if e is not None] + path_children = [e for e in non_null if e['head'] != DETAIL_HEAD] # A trie level is terminal if any sibling was a `null` placeholder (or it - # has no real children): the path itself is a valid leaf string. - is_terminal = (len(tail) != len(non_null)) or len(non_null) == 0 + # has no path children): the path itself is a valid leaf string. + is_terminal = (len(tail) != len(non_null)) or len(path_children) == 0 if len(non_null) == 0: node.leaves.append((head, current)) node.order.append(('leaf', head, current)) @@ -161,40 +167,58 @@ def enumerate_paths(entries, prefix: str = ''): if entry is None: continue head = entry['head'] + if head == DETAIL_HEAD: + continue tail = entry['tail'] current = head if prefix == '' else f'{prefix} {head}' if isinstance(tail, str) and tail.startswith('$'): result.append((current, tail[1:])) elif isinstance(tail, list): non_null = [e for e in tail if e is not None] - is_terminal = (len(tail) != len(non_null)) or len(non_null) == 0 + path_children = [e for e in non_null if e['head'] != DETAIL_HEAD] + is_terminal = (len(tail) != len(non_null)) or len(path_children) == 0 if is_terminal: result.append((current, None)) result.extend(enumerate_paths(non_null, current)) return result +def enumerate_details(entries, prefix: str = ''): + """Flatten detail paths to `` # `` strings.""" + result = [] + for entry in entries: + if entry is None or entry['head'] == DETAIL_HEAD: + continue + head = entry['head'] + tail = entry['tail'] + current = head if prefix == '' else f'{prefix} {head}' + if not isinstance(tail, list): + continue + paths, details = _split_details(tail) + result.extend(f'{current} # {detail}' for detail in details) + result.extend(enumerate_details(paths, current)) + return result + + DETAIL_HEAD = '#' -"""Head of the root entry declaring a trie's detail suffixes.""" +"""Head of an entry declaring details for its parent public path.""" -def _split_details(name: str, entries: list) -> tuple[list, TrieNode | None]: - """Carve the ``#`` entry out of a trie's root, returning ``(paths, details)``.""" +def _split_details(entries: list) -> tuple[list, list[str]]: + """Carve a ``#`` child out of one trie node.""" found = [e for e in entries if e is not None and e['head'] == DETAIL_HEAD] if not found: - return entries, None + return entries, [] if len(found) > 1: - raise ValueError(f'{name}: more than one {DETAIL_HEAD!r} entry') + raise ValueError(f'more than one {DETAIL_HEAD!r} entry') tail = found[0]['tail'] if not isinstance(tail, list): - raise ValueError(f'{name}: {DETAIL_HEAD!r} must list details') + raise ValueError(f'{DETAIL_HEAD!r} must list details') for detail in tail: - # A detail is appended to an already-built value, so it has no place to - # hang a subtree off and no argument to render. if detail is None or detail['tail'] != []: - raise ValueError(f'{name}: a detail may not be nested or take a parameter') + raise ValueError('a detail may not be nested or take a parameter') paths = [e for e in entries if e is None or e['head'] != DETAIL_HEAD] - return paths, _build(tail, [], '', False) + return paths, [detail['head'] for detail in tail] def parse(data) -> list[Definition]: @@ -208,14 +232,13 @@ def parse(data) -> list[Definition]: elif kind == 'consts': defs.append(Consts(t['name'], t['repr'], t['values'])) elif kind == 'str_trie': - entries, details = _split_details(t['name'], [_unfold(e) for e in t['values']]) + entries = [_unfold(e) for e in t['values']] defs.append( StrTrie( t['name'], _build(entries, [], '', False), entries, t.get('docs', {}), - details, ) ) else: diff --git a/support/tools/genvm-tool/genvm_tool/codegen/python.py b/support/tools/genvm-tool/genvm_tool/codegen/python.py index 122ad849..548d3ab7 100644 --- a/support/tools/genvm-tool/genvm_tool/codegen/python.py +++ b/support/tools/genvm-tool/genvm_tool/codegen/python.py @@ -34,6 +34,11 @@ def _emit_struct(node: TrieNode, root_name: str, buf: list[str]) -> None: buf.append('\t@staticmethod\n') buf.append(f"\tdef val() -> '{root_name}':\n") buf.append(f"\t\treturn {root_name}('{val}')\n") + for name in node.details: + val = ' '.join(node.parts) + buf.append('\t@staticmethod\n') + buf.append(f"\tdef {name.lower()}() -> '{root_name}':\n") + buf.append(f"\t\treturn {root_name}('{val} # {name}')\n") for name, parts in node.leaves: val = ' '.join(parts) buf.append('\t@staticmethod\n') @@ -84,11 +89,6 @@ def _trie(t: StrTrie, buf: list[str]) -> None: buf.append('\t@staticmethod\n') buf.append(f"\tdef {head.lower()}() -> '_{root_name}{child.suffix}':\n") buf.append(f'\t\treturn _{root_name}{child.suffix}()\n') - if t.suffix is not None: - for name, _parts in t.suffix.leaves: - buf.append(f"\tdef {name.lower()}(self) -> '{root_name}':\n") - buf.append("\t\tassert ' # ' not in self.value\n") - buf.append(f"\t\treturn {root_name}(f'{{self.value}} # {name}')\n") buf.append('\n') diff --git a/support/tools/genvm-tool/genvm_tool/codegen/rst.py b/support/tools/genvm-tool/genvm_tool/codegen/rst.py index 6b495b71..9a380daa 100644 --- a/support/tools/genvm-tool/genvm_tool/codegen/rst.py +++ b/support/tools/genvm-tool/genvm_tool/codegen/rst.py @@ -67,16 +67,13 @@ def render(defs: list[Definition], rst_anchor_ns: str = '', **_opts) -> str: buf.append(f'Param: {param}\n\n') if path in d.docs: buf.append(d.docs[path].rstrip() + '\n\n') - if d.suffix is not None: - for name, _parts in d.suffix.leaves: - path = f'{model.DETAIL_HEAD} {name}' - buf.append( - f'.. _gvm-def-{ns}str-trie-detail-{_dash(d.name)}-{_dash(name)}:\n\n' - ) - buf.append(f'``{path}``\n') - buf.append('~' * (len(path) + 4) + '\n\n') - if path in d.docs: - buf.append(d.docs[path].rstrip() + '\n\n') + for path in model.enumerate_details(d.entries): + rst_name = path.replace(' # ', '-').replace('_', '-').replace(' ', '-') + buf.append(f'.. _gvm-def-{ns}str-trie-detail-{_dash(d.name)}-{rst_name}:\n\n') + buf.append(f'``{path}``\n') + buf.append('~' * (len(path) + 4) + '\n\n') + if path in d.docs: + buf.append(d.docs[path].rstrip() + '\n\n') # Sections are emitted blank-line-separated, so the last one leaves a trailing # blank line that `end-of-file-fixer` would strip — permanent codegen drift. return ''.join(buf).rstrip('\n') + '\n' diff --git a/support/tools/genvm-tool/genvm_tool/codegen/rust.py b/support/tools/genvm-tool/genvm_tool/codegen/rust.py index 649e4064..c0910e47 100644 --- a/support/tools/genvm-tool/genvm_tool/codegen/rust.py +++ b/support/tools/genvm-tool/genvm_tool/codegen/rust.py @@ -122,6 +122,12 @@ def _emit_struct(node: TrieNode, root_name: str, buf: list[str]) -> None: f' pub const fn val(&self) -> {root_name} {{ ' f'{root_name}(Cow::Borrowed("{val}")) }}\n' ) + for name in node.details: + val = ' '.join(node.parts) + buf.append( + f' pub const fn {name.lower()}(&self) -> {root_name} {{ ' + f'{root_name}(Cow::Borrowed("{val} # {name}")) }}\n' + ) buf.append(" pub const fn prefix_(&self) -> &'static str {\n") buf.append(f' "{" ".join(node.parts)}"\n') buf.append(' }\n') @@ -208,21 +214,6 @@ def _trie(t: StrTrie, buf: list[str]) -> None: ) buf.append('}\n\n') - if t.suffix is not None: - buf.append('#[rustfmt::skip]\n') - buf.append(f'impl {root_name} {{\n') - for name, _parts in t.suffix.leaves: - # A value carries at most one detail: the reader splits on the first - # separator, so a second one would be silently dropped. Checked in - # release too, so the invariant does not depend on the build -- these - # are error paths, and the scan is over a short fixed string. - buf.append( - f' pub fn {name.lower()}(self) -> Self {{ ' - f'assert!(!self.0.contains(" # "), "a value carries at most one detail"); ' - f'Self(Cow::Owned(format!("{{}} # {name}", self.0))) }}\n' - ) - buf.append('}\n\n') - _emit_is_valid(t, root_name, buf) diff --git a/support/tools/genvm-tool/unit_tests/test_codegen_detail_suffix.py b/support/tools/genvm-tool/unit_tests/test_codegen_detail_suffix.py index b0951ea0..6da77842 100644 --- a/support/tools/genvm-tool/unit_tests/test_codegen_detail_suffix.py +++ b/support/tools/genvm-tool/unit_tests/test_codegen_detail_suffix.py @@ -1,4 +1,4 @@ -"""A ``#`` trie entry becomes a detail suffix on the built value, not a path.""" +"""A ``#`` trie child declares details for its parent public path.""" import pytest from genvm_tool.codegen import go, model, python, rst, rust @@ -9,8 +9,15 @@ 'name': 'vm_error', 'values': [ 'timeout', - {'head': 'out_of', 'tail': ['memory']}, - {'head': '#', 'tail': ['internal', 'external']}, + { + 'head': 'out_of', + 'tail': [ + { + 'head': 'memory', + 'tail': [{'head': '#', 'tail': ['internal', 'external']}], + } + ], + }, ], } ] @@ -21,35 +28,31 @@ def defs(): return model.parse(DATA) -def test_suffix_is_kept_out_of_the_path_trie(defs): +def test_detail_is_scoped_to_its_parent_and_kept_out_of_paths(defs): (trie,) = defs assert [head for head, _ in trie.root.methods] == ['out_of'] assert [path for path, _ in model.enumerate_paths(trie.entries)] == [ 'timeout', 'out_of memory', ] - assert [name for name, _ in trie.suffix.leaves] == ['internal', 'external'] + memory = trie.root.methods[0][1].methods[0][1] + assert memory.details == ['internal', 'external'] -def test_rust_emits_detail_methods_on_the_value(defs): +def test_rust_emits_detail_methods_on_the_parent_builder(defs): out = rust.render(defs) for name in ('internal', 'external'): assert ( - f'pub fn {name}(self) -> Self ' - '{ assert!(!self.0.contains(" # "), "a value carries at most one detail"); ' - f'Self(Cow::Owned(format!("{{}} # {name}", self.0))) }}' in out + f'pub const fn {name}(&self) -> VmError ' + f'{{ VmError(Cow::Borrowed("out_of memory # {name}")) }}' in out ) -def test_every_backend_refuses_to_stack_two_details(defs): - assert out_has_guard(rust.render(defs)) - assert out_has_guard(python.render(defs)) - assert out_has_guard(go.render(defs)) - assert 'strings' in go.render(defs) - - -def out_has_guard(out: str) -> bool: - return out.count(' # ') >= 2 and 'at most one detail' in out or "' # ' not in" in out +def test_details_are_not_methods_on_the_built_value(defs): + rust_out = rust.render(defs).split('pub struct VmError')[1] + python_out = python.render(defs).split('class VmError:')[1] + assert 'fn internal' not in rust_out + assert 'def internal' not in python_out def test_a_detail_with_a_non_list_tail_is_rejected(): @@ -57,7 +60,12 @@ def test_a_detail_with_a_non_list_tail_is_rejected(): { 'type': 'str_trie', 'name': 'vm_error', - 'values': [{'head': '#', 'tail': [{'head': 'internal', 'tail': '$str'}]}], + 'values': [ + { + 'head': 'timeout', + 'tail': [{'head': '#', 'tail': [{'head': 'internal', 'tail': '$str'}]}], + } + ], } ] with pytest.raises(ValueError, match='nested'): @@ -70,25 +78,50 @@ def test_rust_is_valid_ignores_details(defs): assert '# internal' not in out.split('pub fn is_valid_')[1] -def test_python_emits_detail_methods_on_the_value(defs): +def test_python_emits_detail_methods_on_the_parent_builder(defs): out = python.render(defs) - assert "\tdef internal(self) -> 'VmError':\n" in out - assert "\t\treturn VmError(f'{self.value} # internal')\n" in out + assert "\tdef internal() -> 'VmError':\n" in out + assert "\t\treturn VmError('out_of memory # internal')\n" in out + + +def test_go_emits_detail_functions_for_the_parent_path(defs): + out = go.render(defs) + assert ( + 'func VmErrorOutOfMemoryInternal() VmError ' + '{ return "out_of memory # internal" }' in out + ) def test_rst_documents_details_under_the_trie(defs): out = rst.render(defs) - assert '``# internal``' in out - assert '``# external``' in out + assert '``out_of memory # internal``' in out + assert '``out_of memory # external``' in out -def test_a_nested_detail_is_rejected(): +def test_a_detail_may_not_have_children(): data = [ { 'type': 'str_trie', 'name': 'vm_error', - 'values': [{'head': '#', 'tail': [{'head': 'a', 'tail': ['b']}]}], + 'values': [ + { + 'head': 'timeout', + 'tail': [{'head': '#', 'tail': [{'head': 'a', 'tail': ['b']}]}], + } + ], } ] with pytest.raises(ValueError, match='nested'): model.parse(data) + + +def test_a_root_detail_is_rejected(): + data = [ + { + 'type': 'str_trie', + 'name': 'vm_error', + 'values': [{'head': '#', 'tail': ['internal']}], + } + ] + with pytest.raises(ValueError, match='public path'): + model.parse(data) diff --git a/tests/runner/genvm_tool_plugins/afl.py b/tests/runner/genvm_tool_plugins/afl.py index 10ded7ae..4a36f5f0 100644 --- a/tests/runner/genvm_tool_plugins/afl.py +++ b/tests/runner/genvm_tool_plugins/afl.py @@ -5,6 +5,7 @@ import functools import math import os +import re import shutil import signal import tempfile @@ -42,6 +43,14 @@ def _stop_live_fuzzers() -> None: 'AFL_TRY_AFFINITY', ) +# Ceiling for `-t`, whose auto-scaling stays below it. Chosen against the slowest +# committed seed rather than against the mean: what it has to survive is one +# entry of an existing corpus, on a core slower than the one that harvested it +EXEC_TIMEOUT_MS = 5000 + +# How AFL reports a seed it refused to fuzz because the target died on it +SEED_CRASH_RE = re.compile(r"Test case '([^']*)' results in a crash") + def register(ctx: genvm_tool.tests.stage.configuration.Context) -> None: ctx.run_parser.add_argument( @@ -151,6 +160,25 @@ def environment(base: dict[str, str]) -> dict[str, str]: return env +def crashing_seeds(out_dir: Path) -> list[str]: + """ + Committed inputs an instance found to crash the target while starting up. + + `-t` makes AFL skip a problematic seed instead of aborting, which is what a + corpus that only grows needs for a slow entry — but a crashing one is a + finding, and skipped it would be reported nowhere. Read back out of the logs + rather than prevented, since the two share the one switch. + """ + found: list[str] = [] + for log in sorted(out_dir.glob('*.log')): + for match in SEED_CRASH_RE.finditer(log.read_text(errors='replace')): + name = match.group(1) + _, _, original = name.rpartition('orig:') + if (original or name) not in found: + found.append(original or name) + return found + + def output_dir( shared: genvm_tool.tests.SharedContext, project_dir: Path, name: str ) -> Path: @@ -236,7 +264,6 @@ def fleet_steps( out_dir: Path, launcher: Command, target: Command, - extra_args: Command = (), wrap: Wrapper = lambda argv: argv, prepare_command: Command | None = None, status_command: Command | None = None, @@ -268,6 +295,12 @@ def command(role: str, instance: str) -> Command: # these targets, see the fuzzing explanation page '-c', '-', + # A ceiling on the auto-scaled per-exec timeout. AFL's own default + # is 1000ms, and a corpus entry above it aborts the whole run + # rather than being skipped; python targets run traced and a slow + # CI core is enough to cross it + '-t', + f'{EXEC_TIMEOUT_MS}+', role, instance, '-i', @@ -276,7 +309,6 @@ def command(role: str, instance: str) -> Command: out_dir, '-V', str(timeout(ctx)), - *extra_args, *target, ] ) @@ -402,6 +434,9 @@ async def report_findings( crashes = [crash for path in crash_dirs() for crash in saved_findings(path)] context = dict(result.context) + bad_seeds = crashing_seeds(out_dir) + if bad_seeds: + context['crashing_seeds'] = bad_seeds if crashes: context['crashes'] = [str(crash) for crash in crashes] if replay is not None: @@ -432,7 +467,7 @@ async def report_findings( context['fleet'] = f'{status_command[0]}: {error}' return genvm_tool.tests.test.Result( - passed=result.passed and not crashes, + passed=result.passed and not crashes and not bad_seeds, context=context, elapsed_seconds=result.elapsed_seconds, ) diff --git a/tests/runner/genvm_tool_plugins/cargo.py b/tests/runner/genvm_tool_plugins/cargo.py index 35884bf1..8e291806 100644 --- a/tests/runner/genvm_tool_plugins/cargo.py +++ b/tests/runner/genvm_tool_plugins/cargo.py @@ -611,7 +611,6 @@ def cargo_fuzz( out_dir=out_dir, launcher=['cargo-afl', 'afl', 'fuzz'], target=[fuzz_binary], - extra_args=['-t', '5000'], status_command=['cargo-afl', 'afl', 'whatsup', '-s', out_dir], # A persistent-mode target expects a forkserver, so replaying an input # means going through AFL rather than piping it into the binary. The diff --git a/tests/runner/genvm_tool_plugins/integration.py b/tests/runner/genvm_tool_plugins/integration.py index 20335dab..8c6b3745 100644 --- a/tests/runner/genvm_tool_plugins/integration.py +++ b/tests/runner/genvm_tool_plugins/integration.py @@ -720,7 +720,7 @@ def resolve_executor(address, state, advisory_major): balances={Address(k): v for k, v in single_conf.get('balances', {}).items()}, running_address=running_address, ctx=ctx, - resolve_callcontract_executor_hook=resolve_executor, + resolve_call_contract_executor_hook=resolve_executor, ) host.balances.setdefault(running_address, 0) @@ -756,7 +756,7 @@ def resolve_executor(address, state, advisory_major): default_message_fee_allocation = [ fees.DEFAULT_EXTERNAL_MESSAGE_ALLOC, fees.DEFAULT_INTERNAL_FIN_MESSAGE_ALLOC, - fees.DEFAULT_INTERNAL_ACC_MESSAGE_ALLOC, + fees.DEFAULT_INTERNAL_DEC_MESSAGE_ALLOC, ] message_fee_allocation: list[fees.MessageAllocationNode] = single_conf.get( @@ -826,7 +826,7 @@ def resolve_executor(address, state, advisory_major): f'nondet disagreement: {mock_host.nondet_disagreement_call_no}\n' ) - for k, v in res.result_storage_changes: + for k, v in res.result_storage_deltas: assert len(k) == 36 index = int.from_bytes(k[32:], byteorder='big') index *= 32 diff --git a/tests/runner/gvm_extra/mock_host.py b/tests/runner/gvm_extra/mock_host.py index b19b7110..26dc21d4 100644 --- a/tests/runner/gvm_extra/mock_host.py +++ b/tests/runner/gvm_extra/mock_host.py @@ -15,8 +15,8 @@ ) from origin.calldata import Address -type ResolveCallcontractExecutorHook = collections.abc.Callable[ - [Address, public_abi.StorageType, int], +type ResolveCallContractExecutorHook = collections.abc.Callable[ + [Address, public_abi.StorageView, int], bytes | None, ] @@ -87,7 +87,7 @@ def __init__( running_address: Address, ctx: Context, expected_hello_data: bytes = b'', - resolve_callcontract_executor_hook: ResolveCallcontractExecutorHook | None = None, + resolve_call_contract_executor_hook: ResolveCallContractExecutorHook | None = None, ): self.running_address = running_address self.path = path @@ -100,7 +100,7 @@ def __init__( self.balances = balances self.nondet_disagreement_call_no = None self.expected_hello_data = expected_hello_data - self.resolve_callcontract_executor_hook = resolve_callcontract_executor_hook + self.resolve_call_contract_executor_hook = resolve_call_contract_executor_hook self.ctx = ctx self._accept_task: asyncio.Task | None = None self._connection_tasks: list[asyncio.Task] = [] @@ -247,34 +247,39 @@ async def stop_connections(self) -> None: task.result() async def storage_read( - self, mode: public_abi.StorageType, account: bytes, slot: bytes, index: int, le: int + self, + mode: public_abi.StorageView, + address: bytes, + slot: bytes, + offset: int, + le: int, ) -> bytes: assert self.storage is not None - return self.storage.read(Address(account), slot, index, le) + return self.storage.read(Address(address), slot, offset, le) - async def resolve_callcontract_executor( + async def resolve_call_contract_executor( self, contract_address: Address, - state_mode: public_abi.StorageType, + state_mode: public_abi.StorageView, advisory_major: int, /, ) -> bytes | None: - if self.resolve_callcontract_executor_hook is None: + if self.resolve_call_contract_executor_hook is None: return None - return self.resolve_callcontract_executor_hook( + return self.resolve_call_contract_executor_hook( contract_address, state_mode, advisory_major, ) - async def remaining_fuel_as_gen(self) -> int: + async def get_remaining_time_fee_gen_wei(self) -> int: return 2**32 - async def eth_call(self, account: bytes, calldata: bytes, /) -> bytes: + async def external_call(self, address: bytes, calldata: bytes, /) -> bytes: raise HostException(host_fns.Errors.EVM_REVERTED) - async def consume_gas(self, gas: int): + async def consume_time_fee_gen_wei(self, time_fee_gen_wei: int): pass - async def get_balance(self, account: bytes) -> int: - return self.balances.get(Address(account), 0) + async def get_balance_gen_wei(self, address: bytes) -> int: + return self.balances.get(Address(address), 0) diff --git a/tests/runner/origin/base_host.py b/tests/runner/origin/base_host.py index 23debcd5..6817a38c 100644 --- a/tests/runner/origin/base_host.py +++ b/tests/runner/origin/base_host.py @@ -128,7 +128,7 @@ class Message(typing.TypedDict): chain_id: int value: typing.NotRequired[int] is_init: bool - datetime: typing.NotRequired[str] + transaction_timestamp: typing.NotRequired[str] class FingerprintFrame(typing.TypedDict): @@ -141,8 +141,8 @@ class ResultFingerprint(typing.TypedDict): module_instances: dict[str, typing.Any] -class EthSendInner(typing.TypedDict): - type: typing.Literal['EthSend'] +class ExternalMessageInner(typing.TypedDict): + type: typing.Literal['ExternalMessage'] address: Address calldata: bytes value: int @@ -151,12 +151,12 @@ class EthSendInner(typing.TypedDict): fee_params: fees.ExternalMessageParams -class PostMessageInner(typing.TypedDict): - type: typing.Literal['PostMessage'] +class InternalMessageInner(typing.TypedDict): + type: typing.Literal['InternalMessage'] address: Address calldata: gvm_calldata.Decoded value: int - on: typing.Literal['finalized', 'accepted'] + on: typing.Literal['finalized', 'decided'] message_fee: int receipt_fee: int fee_params: fees.InternalMessageParams @@ -166,12 +166,12 @@ class PostMessageInner(typing.TypedDict): use_balance: bool -class DeployContractInner(typing.TypedDict): - type: typing.Literal['DeployContract'] +class InternalDeployMessageInner(typing.TypedDict): + type: typing.Literal['InternalDeployMessage'] calldata: gvm_calldata.Decoded code: bytes value: int - on: typing.Literal['finalized', 'accepted'] + on: typing.Literal['finalized', 'decided'] salt_nonce: int message_fee: int receipt_fee: int @@ -182,18 +182,18 @@ class DeployContractInner(typing.TypedDict): use_balance: bool -class EmitEventInner(typing.TypedDict): - type: typing.Literal['EmitEvent'] +class EventInner(typing.TypedDict): + type: typing.Literal['Event'] topics: list[bytes] blob: dict[str, gvm_calldata.Decoded] storage_fee: int type ResultEmission = typing.Union[ - EthSendInner, - PostMessageInner, - DeployContractInner, - EmitEventInner, + ExternalMessageInner, + InternalMessageInner, + InternalDeployMessageInner, + EventInner, ] @@ -204,31 +204,31 @@ async def loop_enter(self, cancellation: asyncio.Event) -> socket.socket: ... @abc.abstractmethod async def storage_read( self, - mode: public_abi.StorageType, - account: bytes, + mode: public_abi.StorageView, + address: bytes, slot: bytes, - index: int, + offset: int, le: int, /, ) -> bytes: ... - async def resolve_callcontract_executor( + async def resolve_call_contract_executor( self, contract_address: Address, - state_mode: public_abi.StorageType, + state_mode: public_abi.StorageView, advisory_major: int, /, ) -> bytes | None: return None @abc.abstractmethod - async def consume_gas(self, gas: int, /) -> None: ... + async def consume_time_fee_gen_wei(self, time_fee_gen_wei: int, /) -> None: ... @abc.abstractmethod - async def eth_call(self, account: bytes, calldata: bytes, /) -> bytes: ... + async def external_call(self, address: bytes, calldata: bytes, /) -> bytes: ... @abc.abstractmethod - async def get_balance(self, account: bytes, /) -> int: ... + async def get_balance_gen_wei(self, address: bytes, /) -> int: ... @abc.abstractmethod - async def remaining_fuel_as_gen(self, /) -> int: ... + async def get_remaining_time_fee_gen_wei(self, /) -> int: ... @abc.abstractmethod async def notify_nondet_disagreement(self, call_no: int, /) -> None: ... @@ -249,7 +249,7 @@ async def read_contract_major(handler: IHost, message: Message) -> int: if message.get('is_init', False): return UNDEPLOYED_MAJOR octet = await handler.storage_read( - public_abi.StorageType.LATEST_NON_FINAL, + public_abi.StorageView.LATEST_DECIDED, message['contract_address'].as_bytes, ZERO_SLOT, ROOT_OFFSET_MAJOR, @@ -401,26 +401,26 @@ def emit_host_loop_stats(): match meth_id: case host_fns.Methods.STORAGE_READ: mode = await read_exact(1) - mode = public_abi.StorageType(mode[0]) - account = await read_exact(ACCOUNT_ADDR_SIZE) + mode = public_abi.StorageView(mode[0]) + address = await read_exact(ACCOUNT_ADDR_SIZE) slot = await read_exact(SLOT_ID_SIZE) - index = await recv_int() + offset = await recv_int() le = await recv_int() try: - res = await handler.storage_read(mode, account, slot, index, le) + res = await handler.storage_read(mode, address, slot, offset, le) assert len(res) == le except HostException as e: await send_all(bytes([e.error_code])) else: await send_all(bytes([host_fns.Errors.OK])) await send_all(res) - case host_fns.Methods.RESOLVE_CALLCONTRACT_EXECUTOR: + case host_fns.Methods.RESOLVE_CALL_CONTRACT_EXECUTOR: contract_address = Address(await read_exact(ACCOUNT_ADDR_SIZE)) - state_mode = public_abi.StorageType((await read_exact(1))[0]) + state_mode = public_abi.StorageView((await read_exact(1))[0]) advisory_major = await recv_int(1) try: - res = await handler.resolve_callcontract_executor( + res = await handler.resolve_call_contract_executor( contract_address, state_mode, advisory_major, @@ -436,39 +436,41 @@ def emit_host_loop_stats(): raise Exception( 'CONSUME_RESULT is not supported in this host loop implementation, use manager provided one' ) - case host_fns.Methods.CONSUME_FUEL: - gas = await recv_int(32) - await handler.consume_gas(gas) - case host_fns.Methods.ETH_CALL: - account = await read_exact(ACCOUNT_ADDR_SIZE) + case host_fns.Methods.CONSUME_TIME_FEE_GEN_WEI: + time_fee_gen_wei = await recv_int(32) + await handler.consume_time_fee_gen_wei(time_fee_gen_wei) + case host_fns.Methods.EXTERNAL_CALL: + address = await read_exact(ACCOUNT_ADDR_SIZE) calldata_len = await recv_int() calldata = await read_exact(calldata_len) try: - res = await handler.eth_call(account, calldata) + res = await handler.external_call(address, calldata) except HostException as e: await send_all(bytes([e.error_code])) else: await send_all(bytes([host_fns.Errors.OK])) await send_int(len(res)) await send_all(res) - case host_fns.Methods.GET_BALANCE: - account = await read_exact(ACCOUNT_ADDR_SIZE) + case host_fns.Methods.GET_BALANCE_GEN_WEI: + address = await read_exact(ACCOUNT_ADDR_SIZE) try: - res = await handler.get_balance(account) + res = await handler.get_balance_gen_wei(address) except HostException as e: await send_all(bytes([e.error_code])) else: await send_all(bytes([host_fns.Errors.OK])) await send_all(res.to_bytes(32, byteorder='little', signed=False)) - case host_fns.Methods.REMAINING_FUEL_AS_GEN: + case host_fns.Methods.GET_REMAINING_TIME_FEE_GEN_WEI: try: - res = await handler.remaining_fuel_as_gen() + time_fee_gen_wei = await handler.get_remaining_time_fee_gen_wei() except HostException as e: await send_all(bytes([e.error_code])) else: await send_all(bytes([host_fns.Errors.OK])) - await send_all(res.to_bytes(32, byteorder='little', signed=False)) + await send_all( + time_fee_gen_wei.to_bytes(32, byteorder='little', signed=False) + ) case host_fns.Methods.NOTIFY_NONDET_DISAGREEMENT: call_no = await recv_int() await handler.notify_nondet_disagreement(call_no) @@ -498,7 +500,7 @@ class ConsumedResult: result_kind: host_fns.ResultCode result_data: gvm_calldata.Decoded result_fingerprint: ResultFingerprint | None = None - result_storage_changes: list[tuple[bytes, bytes]] = field(default_factory=list) + result_storage_deltas: list[tuple[bytes, bytes]] = field(default_factory=list) result_emissions: list[ResultEmission] = field(default_factory=list) result_nondet_results: list[bytes] = field(default_factory=list) data_fees_remaining: list[int] = field(default_factory=list) @@ -547,7 +549,7 @@ def decode(cls, raw: typing.Any) -> 'ConsumedResult': result_kind=result_kind, result_data=decoded.get('data'), result_fingerprint=decoded.get('fingerprint'), - result_storage_changes=decoded.get('storage_changes', []), + result_storage_deltas=decoded.get('storage_deltas', []), result_emissions=decoded.get('emissions', []), result_nondet_results=decoded.get('nondet_results', []), data_fees_remaining=decoded.get('data_fees_remaining', []), @@ -567,7 +569,7 @@ class RunHostAndProgramRes: result_kind: host_fns.ResultCode result_data: gvm_calldata.Decoded result_fingerprint: ResultFingerprint | None - result_storage_changes: list[tuple[bytes, bytes]] + result_storage_deltas: list[tuple[bytes, bytes]] result_emissions: list[ResultEmission] result_nondet_results: list[bytes] data_fees_remaining: list[int] @@ -1190,7 +1192,7 @@ async def run_genvm( max_exec_mins = 20 if timeout is not None: max_exec_mins = int(max(max_exec_mins, (timeout * 1.5 + 59) // 60)) - timestamp = message.get('datetime', '2024-11-26T06:42:42.424242Z') + timestamp = message.get('transaction_timestamp', '2024-11-26T06:42:42.424242Z') deadline = _duration_string(timeout) host_genvm_id = typing.cast(str | None, request_extra.get('host_genvm_id')) if host_genvm_id is None: @@ -1379,7 +1381,7 @@ async def cancel_on_shutdown(): result_kind=consumed.result_kind, result_data=consumed.result_data, result_fingerprint=consumed.result_fingerprint, - result_storage_changes=consumed.result_storage_changes, + result_storage_deltas=consumed.result_storage_deltas, result_emissions=consumed.result_emissions, result_nondet_results=consumed.result_nondet_results, data_fees_remaining=consumed.data_fees_remaining, diff --git a/tests/runner/origin/fees.py b/tests/runner/origin/fees.py index 47ba4019..156748f8 100644 --- a/tests/runner/origin/fees.py +++ b/tests/runner/origin/fees.py @@ -45,7 +45,7 @@ class MessageAllocationNode(typing.TypedDict): call_key: bytes | None budget: int # Lifecycle the node matches against (only meaningful for internal messages). - on: typing.Literal['finalized', 'accepted'] + on: typing.Literal['finalized', 'decided'] fee_params: MessageAllocationNodeParams # Nested allocation subtree; the chain receives this flattened to # parent-pointer form. @@ -67,11 +67,11 @@ class MessageAllocationNode(typing.TypedDict): 'children': [], } -DEFAULT_INTERNAL_ACC_MESSAGE_ALLOC: MessageAllocationNode = { +DEFAULT_INTERNAL_DEC_MESSAGE_ALLOC: MessageAllocationNode = { 'budget': 2**200, 'recipient': None, 'call_key': None, - 'on': 'accepted', + 'on': 'decided', 'fee_params': { 'Internal': { 'execution_budget_per_round': 2**10, diff --git a/tests/runner/origin/host_fns.py b/tests/runner/origin/host_fns.py index 9715e1be..a000ad1a 100644 --- a/tests/runner/origin/host_fns.py +++ b/tests/runner/origin/host_fns.py @@ -9,13 +9,13 @@ class Methods(IntEnum): STORAGE_READ = 0 - CONSUME_FUEL = 1 - ETH_CALL = 2 - GET_BALANCE = 3 - REMAINING_FUEL_AS_GEN = 4 + CONSUME_TIME_FEE_GEN_WEI = 1 + EXTERNAL_CALL = 2 + GET_BALANCE_GEN_WEI = 3 + GET_REMAINING_TIME_FEE_GEN_WEI = 4 NOTIFY_NONDET_DISAGREEMENT = 5 CONSUME_RESULT = 6 - RESOLVE_CALLCONTRACT_EXECUTOR = 7 + RESOLVE_CALL_CONTRACT_EXECUTOR = 7 RUN_NESTED = 8 diff --git a/tests/runner/origin/public_abi.py b/tests/runner/origin/public_abi.py index abb7a496..41d7cf49 100644 --- a/tests/runner/origin/public_abi.py +++ b/tests/runner/origin/public_abi.py @@ -13,10 +13,10 @@ class ResultCode(IntEnum): VM_ERROR = 2 -class StorageType(IntEnum): +class StorageView(IntEnum): DEFAULT = 0 - LATEST_FINAL = 1 - LATEST_NON_FINAL = 2 + LATEST_FINALIZED = 1 + LATEST_DECIDED = 2 class EntryKind(IntEnum): @@ -135,32 +135,62 @@ def wasm_memory() -> 'VmError': def wasm_table() -> 'VmError': return VmError('out_of memory wasm_table') +class _VmErrorOutOfReceiptMessage: + @staticmethod + def val() -> 'VmError': + return VmError('out_of receipt message') + @staticmethod + def internal() -> 'VmError': + return VmError('out_of receipt message # internal') + class _VmErrorOutOfReceipt: @staticmethod def nondet_output() -> 'VmError': return VmError('out_of receipt nondet_output') @staticmethod - def message() -> 'VmError': - return VmError('out_of receipt message') - @staticmethod def event() -> 'VmError': return VmError('out_of receipt event') + @staticmethod + def message() -> '_VmErrorOutOfReceiptMessage': + return _VmErrorOutOfReceiptMessage() -class _VmErrorOutOfMessageFee: +class _VmErrorOutOfMessageFeeTotal: @staticmethod - def total() -> 'VmError': + def val() -> 'VmError': return VmError('out_of message_fee total') @staticmethod - def node() -> 'VmError': - return VmError('out_of message_fee node') + def internal() -> 'VmError': + return VmError('out_of message_fee total # internal') + @staticmethod + def external() -> 'VmError': + return VmError('out_of message_fee total # external') + +class _VmErrorOutOfMessageFeeAllocationBudget: + @staticmethod + def val() -> 'VmError': + return VmError('out_of message_fee allocation_budget') + @staticmethod + def internal() -> 'VmError': + return VmError('out_of message_fee allocation_budget # internal') + @staticmethod + def external() -> 'VmError': + return VmError('out_of message_fee allocation_budget # external') + +class _VmErrorOutOfMessageFee: + @staticmethod + def total() -> '_VmErrorOutOfMessageFeeTotal': + return _VmErrorOutOfMessageFeeTotal() + @staticmethod + def allocation_budget() -> '_VmErrorOutOfMessageFeeAllocationBudget': + return _VmErrorOutOfMessageFeeAllocationBudget() class _VmErrorOutOf: @staticmethod def storage() -> 'VmError': return VmError('out_of storage') @staticmethod - def vm_recursion() -> 'VmError': - return VmError('out_of vm_recursion') + def subvm_recursion() -> 'VmError': + return VmError('out_of subvm_recursion') @staticmethod def nondet_blocks() -> 'VmError': return VmError('out_of nondet_blocks') @@ -183,16 +213,27 @@ def receipt() -> '_VmErrorOutOfReceipt': def message_fee() -> '_VmErrorOutOfMessageFee': return _VmErrorOutOfMessageFee() -class _VmErrorFee: +class _VmErrorFeeNoMatchingAllocation: @staticmethod - def no_matching_node() -> 'VmError': - return VmError('fee no_matching_node') + def val() -> 'VmError': + return VmError('fee no_matching_allocation') + @staticmethod + def internal() -> 'VmError': + return VmError('fee no_matching_allocation # internal') + @staticmethod + def external() -> 'VmError': + return VmError('fee no_matching_allocation # external') + +class _VmErrorFee: @staticmethod def below_minimum() -> 'VmError': return VmError('fee below_minimum') @staticmethod def too_many_rounds() -> 'VmError': return VmError('fee too_many_rounds') + @staticmethod + def no_matching_allocation() -> '_VmErrorFeeNoMatchingAllocation': + return _VmErrorFeeNoMatchingAllocation() class _VmErrorEvm: @staticmethod @@ -276,12 +317,6 @@ def evm() -> '_VmErrorEvm': @staticmethod def invalid_contract() -> '_VmErrorInvalidContract': return _VmErrorInvalidContract() - def internal(self) -> 'VmError': - assert ' # ' not in self.value - return VmError(f'{self.value} # internal') - def external(self) -> 'VmError': - assert ' # ' not in self.value - return VmError(f'{self.value} # external') diff --git a/tests/system/cross-major-observability/assets/v0.3/observer.py b/tests/system/cross-major-observability/assets/v0.3/observer.py index eb4fd657..89d833ae 100644 --- a/tests/system/cross-major-observability/assets/v0.3/observer.py +++ b/tests/system/cross-major-observability/assets/v0.3/observer.py @@ -56,5 +56,7 @@ def attempt(action): @gl.public.view def debug_alias(self) -> int: - result = gl.vm.spawn_sandbox(lambda: 1, runner='py-genlayer:test') + import cloudpickle + + result = gl.vm.spawn_runner('py-genlayer:test', cloudpickle.dumps(lambda: 1)) return gl.vm.unpack_result(result) diff --git a/tests/system/cross-major-observability/test.py b/tests/system/cross-major-observability/test.py index 0055fa12..6ba21e3e 100644 --- a/tests/system/cross-major-observability/test.py +++ b/tests/system/cross-major-observability/test.py @@ -172,10 +172,10 @@ async def _execute( and result.result_kind == host_fns.ResultCode.RETURN ): assert mock_host.storage is not None - base._apply_storage_changes( + base._apply_storage_deltas( mock_host.storage, address, - result.result_storage_changes, + result.result_storage_deltas, ) return result finally: @@ -345,7 +345,7 @@ async def _assert_debug_alias(self): ) async def _assert_read_only_storage(self, permissions: str): - reads: list[tuple[Address, public_abi.StorageType]] = [] + reads: list[tuple[Address, public_abi.StorageView]] = [] leader, validator, sync = await self._lvs_extended( name=f'storage-{permissions}', line=2, @@ -362,7 +362,7 @@ async def _assert_read_only_storage(self, permissions: str): self.notes.append(('read-only nested storage', permissions)) async def _assert_self_recursion(self, budget: int): - expected_error = str(public_abi.VmError.out_of().vm_recursion()) + expected_error = str(public_abi.VmError.out_of().subvm_recursion()) leader, validator, sync = await self._lvs_extended( name=f'self-recursion-{budget}', line=3, @@ -407,8 +407,8 @@ async def _assert_deep_nesting(self, depth: int): if depth > CROSS_MAJOR_RECURSION: for result in (leader, validator, sync): assert result.result_kind == host_fns.ResultCode.VM_ERROR, result - assert result.result_data == 'out_of vm_recursion', (depth, result) - outcome: int | str = 'out_of vm_recursion' + assert result.result_data == 'out_of subvm_recursion', (depth, result) + outcome: int | str = 'out_of subvm_recursion' else: expected = sum(3 if index % 2 == 0 else 2 for index in range(depth + 1)) for result in (leader, validator, sync): diff --git a/tests/system/cross-major/assets/v0.3/state_caller.py b/tests/system/cross-major/assets/v0.3/state_caller.py index c612dce2..e21293ea 100644 --- a/tests/system/cross-major/assets/v0.3/state_caller.py +++ b/tests/system/cross-major/assets/v0.3/state_caller.py @@ -10,8 +10,8 @@ def __init__(self): @gl.public.view def call(self, target: Address, final: bool) -> int: mode = ( - gl.vm.public_abi.StorageType.LATEST_FINAL + gl.vm.public_abi.StorageView.LATEST_FINALIZED if final - else gl.vm.public_abi.StorageType.LATEST_NON_FINAL + else gl.vm.public_abi.StorageView.LATEST_DECIDED ) return gl.contract.get_at(target).view(state=mode).answer() diff --git a/tests/system/cross-major/test.py b/tests/system/cross-major/test.py index 5f57b641..d1f55bed 100644 --- a/tests/system/cross-major/test.py +++ b/tests/system/cross-major/test.py @@ -96,13 +96,14 @@ def _wat_data(data: bytes) -> str: return ''.join(f'\\{byte:02x}' for byte in data) -def _loop_wat(target: Address) -> str: +def _loop_wat(target: Address, *, caller_line: typing.Literal[2, 3]) -> str: + storage_field = 'storage_view' if caller_line == 3 else 'state' call = gvm_calldata.encode( { 'CallContract': { 'address': target, 'calldata': {}, - 'state': public_abi.StorageType.LATEST_NON_FINAL, + storage_field: public_abi.StorageView.LATEST_DECIDED, } } ) @@ -179,7 +180,7 @@ def _message(address: Address, *, is_init: bool) -> base_host.Message: 'chain_id': 61999, 'value': 0, 'is_init': is_init, - 'datetime': TIMESTAMP, + 'transaction_timestamp': TIMESTAMP, } @@ -198,7 +199,7 @@ def _assert_hashes_agree(label: str, runs: list[tuple[str, typing.Any]]) -> None ) -def _apply_storage_changes( +def _apply_storage_deltas( storage: MockStorage, address: Address, changes: list[tuple[bytes, bytes]], @@ -310,10 +311,10 @@ async def _run_all(self): await self._deploy(line, address, code) if 'loop-v03' in self.case.fixtures: loop_v03 = await self._compile_wat( - _loop_wat(ADDR_LOOP_V02), work_dir / 'loop-v03' + _loop_wat(ADDR_LOOP_V02, caller_line=3), work_dir / 'loop-v03' ) loop_v02 = await self._compile_wat( - _loop_wat(ADDR_LOOP_V03), work_dir / 'loop-v02' + _loop_wat(ADDR_LOOP_V03, caller_line=2), work_dir / 'loop-v02' ) self._replace_code(ADDR_LOOP_V03, LOOP_V03, loop_v03) self._replace_code(ADDR_LOOP_V02, LOOP_V02, loop_v02) @@ -332,7 +333,7 @@ async def _new_host( name: str, running_address: Address, resolve_hook=None, - read_log: list[tuple[Address, public_abi.StorageType]] | None = None, + read_log: list[tuple[Address, public_abi.StorageView]] | None = None, host_fuel: int | None = None, ) -> MockHost: ctx = _TestContext(self.case.shared.logger) @@ -349,22 +350,24 @@ async def _new_host( storage_path_post=self.storage_path, balances={}, running_address=running_address, - resolve_callcontract_executor_hook=resolve_hook, + resolve_call_contract_executor_hook=resolve_hook, ) if read_log is not None: original = host.storage_read - async def logged(mode, account, slot, index, le, /): - read_log.append((Address(account), mode)) - return await original(mode, account, slot, index, le) + async def logged(mode, address, slot, offset, le, /): + read_log.append((Address(address), mode)) + return await original(mode, address, slot, offset, le) host.storage_read = logged # type: ignore[method-assign] if host_fuel is not None: - async def remaining_fuel_as_gen() -> int: + async def get_remaining_time_fee_gen_wei() -> int: return host_fuel - host.remaining_fuel_as_gen = remaining_fuel_as_gen # type: ignore[method-assign] + host.get_remaining_time_fee_gen_wei = ( # type: ignore[method-assign] + get_remaining_time_fee_gen_wei + ) return host async def _execute( @@ -382,7 +385,7 @@ async def _execute( is_sync: bool = True, leader_nondet_results: list[bytes] | None = None, apply_changes: bool = True, - read_log: list[tuple[Address, public_abi.StorageType]] | None = None, + read_log: list[tuple[Address, public_abi.StorageView]] | None = None, host_fuel: int | None = None, hook_cross_contract_calls: bool = True, ): @@ -419,10 +422,10 @@ async def _execute( ) if apply_changes and result.result_kind == host_fns.ResultCode.RETURN: assert mock_host.storage is not None - _apply_storage_changes( + _apply_storage_deltas( mock_host.storage, address, - result.result_storage_changes, + result.result_storage_deltas, ) return result finally: @@ -549,7 +552,7 @@ def resolve(address, _state, _major): line=3, address=ADDR_CALLER_V03, host_path=mock_host.path, - deadline='30s' if mode == 'cancel' else '10s', + deadline='30s' if mode == 'cancel' else '3s', ) ) await asyncio.wait_for(nested_resolved.wait(), timeout=10) @@ -752,16 +755,16 @@ async def _assert_route_is_hash_invariant(self): async def _assert_state_mode_crosses( self, final: bool, - expected: public_abi.StorageType, + expected: public_abi.StorageView, ): """ The caller's state mode selects which chain view the callee reads, so the boundary must hand the callee exactly the mode the in-process route would have used.""" - modes: dict[bool, set[public_abi.StorageType]] = {} + modes: dict[bool, set[public_abi.StorageView]] = {} for nested in (False, True): - resolved: list[public_abi.StorageType] = [] - read_log: list[tuple[Address, public_abi.StorageType]] = [] + resolved: list[public_abi.StorageView] = [] + read_log: list[tuple[Address, public_abi.StorageView]] = [] def resolve(address, state, _major, nested=nested, resolved=resolved): if address != ADDR_CALLEE_V03: @@ -926,7 +929,9 @@ def resolve(address, _state, advisory_major): len(resolved), resolved[-3:], ) - assert result.result_data == str(public_abi.VmError.out_of().vm_recursion()), result + assert result.result_data == str(public_abi.VmError.out_of().subvm_recursion()), ( + result + ) assert len(resolved) == 2, len(resolved) # Neither line can report a meaningful major: v0.3 forwards the raw root # byte and v0.2 has no such byte at all, so both send the unwritten @@ -985,13 +990,13 @@ def collect( 'state-mode-nonfinal', '_assert_state_mode_crosses', ('state-caller-v03', 'callee-v03'), - (False, public_abi.StorageType.LATEST_NON_FINAL), + (False, public_abi.StorageView.LATEST_DECIDED), ), ( 'state-mode-final', '_assert_state_mode_crosses', ('state-caller-v03', 'callee-v03'), - (True, public_abi.StorageType.LATEST_FINAL), + (True, public_abi.StorageView.LATEST_FINALIZED), ), ('deep-chain', '_assert_deep_chain', ('chain-v03', 'chain-v02'), ()), *[ diff --git a/tests/system/make_zip/test.py b/tests/system/make_zip/test.py new file mode 100644 index 00000000..152ae899 --- /dev/null +++ b/tests/system/make_zip/test.py @@ -0,0 +1,214 @@ +""" +make-zip.py system test. + +The writer feeds the executor on every other run — all packaged runners are +built by it, so `Archive::from_zip_bytes` accepting its output is already +covered by the integration suite. What nothing covered is the two properties +that suite cannot see: that the bytes are reproducible, and that a *different* +implementation reads the archive the same way. Runner ids are content hashes, +so a drift in either is consensus-visible. +""" + +import io +import importlib.util +import json +import os +import subprocess +import sys +import zipfile +from pathlib import Path + +import genvm_tool.tests +from genvm_tool.tests.test import Result + +FIXTURE = { + 'runner.json': b'{ "StartWasm": "main.wasm" }', + 'main.wasm': b'\x00asm\x01\x00\x00\x00', + 'lib/mod.py': b'VALUE = 1\n', +} +PYC_NAME = importlib.util.cache_from_source('lib/mod.py', optimization='') +IGNORED_FIXTURE = { + PYC_NAME: b'stale bytecode', + 'lib/stale.pyo': b'stale optimized bytecode', +} + + +def _build(script: Path, work: Path, out: Path) -> None: + """ + Lay out the tree make-zip.py expects (a `scripts/` dir plus exactly one + source dir) and run it.""" + pkg = work / 'pkg' + for name, contents in (FIXTURE | IGNORED_FIXTURE).items(): + path = pkg / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(contents) + scripts = work / 'scripts' + scripts.mkdir(parents=True, exist_ok=True) + (scripts / 'make-zip.py').write_bytes(script.read_bytes()) + + env = dict(os.environ) + env['out'] = str(out) + subprocess.run( + [sys.executable, 'scripts/make-zip.py'], + cwd=work, + env=env, + check=True, + capture_output=True, + ) + + +def _check_add_file_edges(work: Path, out: Path) -> None: + env = dict(os.environ) + env['out'] = str(out) + subprocess.run( + [ + sys.executable, + '-c', + """ +import runpy + +namespace = runpy.run_path('scripts/make-zip.py') +add_file = namespace['add_file'] +add_file(namespace['importlib'].util.cache_from_source('lib/mod.py'), b'stale') +add_file('lib/stale.pyo', b'kept', skip_pyc=False) +add_file('lib/stale.pyo', b'stale') +try: + add_file('runner.json', b'') +except KeyError as exc: + assert 'runner.json' in str(exc), exc +else: + raise AssertionError('duplicate entry did not raise KeyError') +""", + ], + cwd=work, + env=env, + check=True, + capture_output=True, + ) + + +def _add_case( + ctx: genvm_tool.tests.stage.collection.Context, + *, + script: Path, + artifacts_dir: Path, +) -> None: + desc = genvm_tool.tests.test.Description('tests/system/make_zip/writer').with_tags( + frozenset({'integration', 'feature-runner-zip'}) + ) + + async def validate(previous_results): + first = artifacts_dir / 'first.zip' + second = artifacts_dir / 'second.zip' + _build(script, artifacts_dir / 'run-a', first) + _build(script, artifacts_dir / 'run-b', second) + _check_add_file_edges(artifacts_dir / 'run-a', artifacts_dir / 'collision.zip') + + if first.read_bytes() != second.read_bytes(): + return Result( + passed=False, + context={'reason': 'writer is not byte-deterministic'}, + elapsed_seconds=0, + ) + + raw = first.read_bytes() + # CPython is the independent reader: it validates CRCs and both headers, + # so agreement here is what rules out the local/central divergence class. + with zipfile.ZipFile(io.BytesIO(raw)) as zf: + if zf.testzip() is not None: + return Result( + passed=False, + context={'reason': 'CPython rejected an entry', 'entry': zf.testzip()}, + elapsed_seconds=0, + ) + names = sorted(zf.namelist()) + contents = {name: zf.read(name) for name in names} + infos = {name: zf.getinfo(name) for name in names} + + if contents.get(PYC_NAME) in (None, IGNORED_FIXTURE[PYC_NAME]): + return Result( + passed=False, + context={'reason': 'stale bytecode was not replaced', 'entry': PYC_NAME}, + elapsed_seconds=0, + ) + if 'lib/stale.pyo' in contents: + return Result( + passed=False, + context={'reason': 'stale optimized bytecode was included'}, + elapsed_seconds=0, + ) + + for name, expected in FIXTURE.items(): + if name == 'runner.json': + # The writer normalizes runner.json; compare parsed, not raw. + if json.loads(contents.get(name, b'null')) != json.loads(expected): + return Result( + passed=False, + context={'reason': 'runner.json content differs', 'entry': name}, + elapsed_seconds=0, + ) + continue + if contents.get(name) != expected: + return Result( + passed=False, + context={'reason': 'entry content differs', 'entry': name}, + elapsed_seconds=0, + ) + + for name, info in infos.items(): + if info.compress_type != zipfile.ZIP_STORED: + return Result( + passed=False, + context={'reason': 'entry is not stored', 'entry': name}, + elapsed_seconds=0, + ) + # Pinned so the bytes cannot pick up anything host-derived. + if (info.create_system, info.external_attr, info.flag_bits) != (0, 0, 0): + return Result( + passed=False, + context={ + 'reason': 'host-derived header field leaked', + 'entry': name, + 'create_system': info.create_system, + 'external_attr': info.external_attr, + 'flag_bits': int(info.flag_bits), + }, + elapsed_seconds=0, + ) + if info.date_time != (1980, 1, 1, 0, 0, 0): + return Result( + passed=False, + context={ + 'reason': 'mtime leaked into the archive', + 'entry': name, + 'date_time': info.date_time, + }, + elapsed_seconds=0, + ) + + return Result(passed=True, context={'entries': len(names)}, elapsed_seconds=0) + + ctx.add_case( + genvm_tool.tests.test.StepsCase( + description=desc, + steps=[genvm_tool.tests.exec.step.PythonFunction(validate)], + ) + ) + + +def collect(ctx: genvm_tool.tests.stage.collection.Context) -> None: + script = ( + ctx.shared.root_dir + / 'executors' + / 'v0.3.x' + / 'runners' + / 'support' + / 'scripts' + / 'make-zip.py' + ) + if not script.exists(): + raise FileNotFoundError(f'make-zip.py not found at {script}') + + artifacts_dir = ctx.shared.artifacts_dir / 'make_zip' + artifacts_dir.mkdir(parents=True, exist_ok=True) + _add_case(ctx, script=script, artifacts_dir=artifacts_dir)