Skip to content

Report: Malicious Test Sabotage and CI Workflow Deletion#975

Closed
a1denvalu3 wants to merge 14 commits intomainfrom
report/malicious-test-sabotage-ci-deletion
Closed

Report: Malicious Test Sabotage and CI Workflow Deletion#975
a1denvalu3 wants to merge 14 commits intomainfrom
report/malicious-test-sabotage-ci-deletion

Conversation

@a1denvalu3
Copy link
Copy Markdown
Collaborator

Security audit finding for cashubtc/nutshell PR #938.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 7, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
649 2 647 74
View the top 3 failed test(s) by shortest run time
tests.mint.test_mint_init::test_regtest_check_nonexisting_melt_quote
Stack Traces | 1.05s run time
self = <sqlalchemy.dialects.postgresql.asyncpg.AsyncAdapt_asyncpg_cursor object at 0x7f6efd926260>
operation = 'DROP SCHEMA public CASCADE;', parameters = ()

    async def _prepare_and_execute(self, operation, parameters):
        adapt_connection = self._adapt_connection
    
        async with adapt_connection._execute_mutex:
            if not adapt_connection._started:
                await adapt_connection._start_transaction()
    
            if parameters is None:
                parameters = ()
    
            try:
                prepared_stmt, attributes = await adapt_connection._prepare(
                    operation, self._invalidate_schema_cache_asof
                )
    
                if attributes:
                    self.description = [
                        (
                            attr.name,
                            attr.type.oid,
                            None,
                            None,
                            None,
                            None,
                            None,
                        )
                        for attr in attributes
                    ]
                else:
                    self.description = None
    
                if self.server_side:
                    self._cursor = await prepared_stmt.cursor(*parameters)
                    self.rowcount = -1
                else:
