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
2 changes: 1 addition & 1 deletion crates/modules-interfaces/src/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub struct ExecutionData {
pub bucket_totals: std::collections::BTreeMap<String, num_bigint::BigInt>,
/// Host-provided `node` fee constants (moved off `host_data`).
pub gas_data: std::collections::BTreeMap<String, String>,
/// Message-fee allocation tree passed alongside the execution.
/// Flat allocations with host-encoded subtree payloads.
pub message_fee_allocation: Vec<fees::MessageAllocationNode>,
/// Initial time-unit budget for this execution.
pub initial_time_units_allocation: u32,
Expand Down
170 changes: 0 additions & 170 deletions crates/modules-interfaces/src/domain/fees/abi.rs

This file was deleted.

60 changes: 16 additions & 44 deletions crates/modules-interfaces/src/domain/fees/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
pub mod abi;

use primitive_types::U256;

use crate::On;

pub const CALL_KEY_WILDCARD: crate::abi_stub::CallKey = crate::abi_stub::CallKey([
0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0,
0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70,
]);

#[derive(
Debug,
Clone,
Expand All @@ -26,12 +19,7 @@ pub struct InternalMessageParams {
/// Per-round rotation allocations; `rotations[0]` is the initial round, the
/// rest are appeal rounds. Must be non-empty.
///
/// The chain's `InternalMessageFeeParams` carries an explicit `appealRounds`
/// field, but it is not stored here: the chain enforces
/// `appealRounds == rotations.length - 1` (`FeesVerifier.InvalidAppealRounds`),
/// so we derive `appeal_rounds = rotations.len() - 1` instead. The ABI
/// encoder re-inserts it so the encoded bytes (and their `keccak256` fee-param
/// pin) match the chain's field layout.
/// The fee evaluator derives `appeal_rounds = rotations.len() - 1`.
pub rotations: Vec<U256>,
/// Per-time-unit GEN price cap locked at activation (consensus CON-549,
/// v0.6-dev). The chain charges at this cap as the funding multiplier and
Expand Down Expand Up @@ -75,7 +63,7 @@ pub enum MessageAllocationNodeParams {
External(ExternalMessageParams),
}

/// One node of the message-fee allocation tree.
/// One allocation available to messages emitted by this execution.
#[derive(
Debug,
Clone,
Expand All @@ -87,25 +75,19 @@ pub enum MessageAllocationNodeParams {
pub struct MessageAllocationNode {
/// Target contract address; `None` means wildcard (any recipient).
pub recipient: Option<genlayer_calldata::Address>,
/// `None` = wildcard: all call keys for this recipient
/// (chain sentinel: `CALL_KEY_WILDCARD` = `keccak256("")`).
/// `None` means any call key; the node converts the chain's wildcard sentinel.
pub call_key: Option<crate::abi_stub::CallKey>,
/// Max budget for matching messages.
pub budget: U256,
/// Available allowance; zero is exhausted, `None` is uncapped.
pub budget: Option<U256>,
pub on: On,
/// Same structure as TX-level params.
pub fee_params: MessageAllocationNodeParams,
pub children: Vec<MessageAllocationNode>,
/// Sum of the direct descendants' allocation budgets, funded per emission.
pub children_budget: U256,
/// Host-encoded matched subtree, including any proof required by the chain.
pub subtree: bytes::Bytes,
}

impl MessageAllocationNode {
/// ABI-encodes this matched node and its descendants for transport to the chain.
/// The matched node is element 0 and descendants are in BFS order.
pub fn abi_encode(&self) -> Vec<u8> {
abi::encode(self)
}

#[allow(clippy::if_same_then_else)]
pub fn matches_internal(
&self,
on: On,
Expand All @@ -115,18 +97,12 @@ impl MessageAllocationNode {
let MessageAllocationNodeParams::Internal(params) = &self.fee_params else {
return None;
};
if on != self.on {
None
} else if self.recipient.as_ref().is_some_and(|r| *r != recipient) {
None
} else if self.call_key.as_ref().is_some_and(|ck| *ck != call_key) {
None
} else {
Some(params.clone())
}
(on == self.on
&& self.recipient.is_none_or(|r| r == recipient)
&& self.call_key.is_none_or(|ck| ck == call_key))
.then(|| params.clone())
}

#[allow(clippy::if_same_then_else)]
pub fn matches_external(
&self,
recipient: genlayer_calldata::Address,
Expand All @@ -135,12 +111,8 @@ impl MessageAllocationNode {
let MessageAllocationNodeParams::External(params) = &self.fee_params else {
return None;
};
if self.recipient.as_ref().is_some_and(|r| *r != recipient) {
None
} else if self.call_key.as_ref().is_some_and(|ck| *ck != call_key) {
None
} else {
Some(*params)
}
(self.recipient.is_none_or(|r| r == recipient)
&& self.call_key.is_none_or(|ck| ck == call_key))
.then_some(*params)
}
}
48 changes: 44 additions & 4 deletions docs/website/src/impl-spec/04-fees.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,21 +286,42 @@ after the exact key; executor-only open-bucket recipient wildcards are less spec
than either. List order therefore cannot make a wildcard shadow an exact allocation.

For internal messages, ``on`` is checked after the allocation key is resolved, so a
phase mismatch on an exact allocation does not fall through to a wildcard. For
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 exhausted exact allocation spills to the per-recipient
``call_key`` wildcard. If neither key has an 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 default v0.3 fee expressions reject external reservations and receipts with
``fee below_minimum`` unless ``node.lockedReceiptGasPrice`` is positive, including
external messages without a matching allocation

Entry presence determines matching, independently of ``budget``. The host supplies
the allowance available at execution start, optionally reduced by prior consumption.
Zero means an exhausted allocation; ``null`` removes the per-allocation cap while
keeping the execution's fee buckets. Local consumption is tracked separately.
An exhausted internal exact key still wins and fails its budget check; it never
falls through. An absent chain allocation (including a chain record whose original
budget is zero) must be omitted, not represented by an exhausted entry.

The host must preserve every existing pinned key, including exhausted keys, 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.

Funding modes
~~~~~~~~~~~~~~

An outgoing internal message is funded one of two ways:

- **Allocation-matched (default).** The fee is matched against the allocation
tree as above. After the expression computes the primary reserve, the executor
adds the budgets of the matched node's direct children; deeper budgets are
already contained by their direct parent. A declared budget exceeding the
list as above. After the expression computes the primary reserve, the executor
adds the host-supplied ``children_budget``; deeper budgets are already
contained by their direct parent. A declared budget exceeding the
matched node's remaining ``budget`` is rejected with an allocation-budget
error; otherwise it consumes ``message_fee`` atomically with the
``message_receipt`` charge.
Expand All @@ -323,6 +344,25 @@ For balance funding the sum is zero. The primary fee already covers the child's
configured lifecycle, including appeals; the remainder becomes the child's
message-fee bucket. External messages declare zero

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
rotations, multiplied by the child's price cap. Its profit reserve is
``bond + floor(bond / 2)``; the developer/DAO overlay applies only to time-unit
work, never to this profit reserve

The manager input ``message_fee_allocation`` contains only the allocations matched
by this execution. Each carries a required ``children_budget`` and opaque
``subtree`` bytes. The host sums the direct descendants' budgets and encodes
the matched subtree with any proof required by the transaction's pinned
storage mode. Both executors forward those bytes unchanged; v0.3 charges their
full length against receipt gas, submitted-message bytes and memory. The subtree
includes the matched root; it is not merely an encoding of its descendants

``children_budget`` funds onward messages per emission; it is
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

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::
Expand Down
Loading
Loading