From 7e58617b3385492df86c537cc8c936309e63c4d1 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 15 Oct 2026 21:00:10 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(fees):=20move=20the=20open-bucket=20ca?= =?UTF-8?q?scade=20carve=20into=20GenVM=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The node raised each sender-pool child of an open-bucket transaction to the chain floor and passed down half of the spare pool after execution. The host now sends both chain values as message_fee_cascade and the executor applies the same arithmetic before the execution hash, so the budgets are part of what validators compare. --- crates/modules-interfaces/src/domain.rs | 4 ++++ .../modules-interfaces/src/domain/fees/mod.rs | 22 +++++++++++++++++++ docs/website/src/impl-spec/04-fees.rst | 15 +++++++++++++ .../src/impl-spec/appendix/manager-api.yaml | 22 +++++++++++++++++++ executors/v0.3.x | 2 +- implementation/src/manager/run.rs | 11 ++++++++++ implementation/src/manager/run_test.rs | 2 ++ .../runner/genvm_tool_plugins/integration.py | 3 +++ 8 files changed, 80 insertions(+), 1 deletion(-) diff --git a/crates/modules-interfaces/src/domain.rs b/crates/modules-interfaces/src/domain.rs index 9b58650..4a679c0 100644 --- a/crates/modules-interfaces/src/domain.rs +++ b/crates/modules-interfaces/src/domain.rs @@ -47,6 +47,10 @@ pub struct ExecutionData { pub gas_data: std::collections::BTreeMap, /// Flat allocations with host-encoded subtree payloads. pub message_fee_allocation: Vec, + /// Present only for a top-level open-bucket execution + #[serde(default)] + #[calldata(default = default_none)] + pub message_fee_cascade: Option, /// Initial time-unit budget for this execution. pub initial_time_units_allocation: u32, /// Auditable supervisor action kinds to return in the execution result. diff --git a/crates/modules-interfaces/src/domain/fees/mod.rs b/crates/modules-interfaces/src/domain/fees/mod.rs index 9a97fe3..2a97a4c 100644 --- a/crates/modules-interfaces/src/domain/fees/mod.rs +++ b/crates/modules-interfaces/src/domain/fees/mod.rs @@ -116,3 +116,25 @@ impl MessageAllocationNode { .then_some(*params) } } + +/// Open-bucket cascade: sender-pool internal messages are raised to +/// `min_primary_fee`, then share half the remaining budget after fees and external reserves +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + genlayer_calldata::Encode, + genlayer_calldata::Decode, +)] +pub struct MessageFeeCascade { + /// Chain `minMessagePrimaryFeesForTx` of the open-bucket internal fee params + pub min_primary_fee: U256, + /// Chain `getMessageFeesBudgetRemaining` at the processing block + pub budget_remaining: U256, + /// Full gas envelope times locked receipt price, per external message + pub external_fee_reserve: U256, +} diff --git a/docs/website/src/impl-spec/04-fees.rst b/docs/website/src/impl-spec/04-fees.rst index c0724b8..a5df8a8 100644 --- a/docs/website/src/impl-spec/04-fees.rst +++ b/docs/website/src/impl-spec/04-fees.rst @@ -403,6 +403,21 @@ not the unspent allocation allowance or consumption from earlier generations. All v0.3 internal emissions reject zero time-unit, storage or receipt price caps with ``Inval`` before charging fees or appending an emission +When the manager input carries ``message_fee_cascade``, a top-level execution +rewrites its final emissions before the execution hash. Each sender-pool +internal message (not ``use_balance``) gets ``fee = max(fee, min_primary_fee)``. +With ``N`` such messages and ``spare = budget_remaining - Σ fee - E`` positive, each then +gets ``floor(spare / (2 * N))`` more. Balance-funded and external messages are +untouched, and buckets are not charged for the addition: the chain carves the +child's own pool from its declared budget. A single floor is enough because the +host gives every open-pool internal wildcard the same fee params + +``budget_remaining`` is read at the pinned processing block. It can still include +a prior generation's charge the chain rolls back only later, which makes the +addition smaller, never unbacked. ``E`` is ``external_fee_reserve`` times the +number of external emissions: each reserves the full chain gas envelope at the +locked receipt price. Floors still apply when the remaining budget is insufficient + The parent allocation's aggregate capacity is a separate invariant. If ``L = appealRounds + 1`` novel executions may each emit a child carrying budget ``C``, preserving that descendant capacity for every execution requires:: diff --git a/docs/website/src/impl-spec/appendix/manager-api.yaml b/docs/website/src/impl-spec/appendix/manager-api.yaml index 41c8084..20731df 100644 --- a/docs/website/src/impl-spec/appendix/manager-api.yaml +++ b/docs/website/src/impl-spec/appendix/manager-api.yaml @@ -1088,6 +1088,28 @@ components: 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 + message_fee_cascade: + type: object + nullable: true + default: null + additionalProperties: false + required: [min_primary_fee, budget_remaining, external_fee_reserve] + properties: + min_primary_fee: + type: integer + format: uint256 + description: Chain `minMessagePrimaryFeesForTx` of the open-pool internal fee params + budget_remaining: + type: integer + format: uint256 + description: Chain `getMessageFeesBudgetRemaining` at the pinned processing block + external_fee_reserve: + type: integer + format: uint256 + description: Full external gas envelope times locked receipt gas price, per external emission + description: | + Open-pool cascade, see the fees impl-spec. Send it only for an open-pool + execution; null leaves declared budgets as metered record_actions: type: array items: diff --git a/executors/v0.3.x b/executors/v0.3.x index c8116fb..7d3e3e1 160000 --- a/executors/v0.3.x +++ b/executors/v0.3.x @@ -1 +1 @@ -Subproject commit c8116fba35243cac378ec9eca3be20c6ebfa88c0 +Subproject commit 7d3e3e1a889482c933c180736cd48c2744c9fdb2 diff --git a/implementation/src/manager/run.rs b/implementation/src/manager/run.rs index 1e72a49..9284ce9 100644 --- a/implementation/src/manager/run.rs +++ b/implementation/src/manager/run.rs @@ -1026,6 +1026,10 @@ pub struct Request { #[serde(default)] #[calldata(default = default_message_fee_allocation)] pub message_fee_allocation: Vec, + /// Open-bucket cascade inputs; absent means no cascade + #[serde(default)] + #[calldata(default = default_message_fee_cascade)] + pub message_fee_cascade: Option, /// Initial time-unit budget for this execution. pub initial_time_units_allocation: u32, /// Auditable supervisor action kinds to return in the execution result. @@ -1062,6 +1066,10 @@ fn default_message_fee_allocation() -> Vec Option { + None +} + fn default_record_actions() -> Vec { Vec::new() } @@ -1998,6 +2006,7 @@ fn execution_data_from_request( bucket_totals: req.bucket_totals.clone(), gas_data: req.gas_data.clone(), message_fee_allocation: req.message_fee_allocation.clone(), + message_fee_cascade: req.message_fee_cascade, initial_time_units_allocation: req.initial_time_units_allocation, record_actions: req.record_actions.clone(), remaining_recursion: if req.debug_mode >= genvm_common::DebugMode::Unsafe { @@ -2229,6 +2238,7 @@ impl Ctx { leader_public_data: None, gas_data: parent_req.gas_data.clone(), message_fee_allocation: Vec::new(), + message_fee_cascade: None, initial_time_units_allocation: 0, record_actions: Vec::new(), host_genvm_id: None, @@ -2243,6 +2253,7 @@ impl Ctx { execution_data.leader_public_data = None; execution_data.record_actions.clear(); execution_data.message_fee_allocation.clear(); + execution_data.message_fee_cascade = None; execution_data.remaining_recursion = Some(cross_major_recursion(envelope.remaining_recursion)); execution_data.nested = Some(genvm_modules_interfaces::NestedExecutionData { diff --git a/implementation/src/manager/run_test.rs b/implementation/src/manager/run_test.rs index 50ef7f8..83f5fc2 100644 --- a/implementation/src/manager/run_test.rs +++ b/implementation/src/manager/run_test.rs @@ -1111,6 +1111,7 @@ fn request_field_names() -> Vec<&'static str> { leader_public_data, gas_data, message_fee_allocation, + message_fee_cascade, initial_time_units_allocation, record_actions, host_genvm_id, @@ -1139,6 +1140,7 @@ fn request_field_names() -> Vec<&'static str> { "leader_public_data", "gas_data", "message_fee_allocation", + "message_fee_cascade", "initial_time_units_allocation", "record_actions", "host_genvm_id", diff --git a/tests/runner/genvm_tool_plugins/integration.py b/tests/runner/genvm_tool_plugins/integration.py index 7551a5a..ac7b0a4 100644 --- a/tests/runner/genvm_tool_plugins/integration.py +++ b/tests/runner/genvm_tool_plugins/integration.py @@ -782,6 +782,9 @@ def resolve_executor(address, state, advisory_major): request_extra['permissions'] = case_permissions if hook_calls: request_extra['hook_cross_contract_calls'] = True + message_fee_cascade = single_conf.get('message_fee_cascade') + if message_fee_cascade is not None: + request_extra['message_fee_cascade'] = message_fee_cascade bucket_totals = base_host.default_bucket_totals(self._test_case.executor_major) bucket_totals.update(single_conf.get('bucket_totals', {})) From 9735fad5daaf15478f67ddf5de8aac220dd6afee Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 15 Oct 2026 21:00:10 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(tests):=20run=20leader=20fault=20checks?= =?UTF-8?q?=20synchronously=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- executors/v0.3.x | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/executors/v0.3.x b/executors/v0.3.x index 7d3e3e1..d4c51c7 160000 --- a/executors/v0.3.x +++ b/executors/v0.3.x @@ -1 +1 @@ -Subproject commit 7d3e3e1a889482c933c180736cd48c2744c9fdb2 +Subproject commit d4c51c7882cbf335d8441d31f1f1d044ad01c5e0 From 5050af6c65e2bab596c0d40a5dd502aecaecff59 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 15 Oct 2026 21:00:10 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix(fees):=20enforce=20the=20pinned=20grant?= =?UTF-8?q?=20admission=20on=20descendant=20grants=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pinned descendant grant is checked against the limits the parent tx pinned at activation: tree depth (gas_data maxAllocationTreeDepth, default 5, root depth 1), the message budget floor on every node, and the minimum external gas limit on external nodes (gas_data minExternalGasLimit, default 0). A grant the chain would reject fails at emission instead of producing a receipt no leader can reveal. --- docs/website/src/impl-spec/04-fees.rst | 7 +++++++ docs/website/src/impl-spec/appendix/manager-api.yaml | 7 ++++++- docs/website/src/spec/appendix/internal-constants.rst | 7 +++++++ executors/v0.3.x | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/website/src/impl-spec/04-fees.rst b/docs/website/src/impl-spec/04-fees.rst index a5df8a8..225c62f 100644 --- a/docs/website/src/impl-spec/04-fees.rst +++ b/docs/website/src/impl-spec/04-fees.rst @@ -418,6 +418,13 @@ addition smaller, never unbacked. ``E`` is ``external_fee_reserve`` times the number of external emissions: each reserves the full chain gas envelope at the locked receipt price. Floors still apply when the remaining budget is insufficient +Pinned descendant grants enforce the parent's ``messageBudgetFloor``, +``gas_data.maxAllocationTreeDepth`` (root depth 1) and +``gas_data.minExternalGasLimit``. When the host omits the depth limit, the +executor defaults to 5; an omitted gas floor is 0. Excess depth fails at emission +with ``fee descendant_grant tree``; an external node whose ``gas_limit`` is below +the floor fails with ``fee descendant_grant external`` + The parent allocation's aggregate capacity is a separate invariant. If ``L = appealRounds + 1`` novel executions may each emit a child carrying budget ``C``, preserving that descendant capacity for every execution requires:: diff --git a/docs/website/src/impl-spec/appendix/manager-api.yaml b/docs/website/src/impl-spec/appendix/manager-api.yaml index 20731df..b822d22 100644 --- a/docs/website/src/impl-spec/appendix/manager-api.yaml +++ b/docs/website/src/impl-spec/appendix/manager-api.yaml @@ -1024,7 +1024,12 @@ components: additionalProperties: type: string default: {} - description: Host-provided `node` fee constants + description: | + Host-provided `node` fee constants. `maxAllocationTreeDepth` is the + parent's pinned grant depth limit as a decimal uint256; v0.3 defaults + to 5 when absent (root depth is 1). `minExternalGasLimit` is the + parent's pinned external-node gas floor as a decimal uint256; 0 when + absent message_fee_allocation: type: array items: diff --git a/docs/website/src/spec/appendix/internal-constants.rst b/docs/website/src/spec/appendix/internal-constants.rst index 4c7a1fb..05f488f 100644 --- a/docs/website/src/spec/appendix/internal-constants.rst +++ b/docs/website/src/spec/appendix/internal-constants.rst @@ -99,6 +99,13 @@ nondet_blocks Value: ``4096`` +.. _gvm-def-consts-value-top-limits-default-allocation-tree-depth: + +default_allocation_tree_depth +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Value: ``5`` + .. _gvm-def-consts-value-top-limits-locked-slots: locked_slots diff --git a/executors/v0.3.x b/executors/v0.3.x index d4c51c7..2e094c6 160000 --- a/executors/v0.3.x +++ b/executors/v0.3.x @@ -1 +1 @@ -Subproject commit d4c51c7882cbf335d8441d31f1f1d044ad01c5e0 +Subproject commit 2e094c6fdd7fb3b620706a2d20845e004fb6d298 From e6b66e1bacc1dfc14f17e5f6273dd2a2c10f9063 Mon Sep 17 00:00:00 2001 From: kp2pml30 Date: Thu, 15 Oct 2026 22:03:01 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs(fees):=20drop=20stale=20grant=20admiss?= =?UTF-8?q?ion=20stand-ins=20=F0=9F=93=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Descendant grants now enforce the pinned depth limit, the external gas floor and the budget floor, so only the local primary quote remains a stand-in --- docs/website/src/impl-spec/04-fees.rst | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/website/src/impl-spec/04-fees.rst b/docs/website/src/impl-spec/04-fees.rst index 225c62f..d4245ff 100644 --- a/docs/website/src/impl-spec/04-fees.rst +++ b/docs/website/src/impl-spec/04-fees.rst @@ -364,14 +364,8 @@ configured lifecycle, including appeals; the remainder becomes the child's 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 transaction-pinned primary quote is not in v0.3 host input yet, so the +existing local primary-fee calculation supplies each child quote The Python SDK takes ``use_balance: UseBalanceParams | None`` on calls, transfers and deploys. ``None`` selects sender funding; the dataclass holds