From bc135e85587ab226c47b9d7913727f5738fd0d8d Mon Sep 17 00:00:00 2001 From: Darien Hernandez Date: Sun, 7 Jun 2026 12:28:27 +0200 Subject: [PATCH] test(fees): cover allocation tree emit trap gates Add intercontract cases asserting that an empty or under-budget message-fee-allocation tree makes emit hard-trap instead of silently emitting nothing. Golden stdout/hash files are produced on Linux CI; the nix test shell does not instantiate on macOS. --- .../alloc_budget_exhausted.jsonnet | 25 +++++++++++++++++ .../intercontract/alloc_budget_exhausted.py | 16 +++++++++++ .../intercontract/alloc_empty_deploy.jsonnet | 7 +++++ .../py/intercontract/alloc_empty_deploy.py | 16 +++++++++++ .../alloc_empty_eth_send.jsonnet | 7 +++++ .../py/intercontract/alloc_empty_eth_send.py | 27 +++++++++++++++++++ .../alloc_empty_post_message.jsonnet | 8 ++++++ .../intercontract/alloc_empty_post_message.py | 19 +++++++++++++ 8 files changed, 125 insertions(+) create mode 100644 tests/cases/stable/py/intercontract/alloc_budget_exhausted.jsonnet create mode 100644 tests/cases/stable/py/intercontract/alloc_budget_exhausted.py create mode 100644 tests/cases/stable/py/intercontract/alloc_empty_deploy.jsonnet create mode 100644 tests/cases/stable/py/intercontract/alloc_empty_deploy.py create mode 100644 tests/cases/stable/py/intercontract/alloc_empty_eth_send.jsonnet create mode 100644 tests/cases/stable/py/intercontract/alloc_empty_eth_send.py create mode 100644 tests/cases/stable/py/intercontract/alloc_empty_post_message.jsonnet create mode 100644 tests/cases/stable/py/intercontract/alloc_empty_post_message.py diff --git a/tests/cases/stable/py/intercontract/alloc_budget_exhausted.jsonnet b/tests/cases/stable/py/intercontract/alloc_budget_exhausted.jsonnet new file mode 100644 index 00000000..d107956b --- /dev/null +++ b/tests/cases/stable/py/intercontract/alloc_budget_exhausted.jsonnet @@ -0,0 +1,25 @@ +local simple_deploy = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; +// A matching wildcard internal-finalized node, but budget=1 (< the computed +// message_fee, which is ~34836 for foo(1,2) on finalized). The find_map matches, +// then consume_message_fee_internal must trap on `fee_cost > node.budget`. +// Mirrors origin/fees.py DEFAULT_INTERNAL_FIN_MESSAGE_ALLOC with budget lowered. +{entry: util.addPaths([simple_deploy.run('${jsonnetDir}/${fileBaseName}.py') { + message_fee_allocation: [ + { + budget: 1, + recipient: null, + call_key: null, + on: 'finalized', + fee_params: { + Internal: { + execution_budget_per_round: 1024, + rotations: [4, 4, 4, 4, 4], + leader_timeunits_allocation: 5, + validator_timeunits_allocation: 5, + }, + }, + children: [], + }, + ], +}])} diff --git a/tests/cases/stable/py/intercontract/alloc_budget_exhausted.py b/tests/cases/stable/py/intercontract/alloc_budget_exhausted.py new file mode 100644 index 00000000..2f94775b --- /dev/null +++ b/tests/cases/stable/py/intercontract/alloc_budget_exhausted.py @@ -0,0 +1,16 @@ +# { "Depends": "py-genlayer:test" } +import genlayer as gl +from genlayer.types import * + + +# Regression guard (genvm fee-allocation item #8): node MATCHES but its budget is +# too small. +# +# Here the allocation tree has a matching wildcard internal-finalized node, so the +# find_map succeeds, but its `budget` is below the computed message_fee. genvm must +# hard-trap in consume_message_fee_internal ("message fee cost exceeds node budget" +# -> oom_trap fees().internal()), never silently emit. This pins the second trap +# gate (budget), distinct from the no-matching-node gate. +class Contract(gl.contract.Contract): + def __init__(self): + gl.contract.get_at(gl.Address(b'\x30' * 20)).emit().foo(1, 2) diff --git a/tests/cases/stable/py/intercontract/alloc_empty_deploy.jsonnet b/tests/cases/stable/py/intercontract/alloc_empty_deploy.jsonnet new file mode 100644 index 00000000..bb714e80 --- /dev/null +++ b/tests/cases/stable/py/intercontract/alloc_empty_deploy.jsonnet @@ -0,0 +1,7 @@ +local simple_deploy = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; +// Empty allocation tree => the DeployContract emit (matched by Address::zero() + +// CallKey::DEPLOY) has no node and must hard-trap (OOM fees internal). +{entry: util.addPaths([simple_deploy.run('${jsonnetDir}/${fileBaseName}.py') { + message_fee_allocation: [], +}])} diff --git a/tests/cases/stable/py/intercontract/alloc_empty_deploy.py b/tests/cases/stable/py/intercontract/alloc_empty_deploy.py new file mode 100644 index 00000000..a3904a5a --- /dev/null +++ b/tests/cases/stable/py/intercontract/alloc_empty_deploy.py @@ -0,0 +1,16 @@ +# { "Depends": "py-genlayer:test" } +import genlayer as gl +from genlayer.types import * + + +# Regression guard (genvm fee-allocation item #8): DeployContract variant. +# +# An empty message-fee-allocation tree must make a deploy emission hard-trap +# (genlayer_sdk.rs DeployContract path: no matching node -> oom_trap fees().internal()), +# never silently emit nothing. +# +# Paired positive case: ../intercontract/deploy.py (default tree -> DeployContract +# emission succeeds). +class Contract(gl.contract.Contract): + def __init__(self): + gl.contract.deploy(code='not really a contract'.encode('utf-8')) diff --git a/tests/cases/stable/py/intercontract/alloc_empty_eth_send.jsonnet b/tests/cases/stable/py/intercontract/alloc_empty_eth_send.jsonnet new file mode 100644 index 00000000..435efea2 --- /dev/null +++ b/tests/cases/stable/py/intercontract/alloc_empty_eth_send.jsonnet @@ -0,0 +1,7 @@ +local simple_deploy = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; +// Empty allocation tree => the external EthSend has no matching node and must +// hard-trap (OOM fees external). +{entry: util.addPaths([simple_deploy.run('${jsonnetDir}/${fileBaseName}.py') { + message_fee_allocation: [], +}])} diff --git a/tests/cases/stable/py/intercontract/alloc_empty_eth_send.py b/tests/cases/stable/py/intercontract/alloc_empty_eth_send.py new file mode 100644 index 00000000..9c850c48 --- /dev/null +++ b/tests/cases/stable/py/intercontract/alloc_empty_eth_send.py @@ -0,0 +1,27 @@ +# { "Depends": "py-genlayer:test" } +import genlayer as gl +from genlayer.types import * + + +# Regression guard (genvm fee-allocation item #8): EthSend (external) variant. +# +# An empty message-fee-allocation tree must make an external (eth) send hard-trap +# (genlayer_sdk.rs EthSend path: no matching node -> oom_trap fees().external()), +# never silently emit nothing. The balance check (value=30 <= 100) passes first, +# so the trap comes from the allocation lookup, not from balance. +# +# Paired positive case: ../intercontract/send_message_eth.py (default tree -> +# EthSend emission succeeds). +@gl.evm.contract_interface +class Ghost: + class View: + pass + + class Write: + def test(self, x: u256, /) -> None: ... + + +class Contract(gl.contract.Contract): + def __init__(self): + print(self.balance) + Ghost(Address(b'\x30' * 20)).emit(value=30).test(10) diff --git a/tests/cases/stable/py/intercontract/alloc_empty_post_message.jsonnet b/tests/cases/stable/py/intercontract/alloc_empty_post_message.jsonnet new file mode 100644 index 00000000..6b9d3de7 --- /dev/null +++ b/tests/cases/stable/py/intercontract/alloc_empty_post_message.jsonnet @@ -0,0 +1,8 @@ +local simple_deploy = import 'templates/simple_deploy.jsonnet'; +local util = import 'templates/util.jsonnet'; +// Empty message-fee-allocation tree => the PostMessage emit has no matching node +// and must hard-trap (OOM fees internal). Overrides the runner default of three +// wildcard catch-all nodes (origin/fees.py DEFAULT_*_MESSAGE_ALLOC). +{entry: util.addPaths([simple_deploy.run('${jsonnetDir}/${fileBaseName}.py') { + message_fee_allocation: [], +}])} diff --git a/tests/cases/stable/py/intercontract/alloc_empty_post_message.py b/tests/cases/stable/py/intercontract/alloc_empty_post_message.py new file mode 100644 index 00000000..32c89513 --- /dev/null +++ b/tests/cases/stable/py/intercontract/alloc_empty_post_message.py @@ -0,0 +1,19 @@ +# { "Depends": "py-genlayer:test" } +import genlayer as gl +from genlayer.types import * + + +# Regression guard (genvm fee-allocation item #8). +# +# genvm is a pure CONSUMER of the message-fee-allocation tree the node provides. +# With an EMPTY tree, emitting an internal message MUST hard-trap (the find_map +# in genlayer_sdk.rs finds no matching node -> `oom_trap(... .fees().internal())`), +# it must NEVER silently emit nothing. A silent-0 here was the exact symptom of +# the consensus/node DisallowedFeeAllocation([]) bug investigated cross-repo; this +# test pins genvm's side so it can't regress to "0 emissions, no error". +# +# Paired positive case: ../intercontract/send_message.py (default wildcard tree +# -> the same emit succeeds with a PostMessage emission). +class Contract(gl.contract.Contract): + def __init__(self): + gl.contract.get_at(gl.Address(b'\x30' * 20)).emit().foo(1, 2)