Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .agents/skills/build/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
12 changes: 0 additions & 12 deletions .agents/skills/build/scripts/run-ninja.sh

This file was deleted.

2 changes: 2 additions & 0 deletions docs/contributing/howto/committing/runners.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/contributing/howto/pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
71 changes: 53 additions & 18 deletions docs/website/src/impl-spec/04-fees.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
~~~~~~~~~~~~~~
Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions docs/website/src/impl-spec/appendix/manager-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------------

Expand Down Expand Up @@ -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
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand All @@ -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``).
Expand All @@ -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
}
}

Expand All @@ -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:

Expand Down
Loading
Loading