diff --git a/.agents/skills/build/SKILL.md b/.agents/skills/build/SKILL.md index d4d813b0..b37e2184 100644 --- a/.agents/skills/build/SKILL.md +++ b/.agents/skills/build/SKILL.md @@ -10,8 +10,7 @@ read it first). Related: `building/runners.md`, `releasing/release-build.md`, Claude-specific: - Build binaries with - `bash .agents/skills/build/scripts/run-ninja.sh -C build all/bin` instead of - raw ninja — it is silent on success and prints output only on failure, which - saves tokens. + `bash -o pipefail -c 'ninja -C build all/bin 2>&1 | tee "$TMPDIR/ninja.log" | tail -n 40'`; + on failure grep the log for `error` instead of rebuilding to see it again - See also: `/submodules` (multi-repo commits, `?submodules=1`), `/test`, `/macos` (never build runners natively on macOS). diff --git a/.agents/skills/build/scripts/run-ninja.sh b/.agents/skills/build/scripts/run-ninja.sh deleted file mode 100644 index 5c7d935e..00000000 --- a/.agents/skills/build/scripts/run-ninja.sh +++ /dev/null @@ -1,12 +0,0 @@ -mkdir -p "$TMPDIR" -FILE_1=$(mktemp) -ninja "$@" > "$FILE_1" 2>&1 -STATUS=$? -if [ $STATUS -ne 0 ]; then - ninja "$@" 2>&1 - STATUS=$? -else - cat "$FILE_1" -fi -rm "$FILE_1" -exit $STATUS diff --git a/docs/contributing/howto/committing/runners.md b/docs/contributing/howto/committing/runners.md index 927cece0..c8d20b36 100644 --- a/docs/contributing/howto/committing/runners.md +++ b/docs/contributing/howto/committing/runners.md @@ -14,6 +14,8 @@ Before committing: 2. Set every `"test"` hash in `current.nix` 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 + Ensure linters and formatters have passed before regenerating hashes; + any later source edits invalidate the hashes and require regeneration 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 diff --git a/docs/contributing/howto/pr.md b/docs/contributing/howto/pr.md index 87b52e50..8953588b 100644 --- a/docs/contributing/howto/pr.md +++ b/docs/contributing/howto/pr.md @@ -78,6 +78,7 @@ does fixing a defect you find in your own diff | --- | --- | | Panel does nothing | missing `ci-safe` | | `full tests have not run` | tick *Force run full tests* or comment `/genvm-run-tests` | +| *GenVM full* still red or cancelled after `/genvm-run-tests` | expected: the command runs in its own check suite and cancels the automatic run; follow the linked run instead | | `... commit(s) behind base` | rebase, `push --force-with-lease` | | `pinned commit ... is not on the executor repo` | push it, or tick *Provision executor PRs* | | Approval revoked | re-request approval for the current head | diff --git a/docs/website/src/impl-spec/04-fees.rst b/docs/website/src/impl-spec/04-fees.rst index 627d1df6..c0724b85 100644 --- a/docs/website/src/impl-spec/04-fees.rst +++ b/docs/website/src/impl-spec/04-fees.rst @@ -68,7 +68,8 @@ Name ``attrs`` keys ``rotationsCount``, ``calldataLength``, ``codeLength``, ``subtreeLength`` ``nondet_output`` ``outputLength`` -``message_fee`` ``isInternal``, ``matchedFeeParams``: an object with +``message_fee`` ``isInternal``, ``skipBudgetFloor``, ``matchedFeeParams``: + an object with ``leaderTimeunitsAllocation``, ``validatorTimeunitsAllocation``, ``executionBudgetPerRound``, ``rotations`` (array) @@ -290,9 +291,9 @@ phase mismatch on an exact allocation does not fall through to a wildcard. Chain keys are unique across phases; only synthetic recipient-wildcard entries may select different fee parameters by phase. For external messages, an exact allocation without room for the reservation spills to -the per-recipient ``call_key`` wildcard. If neither key has a present allocation, the external message uses -the legacy unallocated path and consumes only its receipt cost. Existing but exhausted -candidates yield an allocation-budget error. +the per-recipient ``call_key`` wildcard. If no candidate matches, the external +message uses the legacy unallocated path and consumes only its receipt cost. +Existing but exhausted candidates yield an allocation-budget error. The default v0.3 fee expressions reject external reservations and receipts with ``fee below_minimum`` unless ``node.lockedReceiptGasPrice`` is positive, including @@ -310,12 +311,20 @@ unallocated path. Synthetic recipient wildcards match independently of ``budget` zero there means an exhausted allocation. ``null`` removes the per-allocation cap while keeping the execution's fee buckets. -The host must preserve every existing pinned key and must not add recipient -wildcards to a pinned tree. An empty list restricts internal -pool-funded emissions. Open-pool and view executions may supply synthetic recipient -wildcards with concrete fee parameters and phase, and optionally uncapped budgets. -The executor conservatively treats each emission as novel; remaining allowances -alone do not identify previously delivered occurrences. +The host MUST preserve every existing pinned key and MUST NOT add recipient +wildcards to a pinned tree, except for the unmatched-external guard. It MUST append +that guard to every pinned tree and send it alone for a closed tree. The guard is an +ordinary external recipient and call-key wildcard with zero budget, ``finalized`` +phase, unit gas limit, maximum gas price, zero ``children_budget`` and an empty +``subtree``. An unmatched external message therefore resolves the guard as an +exhausted candidate and fails with +:ref:`gvm-def-str-trie-detail-vm-error-out-of-message-fee-allocation-budget-external` +instead of reaching the receipt-only path that consensus rejects when +``hasAllocations`` is true. Open-pool and allocation-absent executions MUST NOT +receive this guard. They may use ordinary synthetic recipient wildcards with +concrete fee parameters and phase, and optionally uncapped budgets. The executor +conservatively treats each emission as novel; remaining allowances alone do not +identify previously delivered occurrences. Funding modes ~~~~~~~~~~~~~~ @@ -333,21 +342,47 @@ An outgoing internal message is funded one of two ways: ``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 + entirely. The primary fee is metered from the guest-supplied ``fee_params``. + A positive ``descendants`` integer emits an Open grant; a non-empty allocation + list emits a Pinned grant whose budget is the checked sum of root budgets. + The child's ``declaredBudget`` is the primary fee plus that descendant budget, + reserved once from the emitting contract's balance (jointly with ``value``; insufficient balance yields ``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. + allocation subtree contains the canonical grant envelope. Absent, null, zero + and empty-list descendants emit an empty subtree and the Closed policy. -For either phase, an internal message therefore declares:: +For either phase, an internal message therefore declares ``P + D``, where +``P`` is its minimum primary fee and ``D`` is its authored descendant budget:: - minPrimaryFees(feeParams) + sum(directChildAllocation.budget) + minPrimaryFees(feeParams) + descendantBudget -For balance funding the sum is zero. The primary fee already covers the child's +For balance funding the sum is the authored descendant grant budget. The +primary fee already covers the child's configured lifecycle, including appeals; the remainder becomes the child's -message-fee bucket. An internal message whose declared amount is zero is rejected -with ``fee below_minimum`` on both funding paths. External messages declare zero +message-fee bucket. A zero declared budget is rejected with +``fee below_minimum`` on both funding paths. External messages declare zero + +The v0.3 executor currently uses these local stand-ins for transaction-pinned +host inputs that are not available yet: + +- The existing local primary-fee calculation supplies each child quote +- The transaction-pinned minimum external gas limit and per-round allocation + budget floor are not available, so both grant-node admission checks are + skipped. The enclosing child's existing local floor check remains active +- The descendant depth cap equals the 24-node cap + +The Python SDK takes ``use_balance: UseBalanceParams | None`` on calls, +transfers and deploys. ``None`` selects sender funding; the dataclass holds +required ``fee_params`` and optional ``descendants``. Its allocation objects +retain ``children`` for authoring. The SDK walks that tree iteratively in +pre-order and sends a flat array with backward ``parent_index`` references + +The Rust SDK decodes that array with ``LenLimitedVec<1024, AllocationNode>`` +before decoding its elements. The Python flattener has the same 1,024-element +limit, including for cyclic input. Grant admission separately enforces the +24-node limit, parent topology, duplicate sibling keys and budget inequalities The v0.3 primary reserve includes successful-appellant profit for each configured appeal slot. The bond is the next normal round's time-unit cost, including its diff --git a/docs/website/src/impl-spec/appendix/manager-api.yaml b/docs/website/src/impl-spec/appendix/manager-api.yaml index 104c85ea..41c80841 100644 --- a/docs/website/src/impl-spec/appendix/manager-api.yaml +++ b/docs/website/src/impl-spec/appendix/manager-api.yaml @@ -1081,8 +1081,13 @@ components: unchanged and meters their length. External and open allocations without a committed subtree use empty bytes. children_budget and subtree are required on every entry, including allocations without descendants. - Omit allocations absent on-chain. Recipient wildcards are - synthetic entries for open-pool or view execution, not chain allocations + Omit allocations absent on-chain. Recipient wildcards are synthetic entries, + not chain allocations. The host MUST append an unmatched-external guard to + every pinned tree and send it alone for a closed tree: recipient and call_key + null, budget zero, on finalized, External gas_limit one and max_gas_price the + maximum uint256, children_budget zero and an empty subtree. It is an ordinary + exhausted candidate. Open-pool and allocation-absent executions MUST omit the + guard; open-pool and view execution may use ordinary wildcards record_actions: type: array items: 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 15dee7ae..d6930da2 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 @@ -26,6 +26,29 @@ Requirements #. :ref:`gvm-perm-send-messages` #. Sufficient contract balance for value transfer +Semantics +~~~~~~~~~ + +External messages are emitted for finalization. GenVM derives their call key from +the first four calldata bytes, padded with zeros to 32 bytes; calldata shorter than +four bytes uses the all-zero key + +GenVM orders matching candidates from the non-zero exact ``(address, call key)`` +allocation through the same address's call-key wildcard to host-provided recipient +wildcards. A zero-budget exact allocation is absent and therefore permits broader +candidates. GenVM selects the first candidate that can fund the reservation. If +candidates exist but none can fund it, the :term:`sub-VM` exits with +:ref:`gvm-def-str-trie-detail-vm-error-out-of-message-fee-allocation-budget-external` + +If no candidate matches, the message uses the legacy unallocated path and consumes +only its receipt cost. Hosts MUST append an exhausted external recipient and +call-key wildcard to every pinned allocation tree, and supply that wildcard alone +for a closed tree. It has zero budget, ``finalized`` phase, unit gas limit, maximum +gas price, zero child budget and an empty subtree. This ordinary exhausted candidate +makes an otherwise unmatched external message exit with the allocation-budget error +above, as required when consensus marks the execution as having allocations. Hosts +MUST NOT supply this guard for open-pool or allocation-absent executions + ``ExternalCall`` Message ------------------------ @@ -122,7 +145,8 @@ Payload "value": U256, // Wei to transfer "on": String, // "finalized" or "decided" "use_balance": Bool, // optional (default false), see below - "fee_params": FeeParams // optional (default absent), required iff use_balance + "fee_params": FeeParams, // optional (default absent), required iff use_balance + "descendants": Descendants // optional (default absent), see below } } @@ -133,6 +157,7 @@ Requirements #. :ref:`gvm-perm-send-messages` #. Sufficient contract balance for value transfer #. When ``use_balance`` is set: :ref:`gvm-perm-use-balance-for-message-fees` and ``fee_params`` +#. ``descendants`` is absent unless ``use_balance`` is set #. ``calldata`` satisfies :ref:`gvm-def-contract-call-conv` Allocation-funded fees @@ -175,26 +200,90 @@ transaction's fee configuration and mirrors the chain's "receipt_fee_max_gas_price": U256 // receipt price cap (revert guard) } +``descendants`` configures the emitted child's descendant funding policy: + +.. code-block:: + + Descendants = Null | U256 | [Allocation] + InternalAllocation { + "parent_index": U256, + "recipient": Address, + "call_key": Bytes32, + "budget": U256, + "fee_params": FeeParams, + "on": String + } + ExternalAllocation { + "parent_index": U256, + "recipient": Address, + "call_key": Bytes32, + "budget": U256, + "fee_params": { + "gas_limit": U256, + "max_gas_price": U256 + } + } + +Absent, null, zero and an empty list select Closed and emit no grant bytes. A +positive integer selects Open with that descendant budget. A non-empty list +selects Pinned; its descendant budget is the checked sum of root budgets. +The list is flat and preserves authored order. A root's ``parent_index`` is +the maximum U256 value; other indices must refer to an earlier Internal +allocation. ``children`` is not a wire field. ``"decided"`` maps to acceptance-phase +authorization and ``"finalized"`` to finalization-phase authorization + +These Closed spellings apply to balance-funded messages. On sender-funded +messages, the field must be absent or null; a raw present zero or empty list is +rejected with ``Inval`` + +A Pinned grant obeys all of the following: + +#. Every node has a non-zero ``budget`` +#. The tree contains at most 24 nodes and has depth at most 24. An internal + node's encoded fee parameters are at most 1,024 bytes, which permits at most + 22 ``rotations`` entries +#. External allocations occur only at the root. Their ``gas_limit`` and + ``max_gas_price`` are non-zero, and their budget is a positive multiple of + ``gas_limit * max_gas_price``. They authorize only finalization-phase + emission +#. Siblings have unique ``(message type, recipient, call_key)`` keys. The + ``on`` phase is not part of the key; the same key under different parents is + permitted +#. For each internal node *i*, let *P_i* be its primary fee quote and *C_i* the + sum of its direct children's budgets. Its budget is at least + ``(P_i + C_i) * mult_i``. ``mult_i`` is the parent allocation's appeal-round + count plus 1 when ``on`` is ``"decided"``, and 1 otherwise. For a root, the + emitted child's appeal-round count is used + Semantics: -- The fee is metered from ``fee_params`` and that metered amount becomes the child - transaction's ``declaredBudget`` — the contract balance is the only bound. The +- The fee is metered from ``fee_params``. The child transaction's + ``declaredBudget`` is that primary reserve plus the descendant budget; the + contract balance is the only bound. The consensus term is charged at the guest's ``max_price_gen_per_time_unit`` cap (matching the chain's ``minMessagePrimaryFees``), not the node's live ``genPerTimeUnit``, so the fee scales with the cap. The consensus developer and DAO gross-up applies to this time-unit portion but not the execution budget - 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; +- The contract must be able to cover ``value + declaredBudget`` from its balance; 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. +- Closed is fail-closed. Open and Pinned carry a canonical grant in the emitted + allocation subtree +- A zero ``declaredBudget`` is rejected with + :ref:`gvm-def-str-trie-value-vm-error-fee-below-minimum`; this applies exactly + when the primary reserve plus descendant budget is zero ``fee_params`` is validated before metering. The following are rejected with ``Inval``: - ``use_balance`` without ``fee_params``, or ``fee_params`` without ``use_balance``. +- Non-null ``descendants`` without ``use_balance``, malformed allocation shapes, and + internal allocation fee parameters that fail the checks below - Empty ``rotations`` (``appealRounds`` would underflow). +- Exactly one of ``leader_time_units_allocation`` and + ``validator_time_units_allocation`` being zero. Both zero and both non-zero + are valid shapes; later metering still enforces configured timeout bounds - A zero ``max_price_gen_per_time_unit``, ``storage_fee_max_gas_price`` or ``receipt_fee_max_gas_price`` (the chain reverts ``FeeValueMustBeNonZero`` at reveal). @@ -205,14 +294,26 @@ Semantics: ``validator_time_units_allocation``, each ``rotations`` entry) below 2\ :sup:`32`. These bounds keep the metered floor within ``U256``. +Pinned node/depth limits, external-child placement and duplicate sibling keys use +:ref:`gvm-def-str-trie-value-vm-error-fee-descendant-grant-tree`. Zero node +budgets, arithmetic overflow and a failure of the internal-node budget +inequality use +:ref:`gvm-def-str-trie-value-vm-error-fee-descendant-grant-budget`. Invalid +external allocation parameters use +:ref:`gvm-def-str-trie-value-vm-error-fee-descendant-grant-external`. +Validation completes before fee consumption, balance reservation or emission + Metering additionally enforces node-configured bounds, surfaced as ``VMError``\ s: -- :ref:`gvm-def-str-trie-value-vm-error-fee-below-minimum` — either a non-zero - ``execution_budget_per_round`` below ``node.messageBudgetFloor`` (the chain's - ``BudgetTooLow``), or — unless both time-unit allocations are zero — a leader - allocation outside ``node.minProposeTimeout`` through +- :ref:`gvm-def-str-trie-value-vm-error-fee-below-minimum` — for the emitted + child's own fee parameters, either a non-zero ``execution_budget_per_round`` + below ``node.messageBudgetFloor`` (the chain's ``BudgetTooLow``), or — unless + both time-unit allocations are zero — a leader allocation outside + ``node.minProposeTimeout`` through ``node.maxProposeTimeout`` or a validator allocation outside - ``node.minCommitTimeout`` through ``node.maxCommitTimeout``. + ``node.minCommitTimeout`` through ``node.maxCommitTimeout``. Internal grant + nodes use the same local primary quote but skip the unavailable + transaction-pinned budget-floor admission check - :ref:`gvm-def-str-trie-value-vm-error-fee-too-many-rounds` — ``rotations`` implies more consensus rounds than the node's validator table supports (on-chain ``MAX_ROUNDS``). @@ -235,7 +336,8 @@ Payload "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 + "fee_params": FeeParams, // optional (default absent), required iff use_balance + "descendants": Descendants // optional (default absent), see balance-funded fees } } @@ -249,7 +351,8 @@ Requirements #. ``calldata`` satisfies :ref:`gvm-def-contract-call-conv` Supports CREATE2-style deployment with salt nonce for deterministic addressing. -``use_balance`` / ``fee_params`` behave as for :ref:`gvm-gl-call-balance-fees`. +``use_balance`` / ``fee_params`` / ``descendants`` behave as for +:ref:`gvm-gl-call-balance-fees`. .. _gvm-def-gl-call-run-nondet: diff --git a/docs/website/src/spec/appendix/constants.rst b/docs/website/src/spec/appendix/constants.rst index 0e7ad72d..e4b4da78 100644 --- a/docs/website/src/spec/appendix/constants.rst +++ b/docs/website/src/spec/appendix/constants.rst @@ -389,6 +389,21 @@ Param: i32 ``fee too_many_rounds`` ~~~~~~~~~~~~~~~~~~~~~~~ +.. _gvm-def-str-trie-value-vm-error-fee-descendant-grant-budget: + +``fee descendant_grant budget`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _gvm-def-str-trie-value-vm-error-fee-descendant-grant-tree: + +``fee descendant_grant tree`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _gvm-def-str-trie-value-vm-error-fee-descendant-grant-external: + +``fee descendant_grant external`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + .. _gvm-def-str-trie-value-vm-error-forbidden: ``forbidden`` diff --git a/executors/v0.3.x b/executors/v0.3.x index 49aa5746..c8116fba 160000 --- a/executors/v0.3.x +++ b/executors/v0.3.x @@ -1 +1 @@ -Subproject commit 49aa5746603f9a1b41a0144a87698de62a28282a +Subproject commit c8116fba35243cac378ec9eca3be20c6ebfa88c0 diff --git a/support/tools/genvm-tool/genvm_tool/tests/stage/execution.py b/support/tools/genvm-tool/genvm_tool/tests/stage/execution.py index 5b0d3989..6e024788 100644 --- a/support/tools/genvm-tool/genvm_tool/tests/stage/execution.py +++ b/support/tools/genvm-tool/genvm_tool/tests/stage/execution.py @@ -110,6 +110,7 @@ class _ExecutionContext: success_count: int = 0 skipped: int = 0 running_services: dict[str, Handle] = field(default_factory=dict) + failed_services: dict[str, str] = field(default_factory=dict) # Per-test completion tracking for test-to-test dependencies test_completed: dict[str, asyncio.Event] = field(default_factory=dict) test_passed: dict[str, bool] = field(default_factory=dict) @@ -167,6 +168,11 @@ async def _start_service(ctx: _ExecutionContext, service: Service) -> None: """Start a service and track its handle.""" ctx.shared.logger.info('Starting service', service_name=service.name) try: + for dependency in service.depends_on or []: + if dependency.name in ctx.failed_services: + raise RuntimeError( + f'Service {dependency.name} failed: {ctx.failed_services[dependency.name]}' + ) async with asyncio.timeout(SPAWN_TIMEOUT): handle = await service.manager.start() await handle.await_startup() @@ -174,12 +180,12 @@ async def _start_service(ctx: _ExecutionContext, service: Service) -> None: ctx.running_services[service.name] = handle ctx.shared.logger.info('Service started', service_name=service.name) except Exception as e: + ctx.failed_services[service.name] = str(e) ctx.shared.logger.error( 'Failed to start service', service_name=service.name, error=e, ) - raise async def _stop_service(ctx: _ExecutionContext, service: Service) -> None: @@ -346,6 +352,11 @@ async def _run_case_locked(ctx: _ExecutionContext, case: genvm_tool.tests.test.C ) try: + for service in case.description.needed_services: + if service.name in ctx.failed_services: + raise RuntimeError( + f'Service {service.name} failed: {ctx.failed_services[service.name]}' + ) run_shared.logger.debug( 'Running test case', case_name=case.description.name, diff --git a/support/tools/genvm-tool/unit_tests/test_service_startup.py b/support/tools/genvm-tool/unit_tests/test_service_startup.py index 4d999b09..b3602d33 100644 --- a/support/tools/genvm-tool/unit_tests/test_service_startup.py +++ b/support/tools/genvm-tool/unit_tests/test_service_startup.py @@ -1,9 +1,13 @@ """A service that never comes up must not wedge the run.""" import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock import pytest +from genvm_tool.tests import SharedContext, test from genvm_tool.tests.exec import service +from genvm_tool.tests.stage import collection, execution, scheduling class _Handle(service.Handle): @@ -77,3 +81,50 @@ async def healthy(self) -> bool: with pytest.raises(RuntimeError, match='not healthy within'): asyncio.run(asyncio.wait_for(handle.await_startup(), timeout=5)) assert handle.interrupted + + +@pytest.mark.parametrize('failure', ['spawn', 'dead', 'timeout']) +def test_startup_failure_fails_dependents_and_continues(tmp_path, failure): + handle = _Handle(reason='exited with code 1' if failure == 'dead' else None) + start = AsyncMock(return_value=handle) + if failure == 'spawn': + start.side_effect = RuntimeError('could not spawn') + failed = collection.Service('failed', service.FunctionService(start)) + dependent_start = AsyncMock() + dependent = collection.Service( + 'dependent', service.FunctionService(dependent_start), depends_on=[failed] + ) + healthy = _Handle(healthy_after=1) + unrelated = collection.Service( + 'unrelated', service.FunctionService(AsyncMock(return_value=healthy)) + ) + cases = [] + for name, needed in [ + ('direct-1', failed), + ('direct-2', failed), + ('transitive', dependent), + ('unrelated', unrelated), + ]: + case = test.StepsCase( + test.Description(name, needed_services=frozenset({needed})), + [test.CONST_PASSED], + ) + case.into_steps = AsyncMock(wraps=case.into_steps) + cases.append(case) + shared = SharedContext(tmp_path, logger=Mock(), printer=Mock(), watchdog=Mock()) + plan = scheduling.run( + shared, collection.Env(cases=cases, args=SimpleNamespace(max_concurrent=2)) + ) + result = asyncio.run(asyncio.wait_for(execution.run(shared, plan), timeout=5)) + assert set(result.failed) == {'direct-1', 'direct-2', 'transitive'} + assert result.success_count == 1 + assert len(result.results) == 4 + for record in result.results: + if record.name != 'unrelated': + assert not record.passed + assert 'Service failed failed:' in record.failure_message + for case in cases[:3]: + case.into_steps.assert_not_called() + dependent_start.assert_not_called() + assert healthy.interrupted + assert all(s.handle is None for s in [failed, dependent, unrelated]) diff --git a/tests/runner/genvm_tool_plugins/integration.py b/tests/runner/genvm_tool_plugins/integration.py index 89d26085..7551a5a8 100644 --- a/tests/runner/genvm_tool_plugins/integration.py +++ b/tests/runner/genvm_tool_plugins/integration.py @@ -88,6 +88,12 @@ def _normalize_message_fee_allocation( allocation: fees.MessageAllocationNode, ) -> fees.MessageAllocationNode: allocation = allocation.copy() + raw_allocation = typing.cast(dict[str, typing.Any], allocation) + external_params = raw_allocation['fee_params'].get('External') + if external_params is not None and isinstance(external_params['max_gas_price'], str): + external_params = external_params.copy() + external_params['max_gas_price'] = int(external_params['max_gas_price']) + raw_allocation['fee_params'] = {'External': external_params} if isinstance(allocation['recipient'], str): allocation['recipient'] = Address(allocation['recipient']) if isinstance(allocation['subtree'], list): @@ -657,6 +663,12 @@ async def _run_single_step( # Set up paths rel_path = jsonnet_path.relative_to(self._test_case.cases_dir) mock_sock_path = Path('/tmp', 'genvm-test', rel_path.with_suffix(f'.sock{suff}')) + # sun_path is 104 bytes on macOS, 108 on Linux + if len(os.fsencode(mock_sock_path)) > 100: + path_hash = genvm_tool.gvm32.encode( + hashlib.sha3_256(os.fsencode(mock_sock_path)).digest() + ) + mock_sock_path = Path('/tmp', f'genvm-{path_hash}.sock') mock_sock_path.parent.mkdir(exist_ok=True, parents=True) is_leader = single_conf.get('mode') == 'l' diff --git a/tests/runner/origin/fees.py b/tests/runner/origin/fees.py index 3345fdc4..01ddbc0a 100644 --- a/tests/runner/origin/fees.py +++ b/tests/runner/origin/fees.py @@ -51,6 +51,23 @@ class MessageAllocationNode(typing.TypedDict): subtree: bytes +# Hosts append this to pinned trees and send it alone for closed trees. +UNMATCHED_EXTERNAL_GUARD_ALLOC: MessageAllocationNode = { + 'budget': 0, + 'recipient': None, + 'call_key': None, + 'on': 'finalized', + 'fee_params': { + 'External': { + 'gas_limit': 1, + 'max_gas_price': 2**256 - 1, + }, + }, + 'children_budget': 0, + 'subtree': b'', +} + + DEFAULT_EXTERNAL_MESSAGE_ALLOC: MessageAllocationNode = { 'budget': 2**200, 'recipient': None, diff --git a/tests/runner/origin/public_abi.py b/tests/runner/origin/public_abi.py index 41d7cf49..96a4cb31 100644 --- a/tests/runner/origin/public_abi.py +++ b/tests/runner/origin/public_abi.py @@ -224,6 +224,17 @@ def internal() -> 'VmError': def external() -> 'VmError': return VmError('fee no_matching_allocation # external') +class _VmErrorFeeDescendantGrant: + @staticmethod + def budget() -> 'VmError': + return VmError('fee descendant_grant budget') + @staticmethod + def tree() -> 'VmError': + return VmError('fee descendant_grant tree') + @staticmethod + def external() -> 'VmError': + return VmError('fee descendant_grant external') + class _VmErrorFee: @staticmethod def below_minimum() -> 'VmError': @@ -234,6 +245,9 @@ def too_many_rounds() -> 'VmError': @staticmethod def no_matching_allocation() -> '_VmErrorFeeNoMatchingAllocation': return _VmErrorFeeNoMatchingAllocation() + @staticmethod + def descendant_grant() -> '_VmErrorFeeDescendantGrant': + return _VmErrorFeeDescendantGrant() class _VmErrorEvm: @staticmethod