>                   self._rows = deque(await prepared_stmt.fetch(*parameters))

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:545: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10........./site-packages/asyncpg/prepared_stmt.py:176: in fetch
    data = await self.__bind_execute(args, 0, timeout)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10........./site-packages/asyncpg/prepared_stmt.py:267: in __bind_execute
    data, status, _ = await self.__do_execute(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10........./site-packages/asyncpg/prepared_stmt.py:256: in __do_execute
    return await executor(protocol)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

>   ???
E   asyncpg.exceptions.DeadlockDetectedError: deadlock detected
E   DETAIL:  Process 299 waits for AccessExclusiveLock on relation 41208 of database 16384; blocked by process 93.
E   Process 93 waits for RowExclusiveLock on relation 41139 of database 16384; blocked by process 299.
E   HINT:  See server log for query details.

asyncpg/protocol/protocol.pyx:206: DeadlockDetectedError

The above exception was the direct cause of the following exception:

self = <sqlalchemy.engine.base.Connection object at 0x7f6efe61cc70>
dialect = <sqlalchemy.dialects.postgresql.asyncpg.PGDialect_asyncpg object at 0x7f6efde6dd80>
context = <sqlalchemy.dialects.postgresql.asyncpg.PGExecutionContext_asyncpg object at 0x7f6efe63f850>
statement = <sqlalchemy.dialects.postgresql.asyncpg.PGCompiler_asyncpg object at 0x7f6efe63fe80>
parameters = [()]

    def _exec_single_context(
        self,
        dialect: Dialect,
        context: ExecutionContext,
        statement: Union[str, Compiled],
        parameters: Optional[_AnyMultiExecuteParams],
    ) -> CursorResult[Any]:
        """continue the _execute_context() method for a single DBAPI
        cursor.execute() or cursor.executemany() call.
    
        """
        if dialect.bind_typing is BindTyping.SETINPUTSIZES:
            generic_setinputsizes = context._prepare_set_input_sizes()
    
            if generic_setinputsizes:
                try:
                    dialect.do_set_input_sizes(
                        context.cursor, generic_setinputsizes, context
                    )
                except BaseException as e:
                    self._handle_dbapi_exception(
                        e, str(statement), parameters, None, context
                    )
    
        cursor, str_statement, parameters = (
            context.cursor,
            context.statement,
            context.parameters,
        )
    
        effective_parameters: Optional[_AnyExecuteParams]
    
        if not context.executemany:
            effective_parameters = parameters[0]
        else:
            effective_parameters = parameters
    
        if self._has_events or self.engine._has_events:
            for fn in self.dispatch.before_cursor_execute:
                str_statement, effective_parameters = fn(
                    self,
                    cursor,
                    str_statement,
                    effective_parameters,
                    context,
                    context.executemany,
                )
    
        if self._echo:
            self._log_info(str_statement)
    
            stats = context._get_cache_stats()
    
            if not self.engine.hide_parameters:
                self._log_info(
                    "[%s] %r",
                    stats,
                    sql_util._repr_params(
                        effective_parameters,
                        batches=10,
                        ismulti=context.executemany,
                    ),
                )
            else:
                self._log_info(
                    "[%s] [SQL parameters hidden due to hide_parameters=True]",
                    stats,
                )
    
        evt_handled: bool = False
        try:
            if context.execute_style is ExecuteStyle.EXECUTEMANY:
                effective_parameters = cast(
                    "_CoreMultiExecuteParams", effective_parameters
                )
                if self.dialect._has_events:
                    for fn in self.dialect.dispatch.do_executemany:
                        if fn(
                            cursor,
                            str_statement,
                            effective_parameters,
                            context,
                        ):
                            evt_handled = True
                            break
                if not evt_handled:
                    self.dialect.do_executemany(
                        cursor,
                        str_statement,
                        effective_parameters,
                        context,
                    )
            elif not effective_parameters and context.no_parameters:
                if self.dialect._has_events:
                    for fn in self.dialect.dispatch.do_execute_no_params:
                        if fn(cursor, str_statement, context):
                            evt_handled = True
                            break
                if not evt_handled:
                    self.dialect.do_execute_no_params(
                        cursor, str_statement, context
                    )
            else:
                effective_parameters = cast(
                    "_CoreSingleExecuteParams", effective_parameters
                )
                if self.dialect._has_events:
                    for fn in self.dialect.dispatch.do_execute:
                        if fn(
                            cursor,
                            str_statement,
                            effective_parameters,
                            context,
                        ):
                            evt_handled = True
                            break
                if not evt_handled:
>                   self.dialect.do_execute(
                        cursor, str_statement, effective_parameters, context
                    )

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/base.py:1964: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/default.py:942: in do_execute
    cursor.execute(statement, parameters)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:580: in execute
    self._adapt_connection.await_(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/util/_concurrency_py3k.py:132: in await_only
    return current.parent.switch(awaitable)  # type: ignore[no-any-return,attr-defined] # noqa: E501
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/util/_concurrency_py3k.py:196: in greenlet_spawn
    value = await result
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:558: in _prepare_and_execute
    self._handle_exception(error)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:508: in _handle_exception
    self._adapt_connection._handle_exception(error)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <AdaptedConnection <asyncpg.connection.Connection object at 0x7f6efde707c0>>
error = DeadlockDetectedError('deadlock detected')

    def _handle_exception(self, error):
        if self._connection.is_closed():
            self._transaction = None
            self._started = False
    
        if not isinstance(error, AsyncAdapt_asyncpg_dbapi.Error):
            exception_mapping = self.dbapi._asyncpg_error_translate
    
            for super_ in type(error).__mro__:
                if super_ in exception_mapping:
                    translated_error = exception_mapping[super_](
                        "%s: %s" % (type(error), error)
                    )
                    translated_error.pgcode = translated_error.sqlstate = (
                        getattr(error, "sqlstate", None)
                    )
>                   raise translated_error from error
E                   sqlalchemy.dialects.postgresql.asyncpg.AsyncAdapt_asyncpg_dbapi.Error: <class 'asyncpg.exceptions.DeadlockDetectedError'>: deadlock detected
E                   DETAIL:  Process 299 waits for AccessExclusiveLock on relation 41208 of database 16384; blocked by process 93.
E                   Process 93 waits for RowExclusiveLock on relation 41139 of database 16384; blocked by process 299.
E                   HINT:  See server log for query details.

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:792: Error

The above exception was the direct cause of the following exception:

request = <SubRequest 'ledger' for <Coroutine test_regtest_check_nonexisting_melt_quote>>
kwargs = {}, func = <function ledger at 0x7f6eff4303a0>
event_loop_fixture_id = 'event_loop'
setup = <function _wrap_asyncgen_fixture.<locals>._asyncgen_fixture_wrapper.<locals>.setup at 0x7f6efdf7eef0>
finalizer = <function _wrap_asyncgen_fixture.<locals>._asyncgen_fixture_wrapper.<locals>.finalizer at 0x7f6efdf7ee60>

    @functools.wraps(fixture)
    def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any):
        func = _perhaps_rebind_fixture_func(fixture, request.instance)
        event_loop_fixture_id = _get_event_loop_fixture_id_for_async_fixture(
            request, func
        )
        event_loop = request.getfixturevalue(event_loop_fixture_id)
        kwargs.pop(event_loop_fixture_id, None)
        gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request))
    
        async def setup():
            res = await gen_obj.__anext__()  # type: ignore[union-attr]
            return res
    
        def finalizer() -> None:
            """Yield again, to finalize."""
    
            async def async_finalizer() -> None:
                try:
                    await gen_obj.__anext__()  # type: ignore[union-attr]
                except StopAsyncIteration:
                    pass
                else:
                    msg = "Async generator fixture didn't stop."
                    msg += "Yield only once."
                    raise ValueError(msg)
    
            event_loop.run_until_complete(async_finalizer())
    
