From 227e2954fc29cb3f6437a2511d8a925d7d5ea5c9 Mon Sep 17 00:00:00 2001 From: Martin Kourim Date: Tue, 6 Oct 2026 14:37:13 +0200 Subject: [PATCH] fix(utils): wait for txins in check_txins_spent Previously `check_txins_spent` waited a fixed 2 blocks and assumed the transaction was included by then. Now it checks right away and if some txins are still unspent, waits `confirm_blocks` new blocks and checks again, up to `attempts` times, like `submit_tx` does (without resubmitting). It also accepts `TxId#TxIx` strings. Use it in places that submitted with `submit_tx_bare` and then waited a fixed number of blocks: * collateral tests wait until the collateral is spent, then report `consensus_973` on timeout * node upgrade test waits for the inputs read from the tx file * `defragment_utxos` waits for one txin per submitted tx, which keeps the query within the command line length limit --- cardano_node_tests/tests/test_node_upgrade.py | 7 ++- .../tests/tests_plutus/spend_build.py | 12 ++--- .../tests/tests_plutus/spend_raw.py | 12 ++--- .../test_spend_collateral_build.py | 24 ++++----- .../test_spend_collateral_raw.py | 24 ++++----- cardano_node_tests/utils/clusterlib_utils.py | 52 ++++++++++++++++--- cardano_node_tests/utils/defragment_utxos.py | 17 ++++-- 7 files changed, 99 insertions(+), 49 deletions(-) diff --git a/cardano_node_tests/tests/test_node_upgrade.py b/cardano_node_tests/tests/test_node_upgrade.py index 2abef29a7..91732f2b3 100644 --- a/cardano_node_tests/tests/test_node_upgrade.py +++ b/cardano_node_tests/tests/test_node_upgrade.py @@ -486,7 +486,7 @@ def test_submit_tx( * Submit the transaction to the upgraded node * Wait for transaction to be included in a block * Verify transaction was successfully submitted and processed - * Check expected balances after transaction submission + * Verify transaction inputs were spent * Demonstrates backward compatibility of transaction format across versions/eras """ temp_template = common.get_test_id(cluster) @@ -515,4 +515,7 @@ def test_submit_tx( ) cluster.g_transaction.submit_tx_bare(tx_file=tx_file) - cluster.wait_for_new_block(2) + + txins = cluster.g_transaction.view_tx_dict(tx_file=tx_file).get("inputs") or [] + assert txins, "No inputs found in the transaction" + clusterlib_utils.check_txins_spent(cluster_obj=cluster, txins=txins) diff --git a/cardano_node_tests/tests/tests_plutus/spend_build.py b/cardano_node_tests/tests/tests_plutus/spend_build.py index 25306589f..ce721b826 100644 --- a/cardano_node_tests/tests/tests_plutus/spend_build.py +++ b/cardano_node_tests/tests/tests_plutus/spend_build.py @@ -292,7 +292,12 @@ def _build_spend_locked_txin( # noqa: C901 if not script_valid: cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed) - cluster_obj.wait_for_new_block(new_blocks=2) + # Check that the collateral UTxO was spent + try: + clusterlib_utils.check_txins_spent(cluster_obj=cluster_obj, txins=collateral_utxos) + except AssertionError: + issues.consensus_973.finish_test() + try: cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed) except clusterlib.CLIError as exc: @@ -308,11 +313,6 @@ def _build_spend_locked_txin( # noqa: C901 else: pytest.fail("Transaction was not submitted successfully") - # Check that the collateral UTxO was spent - spent_collateral_utxo = cluster_obj.g_query.get_utxo(utxo=collateral_utxos) - if spent_collateral_utxo: - issues.consensus_973.finish_test() - assert ( cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance - collateral_utxos[0].amount diff --git a/cardano_node_tests/tests/tests_plutus/spend_raw.py b/cardano_node_tests/tests/tests_plutus/spend_raw.py index 8b3799e93..0218deab6 100644 --- a/cardano_node_tests/tests/tests_plutus/spend_raw.py +++ b/cardano_node_tests/tests/tests_plutus/spend_raw.py @@ -293,7 +293,12 @@ def _spend_locked_txin( # noqa: C901 if not script_valid: cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed) - cluster_obj.wait_for_new_block(new_blocks=2) + # Check that the collateral UTxO was spent + try: + clusterlib_utils.check_txins_spent(cluster_obj=cluster_obj, txins=collateral_utxos) + except AssertionError: + issues.consensus_973.finish_test() + try: cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed) except clusterlib.CLIError as exc: @@ -303,11 +308,6 @@ def _spend_locked_txin( # noqa: C901 else: pytest.fail("Transaction was not submitted successfully") - # Check that the collateral UTxO was spent - spent_collateral_utxo = cluster_obj.g_query.get_utxo(utxo=collateral_utxos) - if spent_collateral_utxo: - issues.consensus_973.finish_test() - assert ( cluster_obj.g_query.get_address_balance(dst_addr.address) == dst_init_balance - collateral_utxos[0].amount diff --git a/cardano_node_tests/tests/tests_plutus_v2/test_spend_collateral_build.py b/cardano_node_tests/tests/tests_plutus_v2/test_spend_collateral_build.py index 7f6480dd5..a0dbd7164 100644 --- a/cardano_node_tests/tests/tests_plutus_v2/test_spend_collateral_build.py +++ b/cardano_node_tests/tests/tests_plutus_v2/test_spend_collateral_build.py @@ -199,7 +199,12 @@ def test_with_total_return_collateral( issues.consensus_947.finish_test() raise - cluster.wait_for_new_block(new_blocks=2) + # Check that the collateral UTxO was spent + try: + clusterlib_utils.check_txins_spent(cluster_obj=cluster, txins=collateral_utxos) + except AssertionError: + issues.consensus_973.finish_test() + try: cluster.g_transaction.submit_tx_bare( tx_file=tx_output_redeem.out_file.with_suffix(".signed") @@ -217,11 +222,6 @@ def test_with_total_return_collateral( else: pytest.fail("Transaction was not submitted successfully") - # Check that collateral was taken - spent_collateral_utxo = cluster.g_query.get_utxo(utxo=collateral_utxos) - if spent_collateral_utxo: - issues.consensus_973.finish_test() - # Check that input UTxOs were not spent assert cluster.g_query.get_utxo(utxo=tx_output_redeem.txins), "Payment UTxO was spent" assert cluster.g_query.get_utxo(utxo=script_utxos), "Script UTxO was spent" @@ -346,7 +346,12 @@ def test_collateral_with_tokens( issues.consensus_947.finish_test() raise - cluster.wait_for_new_block(new_blocks=2) + # Check that the collateral UTxO was spent + try: + clusterlib_utils.check_txins_spent(cluster_obj=cluster, txins=collateral_utxos) + except AssertionError: + issues.consensus_973.finish_test() + try: cluster.g_transaction.submit_tx_bare( tx_file=tx_output_redeem.out_file.with_suffix(".signed") @@ -364,11 +369,6 @@ def test_collateral_with_tokens( else: pytest.fail("Transaction was not submitted successfully") - # Check that collateral was taken - spent_collateral_utxo = cluster.g_query.get_utxo(utxo=collateral_utxos) - if spent_collateral_utxo: - issues.consensus_973.finish_test() - # Check that input UTxOs were not spent assert cluster.g_query.get_utxo(utxo=tx_output_redeem.txins), "Payment UTxO was spent" assert cluster.g_query.get_utxo(utxo=script_utxos), "Script UTxO was spent" diff --git a/cardano_node_tests/tests/tests_plutus_v2/test_spend_collateral_raw.py b/cardano_node_tests/tests/tests_plutus_v2/test_spend_collateral_raw.py index 9d2a27b5d..4f6c8b43b 100644 --- a/cardano_node_tests/tests/tests_plutus_v2/test_spend_collateral_raw.py +++ b/cardano_node_tests/tests/tests_plutus_v2/test_spend_collateral_raw.py @@ -162,7 +162,12 @@ def test_with_total_return_collateral( raise issues.consensus_947.finish_test() - cluster.wait_for_new_block(new_blocks=2) + # Check that the collateral UTxO was spent + try: + clusterlib_utils.check_txins_spent(cluster_obj=cluster, txins=collateral_utxos) + except AssertionError: + issues.consensus_973.finish_test() + try: cluster.g_transaction.submit_tx_bare(tx_file=tx_signed_redeem) except clusterlib.CLIError as exc: @@ -178,11 +183,6 @@ def test_with_total_return_collateral( else: pytest.fail("Transaction was not submitted successfully") - # Check that the collateral UTxO was spent - spent_collateral_utxo = cluster.g_query.get_utxo(utxo=collateral_utxos) - if spent_collateral_utxo: - issues.consensus_973.finish_test() - # Check that the right amount of collateral was spent dst_balance = cluster.g_query.get_address_balance(dst_addr.address) assert dst_balance == dst_init_balance - redeem_cost.collateral, ( @@ -323,7 +323,12 @@ def test_collateral_with_tokens( raise issues.consensus_947.finish_test() - cluster.wait_for_new_block(new_blocks=2) + # Check that the collateral UTxO was spent + try: + clusterlib_utils.check_txins_spent(cluster_obj=cluster, txins=collateral_utxos) + except AssertionError: + issues.consensus_973.finish_test() + try: cluster.g_transaction.submit_tx_bare(tx_file=tx_signed_redeem) except clusterlib.CLIError as exc: @@ -339,11 +344,6 @@ def test_collateral_with_tokens( else: pytest.fail("Transaction was not submitted successfully") - # Check that the collateral UTxO was spent - spent_collateral_utxo = cluster.g_query.get_utxo(utxo=collateral_utxos) - if spent_collateral_utxo: - issues.consensus_973.finish_test() - # Check that the right amount of collateral was spent and that the tokens were returned txid_redeem = cluster.g_transaction.get_txid(tx_body_file=tx_output_redeem.out_file) diff --git a/cardano_node_tests/utils/clusterlib_utils.py b/cardano_node_tests/utils/clusterlib_utils.py index c521ac973..fa6f5ac12 100644 --- a/cardano_node_tests/utils/clusterlib_utils.py +++ b/cardano_node_tests/utils/clusterlib_utils.py @@ -1585,17 +1585,53 @@ def cli_has(command: str) -> bool: def check_txins_spent( - *, cluster_obj: clusterlib.ClusterLib, txins: list[clusterlib.UTXOData], wait_blocks: int = 2 + *, + cluster_obj: clusterlib.ClusterLib, + txins: tp.Sequence[clusterlib.UTXOData | str], + wait_blocks: int | None = None, + attempts: int = 10, ) -> None: - """Check that txins were spent.""" - if wait_blocks > 0: - cluster_obj.wait_for_new_block(wait_blocks) + """Wait until txins are spent, i.e. until the transactions spending them make it to the chain. - utxo_data = cluster_obj.g_query.get_utxo(utxo=txins) + The txins are checked right away, as the transactions may already be on chain. If some + of the txins are not spent yet, wait for new blocks and check again. Unlike + `submit_tx`, the transactions are not resubmitted. - if utxo_data: - msg = f"Some txins were not spent: {txins}" - raise AssertionError(msg) + Args: + cluster_obj: An instance of `clusterlib.ClusterLib`. + txins: Input UTxOs, as `clusterlib.UTXOData` records or `TxId#TxIx` strings. + wait_blocks: A number of new blocks to wait for before checking again + (default = `cluster_obj.confirm_blocks`). + attempts: Max number of checks after waiting for new blocks. With 0, check just once, + without waiting. + + Raises: + AssertionError: If some of the txins were not spent in time. + """ + txins_str = [t if isinstance(t, str) else f"{t.utxo_hash}#{t.utxo_ix}" for t in txins] + if not txins_str: + return + + wait_blocks = cluster_obj.confirm_blocks if wait_blocks is None else wait_blocks + if wait_blocks < 1: + msg = f"`wait_blocks` must be a positive number, got {wait_blocks}." + raise ValueError(msg) + + unspent: list[clusterlib.UTXOData] = [] + for r in range(attempts + 1): + if r > 0: + cluster_obj.wait_for_new_block(new_blocks=wait_blocks) + + unspent = cluster_obj.g_query.get_utxo(txin=txins_str) + if not unspent: + return + + unspent_str = sorted({f"{u.utxo_hash}#{u.utxo_ix}" for u in unspent}) + msg = ( + f"Some txins were not spent after waiting for {attempts * wait_blocks} blocks: " + f"{unspent_str}" + ) + raise AssertionError(msg) def create_reference_utxo( diff --git a/cardano_node_tests/utils/defragment_utxos.py b/cardano_node_tests/utils/defragment_utxos.py index d1fcb940b..d4ca464d5 100644 --- a/cardano_node_tests/utils/defragment_utxos.py +++ b/cardano_node_tests/utils/defragment_utxos.py @@ -5,6 +5,8 @@ from cardano_clusterlib import clusterlib +from cardano_node_tests.utils import clusterlib_utils + LOGGER = logging.getLogger(__name__) @@ -17,7 +19,6 @@ def defragment( name_template: str = "", ) -> None: """Defragment address UTxOs.""" - new_blocks = 3 name_template = f"{name_template}_" if name_template else "" loop = 1 @@ -41,6 +42,7 @@ def defragment( batch_size = min(100, utxos_len) batch_num = 1 + loop_txins: list[clusterlib.UTXOData] = [] for b in range(0, utxos_len, batch_size): LOGGER.info(f"Defragmenting UTxOs: Running loop {loop}, batch {batch_num}") batch = utxos[b : b + batch_size] @@ -58,8 +60,17 @@ def defragment( signing_key_files=[skey_file], ) cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed_file) + # A Tx is applied atomically, so it is enough to check a single txin per Tx. + # Checking all of them could exceed the command line length limit. + loop_txins.append(tx_output.txins[0]) batch_num += 1 - LOGGER.info(f"Defragmenting UTxOs: Waiting for {new_blocks} new blocks after loop {loop}") - cluster_obj.wait_for_new_block(new_blocks=new_blocks) + LOGGER.info( + f"Defragmenting UTxOs: Waiting for the Txs of loop {loop} to make it to the chain" + ) + try: + clusterlib_utils.check_txins_spent(cluster_obj=cluster_obj, txins=loop_txins) + except AssertionError: + # Defragmentation is best-effort, the next loop works with the current UTxOs + LOGGER.warning(f"Defragmenting UTxOs: Some Txs of loop {loop} didn't make it") loop += 1