Skip to content

Conversation

@hodlinator
Copy link
Contributor

@hodlinator hodlinator commented Jan 23, 2025

Facilitates debugging specific instances of bitcoind in the context of Python tests, inside of an editor/IDE/debugger.

Makes bitcoind spin during startup, waiting for a debugger to be attached, so that breakpoints during process init can be debugged.

Example usage

  1. Generate build files with -DENABLE_WAIT_FOR_DEBUGGER=ON & build.

    cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_WAIT_FOR_DEBUGGER=ON && cmake --build build -j$(nproc)
    
  2. Set breakpoints in the C++ code in your editor/IDE/debugger.

  3. Start a Python functional test with a debugger command such as:

    • LLDB inside an already started Tmux pane (append -o continue to the LLDB args to have the debugger continue automatically):
      ./build/test/functional/feature_loadblock.py --debug_runs 2 --debug_cmd "tmux split-window -h lldb -p \$PID\$"
      
    • Sublime Text Debugger, allows a running session to attach:
      ./build/test/functional/feature_loadblock.py --debug_runs 2 --debug_cmd "subl --command \"debugger {\\\"action\\\": \\\"start\\\", \\\"configuration\\\": {\\\"name\\\": \\\"foo\\\", \\\"type\\\": \\\"lldb\\\", \\\"request\\\": \\\"attach\\\", \\\"program\\\": \\\"dummyprogram\\\", \\\"pid\\\": \\\"\$PID\$\\\"}}\""
      
    • Don't specify --debug_cmd ...\$PID\$..., instead have the test framework log which bitcoind PID you should manually attach to:
      ./build/test/functional/feature_loadblock.py --debug_runs 2
      
  4. Experience how debugger hits breakpoints as the functional test executes RPCs.

Using -debug_runs <RUN_INDEX> often requires doing a dry-run of a functional test with -ldebug and examining the log to see which run index is the one we want to debug:

