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
22 changes: 12 additions & 10 deletions backend/node/genvm/origin/fees.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""
Message-fee allocation tree types.
Message-fee allocation types.

Mirrors the executor's `genvm_common::domain::fees` module: the fee parameters
and the nested `MessageAllocationNode` tree that is passed alongside an
Mirrors the shared `genvm_modules_interfaces::fees` module: the fee parameters
and the flat allocations that are passed alongside an
execution and matched against emitted messages.
"""

Expand Down Expand Up @@ -43,13 +43,12 @@ class _ExternalParams(typing.TypedDict):
class MessageAllocationNode(typing.TypedDict):
recipient: Address | None
call_key: bytes | None
budget: int
budget: int | None
# Lifecycle the node matches against (only meaningful for internal messages).
on: typing.Literal["finalized", "decided"]
fee_params: MessageAllocationNodeParams
# Nested allocation subtree; the chain receives this flattened to
# parent-pointer form.
children: list["MessageAllocationNode"]
children_budget: int
subtree: bytes


DEFAULT_EXTERNAL_MESSAGE_ALLOC: MessageAllocationNode = {
Expand All @@ -64,7 +63,8 @@ class MessageAllocationNode(typing.TypedDict):
"max_gas_price": 0,
},
},
"children": [],
"children_budget": 0,
"subtree": b"",
}

DEFAULT_INTERNAL_DEC_MESSAGE_ALLOC: MessageAllocationNode = {
Expand All @@ -83,7 +83,8 @@ class MessageAllocationNode(typing.TypedDict):
"receipt_fee_max_gas_price": 2**200,
},
},
"children": [],
"children_budget": 0,
"subtree": b"",
}

DEFAULT_INTERNAL_FIN_MESSAGE_ALLOC: MessageAllocationNode = {
Expand All @@ -102,5 +103,6 @@ class MessageAllocationNode(typing.TypedDict):
"receipt_fee_max_gas_price": 20,
},
},
"children": [],
"children_budget": 0,
"subtree": b"",
}
49 changes: 30 additions & 19 deletions backend/protocol_rpc/fees.py
Original file line number Diff line number Diff line change
Expand Up @@ -2089,22 +2089,28 @@ def genvm_message_fee_allocation(
_serializable_message_allocation(raw_node)
for raw_node in accounting.get("message_allocations") or []
]
genvm_nodes = [
_genvm_message_allocation_node(
node,
address_factory,
fees_distribution,
)
for node in studio_nodes
]
roots: list[dict[str, Any]] = []
children_budgets = [0] * len(studio_nodes)
for index, node in enumerate(studio_nodes):
_validate_allocation_parent(index, node, studio_nodes)
parent_index = int(node["parentIndex"])
if parent_index == NODE_ROOT_SENTINEL:
roots.append(genvm_nodes[index])
if parent_index != NODE_ROOT_SENTINEL:
children_budgets[parent_index] = _u256_add(
children_budgets[parent_index], int(node["budget"])
)

roots: list[dict[str, Any]] = []
for index, node in enumerate(studio_nodes):
if int(node["parentIndex"]) != NODE_ROOT_SENTINEL:
continue
if 0 <= parent_index < len(genvm_nodes):
genvm_nodes[parent_index]["children"].append(genvm_nodes[index])
allocation = _genvm_message_allocation_node(
node, address_factory, fees_distribution
)
allocation["children_budget"] = children_budgets[index]
if int(node["messageType"]) == MESSAGE_TYPE_INTERNAL:
allocation["subtree"] = _allocation_subtree_bytes(
_allocation_subtree(studio_nodes, index)
)
roots.append(allocation)

return roots

Expand Down Expand Up @@ -3725,7 +3731,7 @@ def _validate_allocation_parent(
parent_index = int(node["parentIndex"])
if parent_index == NODE_ROOT_SENTINEL:
return
if parent_index >= index:
if parent_index < 0 or parent_index >= index:
raise AllocationTreeMalformed("AllocationTreeMalformed")

parent_node = _normalize_message_allocation(message_allocations[parent_index])
Expand Down Expand Up @@ -4649,7 +4655,8 @@ def _genvm_message_allocation_node(
"budget": int(node["budget"]),
"on": _genvm_message_on(node),
"fee_params": _genvm_message_fee_params(node, fees_distribution),
"children": [],
"children_budget": 0,
"subtree": b"",
}


Expand Down Expand Up @@ -4702,7 +4709,8 @@ def _genvm_unmetered_message_fee_allocation() -> list[dict[str, Any]]:
"max_gas_price": 0,
},
},
"children": [],
"children_budget": 0,
"subtree": b"",
},
{
"recipient": None,
Expand All @@ -4716,15 +4724,17 @@ def _genvm_unmetered_message_fee_allocation() -> list[dict[str, Any]]:
"receipt_fee_max_gas_price": 20,
},
},
"children": [],
"children_budget": 0,
"subtree": b"",
},
{
"recipient": None,
"call_key": None,
"budget": budget,
"on": "decided",
"fee_params": internal_fee_params,
"children": [],
"children_budget": 0,
"subtree": b"",
},
]

Expand All @@ -4741,7 +4751,8 @@ def _genvm_external_legacy_fallback_message_fee_allocation() -> dict[str, Any]:
"max_gas_price": 0,
},
},
"children": [],
"children_budget": 0,
"subtree": b"",
}


Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_deploy_reroute_to.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
# every 0.x line and so cannot name a directory. Until something exposes it,
# this constant has to be updated whenever the image's current line moves;
# override the env var when it does.
CURRENT_EXECUTOR = os.environ.get("TEST_CURRENT_EXECUTOR", "v0.3.0-rc7")
CURRENT_EXECUTOR = os.environ.get("TEST_CURRENT_EXECUTOR", "v0.3.0-rc9")

# Shares the validator registry with the other integration suites, which wipe
# validators while seeding mock responses.
Expand Down
15 changes: 11 additions & 4 deletions tests/unit/test_node_state_proxy_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from eth_abi import encode
from eth_abi import decode, encode

from backend.database_handler.contract_snapshot import ContractSnapshot
from backend.domain.types import LLMProvider, Validator
Expand All @@ -22,6 +22,7 @@
from backend.node.types import Address, ExecutionMode, ExecutionResultStatus
from backend.protocol_rpc.fees import (
GENVM_UNMETERED_DATA_FEE_BUCKET,
MESSAGE_ALLOCATION_NODE_ABI_TYPE,
StudioFeePolicy,
create_fee_accounting,
required_fee_deposit,
Expand Down Expand Up @@ -445,9 +446,15 @@ async def test_run_genvm_passes_mode2_message_fee_allocations_to_genvm():
"receipt_fee_max_gas_price": 2**200,
},
}
assert allocation["children"][0]["recipient"].as_hex.lower() == child_recipient
assert allocation["children"][0]["call_key"] == bytes.fromhex("56" * 32)
assert allocation["children"][0]["budget"] == 60
assert allocation["children_budget"] == 60
subtree = decode([MESSAGE_ALLOCATION_NODE_ABI_TYPE], allocation["subtree"])[0]
assert subtree[0][2] == (1 << 256) - 1
assert subtree[0][3] == recipient
assert subtree[1][2] == 0
assert subtree[1][3] == child_recipient
assert subtree[1][4] == bytes.fromhex("56" * 32)
assert subtree[1][5] == 60
assert subtree[1][6] == child_fee_params


@pytest.mark.asyncio
Expand Down
114 changes: 92 additions & 22 deletions tests/unit/test_studio_fees.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest
import rlp
from eth_abi import encode
from eth_abi import decode, encode
from web3 import Web3

from backend.consensus.base import (
Expand Down Expand Up @@ -3344,12 +3344,28 @@ def test_genvm_message_fee_allocation_maps_studio_nodes():
"receipt_fee_max_gas_price": 2**200,
},
},
"children": [],
"children_budget": 0,
"subtree": encode(
[MESSAGE_ALLOCATION_NODE_ABI_TYPE],
[
[
(
1,
True,
NODE_ROOT_SENTINEL,
"0x" + "22" * 20,
bytes.fromhex("12" * 32),
60,
fee_params,
)
]
],
),
}
assert len(allocations) == 1


def test_genvm_message_fee_allocation_nests_descendants_under_roots():
def test_genvm_message_fee_allocation_encodes_descendants_under_roots():
root_fee_params = _encode_internal_fee_params(leader_timeunits=6)
child_fee_params = _encode_internal_fee_params(leader_timeunits=7)
root = _allocation(
Expand Down Expand Up @@ -3379,26 +3395,76 @@ def test_genvm_message_fee_allocation_nests_descendants_under_roots():
assert allocations[0]["recipient"] == root["recipient"]
assert allocations[0]["call_key"] == bytes.fromhex("12" * 32)
assert allocations[0]["budget"] == 120
assert allocations[0]["children"] == [
{
"recipient": descendant["recipient"],
"call_key": bytes.fromhex("34" * 32),
"budget": 60,
"on": "decided",
"fee_params": {
"Internal": {
"leader_timeunits_allocation": 7,
"validator_timeunits_allocation": 10,
"execution_budget_per_round": 0,
"rotations": [0],
"max_price_gen_per_time_unit": 1,
"storage_fee_max_gas_price": 2**200,
"receipt_fee_max_gas_price": 2**200,
},
},
"children": [],
}
assert allocations[0]["children_budget"] == 60
assert "children" not in allocations[0]
assert decode([MESSAGE_ALLOCATION_NODE_ABI_TYPE], allocations[0]["subtree"])[0] == (
(
1,
True,
NODE_ROOT_SENTINEL,
root["recipient"],
bytes.fromhex("12" * 32),
120,
root_fee_params,
),
(
1,
True,
0,
descendant["recipient"],
bytes.fromhex("34" * 32),
60,
child_fee_params,
),
)


def test_genvm_message_fee_allocation_preserves_subtree_order_and_direct_budget():
fee_params = _encode_internal_fee_params()
nodes = [
_allocation(budget=250, fee_params=fee_params),
_allocation(budget=150, fee_params=fee_params, recipient="0x" + "33" * 20),
_allocation(parent_index=0, budget=120, fee_params=fee_params),
_allocation(parent_index=1, budget=70, fee_params=fee_params),
_allocation(parent_index=2, budget=55, fee_params=fee_params),
_allocation(
parent_index=0, budget=60, fee_params=fee_params, recipient="0x" + "44" * 20
),
]
accounting = {"message_allocations": nodes, "allocation_consumed": {"0": 250}}

allocations = genvm_message_fee_allocation(accounting)

assert [node["budget"] for node in allocations] == [250, 150]
assert [node["children_budget"] for node in allocations] == [180, 70]
first = decode([MESSAGE_ALLOCATION_NODE_ABI_TYPE], allocations[0]["subtree"])[0]
second = decode([MESSAGE_ALLOCATION_NODE_ABI_TYPE], allocations[1]["subtree"])[0]
assert [node[2] for node in first] == [NODE_ROOT_SENTINEL, 0, 1, 0]
assert [node[5] for node in first] == [250, 120, 55, 60]
assert [node[2] for node in second] == [NODE_ROOT_SENTINEL, 0]
assert [node[5] for node in second] == [150, 70]
assert all(node[6] == fee_params for node in (*first, *second))
assert nodes[3]["parentIndex"] == 1
assert nodes[4]["parentIndex"] == 2


def test_genvm_message_fee_allocation_rejects_children_budget_overflow():
nodes = [
_allocation(budget=NODE_ROOT_SENTINEL),
_allocation(parent_index=0, budget=NODE_ROOT_SENTINEL),
_allocation(parent_index=0, budget=1),
]

with pytest.raises(ArithmeticOverflow):
genvm_message_fee_allocation({"message_allocations": nodes})


@pytest.mark.parametrize("parent_index", [-1, 0, 1])
def test_genvm_message_fee_allocation_rejects_invalid_parent(parent_index):
with pytest.raises(AllocationTreeMalformed):
genvm_message_fee_allocation(
{"message_allocations": [_allocation(parent_index=parent_index)]}
)


def test_genvm_message_fee_allocation_does_not_add_uncommitted_fallback():
Expand Down Expand Up @@ -3426,6 +3492,8 @@ def test_genvm_message_fee_allocation_does_not_add_uncommitted_fallback():
)
assert allocations[0]["budget"] == 210_000
assert allocations[0]["on"] == "finalized"
assert allocations[0]["children_budget"] == 0
assert allocations[0]["subtree"] == b""
assert allocations[0]["fee_params"] == {
"External": {
"gas_limit": 21_000,
Expand All @@ -3450,6 +3518,8 @@ def test_genvm_message_fee_allocation_keeps_legacy_gasless_messages_unmetered():
]
assert all(node["recipient"] is None for node in allocations)
assert all(node["call_key"] is None for node in allocations)
assert all(node["children_budget"] == 0 for node in allocations)
assert all(node["subtree"] == b"" for node in allocations)
base_internal_params = {
"leader_timeunits_allocation": 0,
"validator_timeunits_allocation": 0,
Expand Down
2 changes: 1 addition & 1 deletion third_party/genvm/version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.6.0-rc5
v0.6.0-rc7
Loading