diff --git a/backend/node/genvm/origin/fees.py b/backend/node/genvm/origin/fees.py index 4546185c5..26ca9fd59 100644 --- a/backend/node/genvm/origin/fees.py +++ b/backend/node/genvm/origin/fees.py @@ -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. """ @@ -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 = { @@ -64,7 +63,8 @@ class MessageAllocationNode(typing.TypedDict): "max_gas_price": 0, }, }, - "children": [], + "children_budget": 0, + "subtree": b"", } DEFAULT_INTERNAL_DEC_MESSAGE_ALLOC: MessageAllocationNode = { @@ -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 = { @@ -102,5 +103,6 @@ class MessageAllocationNode(typing.TypedDict): "receipt_fee_max_gas_price": 20, }, }, - "children": [], + "children_budget": 0, + "subtree": b"", } diff --git a/backend/protocol_rpc/fees.py b/backend/protocol_rpc/fees.py index 9577baa1f..4f59216ac 100644 --- a/backend/protocol_rpc/fees.py +++ b/backend/protocol_rpc/fees.py @@ -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 @@ -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]) @@ -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"", } @@ -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, @@ -4716,7 +4724,8 @@ def _genvm_unmetered_message_fee_allocation() -> list[dict[str, Any]]: "receipt_fee_max_gas_price": 20, }, }, - "children": [], + "children_budget": 0, + "subtree": b"", }, { "recipient": None, @@ -4724,7 +4733,8 @@ def _genvm_unmetered_message_fee_allocation() -> list[dict[str, Any]]: "budget": budget, "on": "decided", "fee_params": internal_fee_params, - "children": [], + "children_budget": 0, + "subtree": b"", }, ] @@ -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"", } diff --git a/tests/integration/test_deploy_reroute_to.py b/tests/integration/test_deploy_reroute_to.py index 08832de54..641ad4454 100644 --- a/tests/integration/test_deploy_reroute_to.py +++ b/tests/integration/test_deploy_reroute_to.py @@ -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. diff --git a/tests/unit/test_node_state_proxy_metrics.py b/tests/unit/test_node_state_proxy_metrics.py index 7087862a5..75f78ba9c 100644 --- a/tests/unit/test_node_state_proxy_metrics.py +++ b/tests/unit/test_node_state_proxy_metrics.py @@ -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 @@ -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, @@ -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 diff --git a/tests/unit/test_studio_fees.py b/tests/unit/test_studio_fees.py index 1bb779609..9312fde2c 100644 --- a/tests/unit/test_studio_fees.py +++ b/tests/unit/test_studio_fees.py @@ -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 ( @@ -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( @@ -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(): @@ -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, @@ -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, diff --git a/third_party/genvm/version b/third_party/genvm/version index 60b7d79e2..dcfffbc57 100644 --- a/third_party/genvm/version +++ b/third_party/genvm/version @@ -1 +1 @@ -v0.6.0-rc5 +v0.6.0-rc7