2025-...Z TestFramework.node0 (DEBUG): bitcoind started (run #1, node #0), waiting for RPC to come up

Alternative usage

Tweak functional test code to specify wait_for_debugger when (re)starting a node:

-        self.restart_node(0)
+        self.restart_node(0, wait_for_debugger=True)

While it can be messy to modify files while moving between commits, and cumbersome inside of loops, it might be more straightforward than -debug_runs <RUN_INDEX> in a given case.

Needed before merge

  • Testing on Mac to confirm -waitfordebugger on the C++ side behaves as expected.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 23, 2025

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/31723.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK l0rinc

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #33974 (cmake: Check dependencies after build option interaction by hebasto)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@fanquake
Copy link
Member

Makes bitcoind spin during startup, waiting for a debugger to be attached.

Can you explain more? If you want to run under a debugger, then why not just start bitcoind under a debugger; why do we need to add this option?

@hodlinator
Copy link
Contributor Author

Can you explain more? If you want to run under a debugger, then why not just start bitcoind under a debugger; why do we need to add this option?

Clarified PR description text and added example usage. But I'm curious to hear other ways of doing the same thing if there are any.

@maflcko
Copy link
Member

maflcko commented Jan 23, 2025

I think if you want to debug the start-up sequence specifically, it may be easier to do it outside the tests? Then anything else, if needed, can happen without patching Bitcoin Core source code itself.

@hodlinator
Copy link
Contributor Author

I think if you want to debug the start-up sequence specifically, it may be easier to do it outside the tests? Then anything else, if needed, can happen without patching Bitcoin Core source code itself.

This is not about debugging the start-up sequence specifically, but rather debugging from an early stage. Maybe one has set 5 breakpoints, some of which are hit during bitcoind init, and some which are hit by re-running a Python test which calls a set of RPCs.

@fanquake
Copy link
Member

Ideally we wouldn't be adding debug code, to production binaries, to facilitate using an IDE.

Can you configure your IDE to do something like the equivalent of using gdb/lldb, and process attach --name bitcoind --waitfor? So it just grabs bitcoind once it spins up?

@hodlinator
Copy link
Contributor Author

hodlinator commented Jan 23, 2025

Ideally we wouldn't be adding debug code, to production binaries, to facilitate using an IDE.

Agree it should not be in production binaries, should be #ifdefed away. Added as another reason for draft status.

Can you configure your IDE to do something like the equivalent of using gdb/lldb, and process attach --name bitcoind --waitfor? So it just grabs bitcoind once it spins up?

I've never seen something like that in an IDE/editor, but I'll have a look, might exist a debugger console which allows that kind of thing.

This PR does however allow for attaching to a specific node instance of bitcoind if a Python tests runs several nodes at once. (I'm considering changing the index to refer to how many bitcoind-launches one has done, some tests just relaunch node 0 a bunch of times, and one might be interested in debugging it on the 3rd execution Edit: this is now implemented).

@yuvicc
Copy link
Contributor

yuvicc commented Jan 27, 2025

Agree with @maflcko @fanquake, not sure if we want to add that to the production code.

@hodlinator hodlinator force-pushed the 2024/06/wait_for_debugger branch from 7d181da to 7dd8609 Compare January 27, 2025 19:11
@hodlinator
Copy link
Contributor Author

hodlinator commented Jan 27, 2025

Added tentative #ifdef solution in latest push. CMake part could probably be improved since now I think the #define leaks into the CMake project's public interface, which should not be necessary.

@hodlinator
Copy link
Contributor Author

  • Added CMake option WAIT_FOR_DEBUGGER.
  • Changed the Python logic from debugging specific nodes, to debugging specific node runs/executions, which is more specific.
  • Use prctl() on Linux to allow any process to attach to bitcoind. This removes the necessity to disable kernel.yama.ptrace_scope on some systems, which may be seen as decreasing security, but is compiled out unless WAIT_FOR_DEBUGGER=ON.
  • (Confirmed PR works on Windows).
  • (Rebased).

@hodlinator hodlinator changed the title qa debug: Add --debugnode/-waitfordebugger [DRAFT] qa debug: Add --debug_runs/-waitfordebugger [DRAFT] Feb 13, 2025
@maflcko
Copy link
Member

maflcko commented Feb 13, 2025

I think if you want to debug the start-up sequence specifically, it may be easier to do it outside the tests? Then anything else, if needed, can happen without patching Bitcoin Core source code itself.

This is not about debugging the start-up sequence specifically, but rather debugging from an early stage. Maybe one has set 5 breakpoints, some of which are hit during bitcoind init, and some which are hit by re-running a Python test which calls a set of RPCs.

I'd still say it is easier to debug the init sequence separately than to start a new build with a new build flag for the edge case where someone wants to debug both in an IDE and also the init sequence , but no strong opinion.

@hodlinator
Copy link
Contributor Author

fanquake:

Can you configure your IDE to do something like the equivalent of using gdb/lldb, and process attach --name bitcoind --waitfor? So it just grabs bitcoind once it spins up?

I've never seen something like that in an IDE/editor, but I'll have a look, might exist a debugger console which allows that kind of thing.

The only debugging console I get with the IDE states "No Active Debug Session" when I try to input commands. :(

maflcko:

This is not about debugging the start-up sequence specifically, but rather debugging from an early stage. Maybe one has set 5 breakpoints, some of which are hit during bitcoind init, and some which are hit by re-running a Python test which calls a set of RPCs.

I'd still say it is easier to debug the init sequence separately than to start a new build with a new build flag for the edge case where someone wants to debug both in an IDE and also the init sequence , but no strong opinion.

Maybe part of this is down to me not getting used to using a Python debugger, breaking in Python at appropriate times and then attaching a C++ debugger to the new process. Being able to specify which bitcoind-executions one wants to debug still seems like a smoother workflow when testing the same scenario repeatedly.

I recently used a version of this PR when digging into #31734. Being able to step through the code (with a panel for inspecting local variables and watches), as the Python test framework is issuing RPCs, in the safety of one's IDE, is nice. Knowing that bitcoind has not passed through one's breakpoints (unless there's some rare static initialization going on) helps build certainty about what unfamiliar code is involved in.

Granted, using --waitfor straight in gdb/lldb would give definite certainty what code runs. But the IDE GUI would not be there. I know gdb has some kind of TUI, and there are probably commands for saving and restoring state. It just feels like operating the computer with my feet. Happy to be proven wrong though. Please shill your setups. :)

@hodlinator hodlinator force-pushed the 2024/06/wait_for_debugger branch from 8c99eab to 56439ef Compare October 1, 2025 07:52
Copy link
Contributor Author

@hodlinator hodlinator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest push adds a functional test and breaks apart commits somewhat.

CMakeLists.txt Outdated
@@ -120,6 +120,7 @@ cmake_dependent_option(BUILD_WALLET_TOOL "Build bitcoin-wallet tool." ${BUILD_TE
option(REDUCE_EXPORTS "Attempt to reduce exported symbols in the resulting executables." OFF)
option(WERROR "Treat compiler warnings as errors." OFF)
option(WITH_CCACHE "Attempt to use ccache for compiling." ON)
option(WAIT_FOR_DEBUGGER "Support for waiting during startup for debugger to be attached." OFF)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me there's a silent, implied, "Enables ..." or "Provides ..." at the beginning of the description for these options that would get repetitive if spelled out, which may make "support" more natural.

Note that simply enabling the option does not pause startup, one also has to start with -waitfordebugger.

Curious to hear what other reviewers think.

@hodlinator hodlinator force-pushed the 2024/06/wait_for_debugger branch 3 times, most recently from e785b47 to d1a50b1 Compare October 1, 2025 12:28
@hodlinator hodlinator force-pushed the 2024/06/wait_for_debugger branch from d1a50b1 to 57df47a Compare October 22, 2025 20:42
@hodlinator hodlinator force-pushed the 2024/06/wait_for_debugger branch from 57df47a to 9d1de2e Compare October 22, 2025 21:54
@hodlinator
Copy link
Contributor Author

Latest push adds 9d1de2e8b3c73edc44bb3f7578ec96c2b91c2dd8 to support modifying a functional test to make a specific node (re)start trigger attaching of a debugger. This may be a more natural alternative when humans read through functional test code and want to select a specific node instance to debug.

(It also removes a header which caused a build failure on Alpine/musl, and adds helpful details in 2 commit messages).

There has been some feedback to see if we could avoid changing the functional test framework and move most of this feature to /contrib/, so I might try to figure out alternative approaches.

@DrahtBot
Copy link
Contributor

🚧 At least one of the CI tasks failed.
Task lint: https://github.com/bitcoin/bitcoin/actions/runs/20229750611/job/58069642887
LLM reason (✨ experimental): CI failure due to Python lint errors reported by ruff (unused import and an f-string issue) in the lint step.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@hodlinator
Copy link
Contributor Author

Changes compared to f352bd9d246b20579b9b4ccec366bb694df5e051:

  • feature_debugging.py - Added stderr output upon failure when we expect to be able to attach. Switched to subprocess.run(..., text=True) in spirit of fab085c.
  • Renamed CMake option WAIT_FOR_DEBUGGER -> ENABLE_WAIT_FOR_DEBUGGER to align convention with other options (also renamed constexpr bool in bitcoind.cpp from WAIT_FOR_DEBUGGER_ENABLE -> WAIT_FOR_DEBUGGER_SUPPORT to disambiguate).
  • Added config.ini support for ENABLE_WAIT_FOR_DEBUGGER and implemented is_wait_for_debugger_compiled() instead of custom parsing of CMakeCache.txt in feature_debugging.py.
  • Added comment in test_node.py explaining that functional test will continue executing even though a debugger has not been attached yet.
  • Switched from mentioning Gnome Console in commit message and developer-notes.md to using Tmux as an example.
  • Switched example test from feature_abortnode.py to feature_loadblock.py since the former causes a node to crash.
  • Made --debug_cmd without --debug_runs not enable it for all runs, but instead require modifying test code to specify wait_for_debugger=True or use --debug_runs.

Verifies that debugging bitcoind is not allowed by default on Linux, at least in sane environments.
Makes bitcoind spin during startup, waiting for a debugger to be attached. Once a debugger is detected, execution will continue.
Useful for debugging one or several bitcoind nodes running in the context of a functional test.

You can run a test with -ldebug once first to get output with run#s to see which you are interested in.
Enables automated attaching of debuggers.

The special identifier "$PID$" is substituted by the actual bitcoind process id number.

Attaching with LLDB in Tmux pane:
$ ./build/test/functional/feature_loadblock.py --debug_runs 2 --debug_cmd "tmux split-window -h lldb -p \$PID\$"

Attaching with Sublime Text Debugger:
$ ./build/test/functional/feature_loadblock.py --debug_runs 2 --debug_cmd "subl --command \"debugger {\\\"action\\\": \\\"start\\\", \\\"configuration\\\": {\\\"name\\\": \\\"foo\\\", \\\"type\\\": \\\"lldb\\\", \\\"request\\\": \\\"attach\\\", \\\"program\\\": \\\"dummyprogram\\\", \\\"pid\\\": \\\"\$PID\$\\\"}}\""
Enables tweaking functional tests to start debugging a specific node run/execution.
@hodlinator hodlinator changed the title node: Add --debug_runs/-waitfordebugger + --debug_cmd qa: Facilitate debugging bitcoind inside functional tests Dec 18, 2025
@DrahtBot DrahtBot added the Tests label Dec 18, 2025
@hodlinator hodlinator force-pushed the 2024/06/wait_for_debugger branch from f021644 to 42db717 Compare December 18, 2025 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants