From c3957acc224617df8b69dd1e19ceab255858340a Mon Sep 17 00:00:00 2001 From: Martin Kourim Date: Fri, 21 Aug 2026 15:16:28 +0200 Subject: [PATCH] fix: cap the timerange offset with the stability window The Plutus "time range" minting tests set the tx validity interval 300 slots past the tip. The ledger has to translate that upper bound to a time for the script, and it can do so only up to its forecast horizon: the end of the first epoch that starts at or after the stability window (`3k/f` slots) from the tip. On the `leios_fast` testnet variant the window is only 240 slots (`securityParam` 4, `activeSlotsCoeff` 0.05), so a 300 slot interval reaches past the horizon whenever the tip is 300 to 240 slots before an epoch boundary - 60 of the 800 slots of an epoch - and the submission fails with `TimeTranslationPastHorizon`. Cap the offset with the window, which is by definition always inside the horizon. Variants where the window is at least 300 slots, which is every other one, are unaffected. The cap lives in `plutus_common.get_timerange_slots_offset`, with the 300 slots as `TIMERANGE_SLOTS_OFFSET`. It builds on the new `clusterlib_utils.get_stability_window`, which `test_ttl_horizon` uses too instead of computing `3k/f` inline. The window is rounded down, as a caller using it as a limit needs a value that is never above the window the ledger itself uses; every testnet variant divides exactly, so no value changes. --- cardano_node_tests/tests/plutus_common.py | 27 +++++++++++++++++++ .../tests/tests_plutus/test_mint_build.py | 9 +++---- .../tests/tests_plutus/test_mint_raw.py | 10 +++---- cardano_node_tests/utils/clusterlib_utils.py | 19 +++++++++++++ 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/cardano_node_tests/tests/plutus_common.py b/cardano_node_tests/tests/plutus_common.py index b7280b6a4..6f628d71e 100644 --- a/cardano_node_tests/tests/plutus_common.py +++ b/cardano_node_tests/tests/plutus_common.py @@ -304,6 +304,33 @@ class PlutusScriptData: ) +# Slots between the tip and the end of the validity interval of a transaction that runs +# a "time range" script. Long enough for the transaction to get submitted and land in a +# block, and capped by `get_timerange_slots_offset`. +TIMERANGE_SLOTS_OFFSET = 300 + + +def get_timerange_slots_offset(*, cluster_obj: clusterlib.ClusterLib) -> int: + """Return the slot offset to use for the validity interval of a "time range" script. + + A script that looks at the transaction validity interval makes the ledger translate + the end of the interval to a time, which it can do only up to its forecast horizon: + the end of the first epoch that starts at or after the stability window from the tip. + An interval reaching past that fails the script with `TimeTranslationPastHorizon`, so + `TIMERANGE_SLOTS_OFFSET` is capped with the stability window, which is by definition + always inside the horizon. + + Args: + cluster_obj: An instance of `clusterlib.ClusterLib`. + + Returns: + int: The number of slots to set the validity interval to, counted from the tip. + """ + return min( + TIMERANGE_SLOTS_OFFSET, clusterlib_utils.get_stability_window(cluster_obj=cluster_obj) + ) + + def rotate_shift_bitwise_fails(protocol_version: int) -> bool: """Check if the rotate/shift bitwise builtin scripts fail on the given protocol version. diff --git a/cardano_node_tests/tests/tests_plutus/test_mint_build.py b/cardano_node_tests/tests/tests_plutus/test_mint_build.py index 61249be88..2735407ab 100644 --- a/cardano_node_tests/tests/tests_plutus/test_mint_build.py +++ b/cardano_node_tests/tests/tests_plutus/test_mint_build.py @@ -405,7 +405,7 @@ def test_time_range_minting( # Step 2: mint the "qacoin" slot_step2 = cluster.g_query.get_slot_no() - slots_offset = 300 + slots_offset = plutus_common.get_timerange_slots_offset(cluster_obj=cluster) timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 # POSIX timestamp + offset @@ -653,7 +653,7 @@ def test_two_scripts_minting( ] # "time range" qacoin - slots_offset = 300 + slots_offset = plutus_common.get_timerange_slots_offset(cluster_obj=cluster) timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 # POSIX timestamp + offset @@ -997,10 +997,7 @@ def test_ttl_horizon( *mint_txouts, ] - # Calculate 3k/f - offset_3kf = round( - 3 * cluster.genesis["securityParam"] / cluster.genesis["activeSlotsCoeff"] - ) + offset_3kf = clusterlib_utils.get_stability_window(cluster_obj=cluster) # Use 3k/f + `epoch_length` slots for ttl - this will not meet the `expect_pass` condition if ttl_offset == -1: diff --git a/cardano_node_tests/tests/tests_plutus/test_mint_raw.py b/cardano_node_tests/tests/tests_plutus/test_mint_raw.py index f31475b38..f13a39871 100644 --- a/cardano_node_tests/tests/tests_plutus/test_mint_raw.py +++ b/cardano_node_tests/tests/tests_plutus/test_mint_raw.py @@ -14,6 +14,7 @@ from cardano_node_tests.tests import common from cardano_node_tests.tests import plutus_common from cardano_node_tests.tests.tests_plutus import mint_raw +from cardano_node_tests.utils import clusterlib_utils from cardano_node_tests.utils import dbsync_utils from cardano_node_tests.utils import helpers from cardano_node_tests.utils import tx_view @@ -422,7 +423,7 @@ def test_time_range_minting( # Step 2: mint the "qacoin" slot_step2 = cluster.g_query.get_slot_no() - slots_offset = 300 + slots_offset = plutus_common.get_timerange_slots_offset(cluster_obj=cluster) timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 # POSIX timestamp + offset @@ -625,7 +626,7 @@ def test_two_scripts_minting( ] # "timerange" qacoin - slots_offset = 300 + slots_offset = plutus_common.get_timerange_slots_offset(cluster_obj=cluster) timestamp_offset_ms = int(slots_offset * cluster.slot_length + 5) * 1_000 # POSIX timestamp + offset @@ -1117,10 +1118,7 @@ def test_ttl_horizon( *mint_txouts, ] - # Calculate 3k/f - offset_3kf = round( - 3 * cluster.genesis["securityParam"] / cluster.genesis["activeSlotsCoeff"] - ) + offset_3kf = clusterlib_utils.get_stability_window(cluster_obj=cluster) # Use 3k/f + `epoch_length` slots for ttl - this will not meet the `expect_pass` condition if ttl_offset == -1: diff --git a/cardano_node_tests/utils/clusterlib_utils.py b/cardano_node_tests/utils/clusterlib_utils.py index 5f5c40e5d..4a7a60795 100644 --- a/cardano_node_tests/utils/clusterlib_utils.py +++ b/cardano_node_tests/utils/clusterlib_utils.py @@ -1159,6 +1159,25 @@ def save_ledger_state( return json_file +def get_stability_window(*, cluster_obj: clusterlib.ClusterLib) -> int: + """Return the stability window (`3k/f`) in slots. + + Rounded down, so that the result is never above the window the ledger itself uses. A + caller that uses it as a limit therefore stays inside the window. The division is + exact on every testnet variant, so the rounding only guards against a variant that + picks a `securityParam` and `activeSlotsCoeff` that don't divide. + + Args: + cluster_obj: An instance of `clusterlib.ClusterLib`. + + Returns: + int: The number of slots in the stability window. + """ + security_param = int(cluster_obj.genesis["securityParam"]) + active_slots_coeff = float(cluster_obj.genesis["activeSlotsCoeff"]) + return math.floor(3 * security_param / active_slots_coeff) + + def wait_for_epoch_interval( *, cluster_obj: clusterlib.ClusterLib,