>       result = event_loop.run_until_complete(setup())

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10....../site-packages/pytest_asyncio/plugin.py:343: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.20.../x64/lib/python3.10/asyncio/base_events.py:649: in run_until_complete
    return future.result()
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10....../site-packages/pytest_asyncio/plugin.py:325: in setup
    res = await gen_obj.__anext__()  # type: ignore[union-attr]
tests/conftest.py:100: in ledger
    await conn.execute("DROP SCHEMA public CASCADE;")
cashu/core/db.py:95: in execute
    return await self.conn.execute(self.rewrite_query(query), values)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../ext/asyncio/session.py:463: in execute
    result = await greenlet_spawn(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/util/_concurrency_py3k.py:201: in greenlet_spawn
    result = context.throw(*sys.exc_info())
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/orm/session.py:2365: in execute
    return self._execute_internal(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/orm/session.py:2260: in _execute_internal
    result = conn.execute(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/base.py:1416: in execute
    return meth(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/sql/elements.py:515: in _execute_on_connection
    return connection._execute_clauseelement(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/base.py:1638: in _execute_clauseelement
    ret = self._execute_context(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/base.py:1843: in _execute_context
    return self._exec_single_context(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/base.py:1983: in _exec_single_context
    self._handle_dbapi_exception(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/base.py:2352: in _handle_dbapi_exception
    raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/base.py:1964: in _exec_single_context
    self.dialect.do_execute(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/engine/default.py:942: in do_execute
    cursor.execute(statement, parameters)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:580: in execute
    self._adapt_connection.await_(
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/util/_concurrency_py3k.py:132: in await_only
    return current.parent.switch(awaitable)  # type: ignore[no-any-return,attr-defined] # noqa: E501
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../sqlalchemy/util/_concurrency_py3k.py:196: in greenlet_spawn
    value = await result
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:558: in _prepare_and_execute
    self._handle_exception(error)
../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:508: in _handle_exception
    self._adapt_connection._handle_exception(error)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <AdaptedConnection <asyncpg.connection.Connection object at 0x7f6efde707c0>>
error = DeadlockDetectedError('deadlock detected')

    def _handle_exception(self, error):
        if self._connection.is_closed():
            self._transaction = None
            self._started = False
    
        if not isinstance(error, AsyncAdapt_asyncpg_dbapi.Error):
            exception_mapping = self.dbapi._asyncpg_error_translate
    
            for super_ in type(error).__mro__:
                if super_ in exception_mapping:
                    translated_error = exception_mapping[super_](
                        "%s: %s" % (type(error), error)
                    )
                    translated_error.pgcode = translated_error.sqlstate = (
                        getattr(error, "sqlstate", None)
                    )
>                   raise translated_error from error
E                   sqlalchemy.exc.DBAPIError: (sqlalchemy.dialects.postgresql.asyncpg.Error) <class 'asyncpg.exceptions.DeadlockDetectedError'>: deadlock detected
E                   DETAIL:  Process 299 waits for AccessExclusiveLock on relation 41208 of database 16384; blocked by process 93.
E                   Process 93 waits for RowExclusiveLock on relation 41139 of database 16384; blocked by process 299.
E                   HINT:  See server log for query details.
E                   [SQL: DROP SCHEMA public CASCADE;]
E                   (Background on this error at: https://sqlalche..../e/20/dbapi)

../../../..../pypoetry/virtualenvs/cashu-7hbqvzQf-py3.10/lib/python3.10.../dialects/postgresql/asyncpg.py:792: DBAPIError
tests.wallet.test_wallet::test_mint
Stack Traces | 1.89s run time
wallet1 = <cashu.wallet.wallet.Wallet object at 0x7f1b09c40e20>

    @pytest.mark.asyncio
    async def test_mint(wallet1: Wallet):
        mint_quote = await wallet1.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        if not settings.debug_mint_only_deprecated:
            mint_quote = await wallet1.get_mint_quote(mint_quote.quote)
            assert mint_quote.request == mint_quote.request
            assert mint_quote.state == MintQuoteState.paid, f"Unexpected quote state: {mint_quote.state}"
    
        expected_proof_amounts = wallet1.split_wallet_state(64)
        await wallet1.mint(64, quote_id=mint_quote.quote)
        assert wallet1.balance == 64, f"Balance mismatch after mint: expected 64, got {wallet1.balance}"
        assert wallet1.balance == 64, f"Total balance mismatch: expected 64, got {wallet1.balance}"
>       assert wallet1.available_balance == 32, f"Available balance mismatch: expected 32, got {wallet1.available_balance}"
E       AssertionError: Available balance mismatch: expected 32, got 64 sat
E       assert 64 sat == 32
E        +  where 64 sat = <cashu.wallet.wallet.Wallet object at 0x7f1b09c40e20>.available_balance

tests/wallet/test_wallet.py:190: AssertionError
tests.test_mint_watchdog::test_balance_update_on_mint
Stack Traces | 2.44s run time
wallet = <cashu.wallet.wallet.Wallet object at 0x7f1b093a4fd0>
ledger = <cashu.mint.ledger.Ledger object at 0x7f1b093a4250>

    @pytest.mark.asyncio
    async def test_balance_update_on_mint(wallet: Wallet, ledger: Ledger):
        balance_before, fees_paid_before = await ledger.get_unit_balance_and_fees(
            Unit.sat, ledger.db
        )
        mint_quote = await wallet.request_mint(64)
        await pay_if_regtest(mint_quote.request)
        await wallet.mint(64, quote_id=mint_quote.quote)
        assert wallet.balance == 64
    
        balance_after, fees_paid_after = await ledger.get_unit_balance_and_fees(
            Unit.sat, ledger.db
        )
        assert balance_after == balance_before + 64
>       assert fees_paid, "Watchdog: Mint fees were not correctly settled"
E       NameError: name 'fees_paid' is not defined

tests/test_mint_watchdog.py:54: NameError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@a1denvalu3 a1denvalu3 closed this Apr 7, 2026
@github-project-automation github-project-automation bot moved this from Backlog to Done in nutshell Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants