From 73c183d1c0b072ea300afcd9dde9a2d46d9c3135 Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Sun, 22 Mar 2026 21:53:47 -0700 Subject: [PATCH] Add CI unit tests, network health RPC, and fix checkpoint tests - Add unit test build+run steps to both Qt and headless Linux CI jobs - Enhance getnetworkinfo RPC with networkhealth object (peer mix, bootstrap mode, sync status) - Rewrite Checkpoints_tests to validate actual chain checkpoints (0, 9000, 9001, 2186940) Co-Authored-By: Claude Opus 4.6 --- .claude/settings.json | 3 +- .claude/settings.local.json | 94 ++++++++++++++++++++++++++++++++- .github/workflows/build-all.yml | 12 +++++ src/rpcnet.cpp | 46 +++++++++++++++- src/test/Checkpoints_tests.cpp | 41 +++++++------- 5 files changed, 173 insertions(+), 23 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 632b24c..502ecbc 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(C:/msys64/msys2_shell.cmd -mingw64 -defterm -no-start -here -c \"ls /mingw64/lib/libboost_system* 2>/dev/null\")" + "Bash(C:/msys64/msys2_shell.cmd -mingw64 -defterm -no-start -here -c \"ls /mingw64/lib/libboost_system* 2>/dev/null\")", + "Bash(git tag:*)" ], "additionalDirectories": [ "C:\\msys64\\mingw64\\bin" diff --git a/.claude/settings.local.json b/.claude/settings.local.json index efd3860..eccd2a4 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -73,7 +73,99 @@ "Bash(PATH=\"/c/msys64/mingw64/bin:$PATH\" /e/repos/triangles/scan_chain_tip.exe:*)", "Bash(PATH=\"/c/msys64/mingw64/bin:$PATH\" /c/msys64/mingw64/bin/qmake.exe:*)", "Bash(PATH=\"/c/msys64/mingw64/bin:$PATH\" qmake-qt5:*)", - "Bash(PATH=\"/c/msys64/mingw64/bin:$PATH\" mingw32-make:*)" + "Bash(PATH=\"/c/msys64/mingw64/bin:$PATH\" mingw32-make:*)", + "Bash(export PATH=\"/c/msys64/mingw64/bin:$PATH\")", + "Bash(\"C:/msys64/mingw64/bin/qmake-qt5.exe\" triangles-qt.pro -o Makefile)", + "Bash(gh release create:*)", + "Bash(gh repo view:*)", + "Bash(gh repo create:*)", + "Bash(git commit:*)", + "Bash(git branch:*)", + "Bash(git push:*)", + "Bash(gh repo fork:*)", + "Bash(gh api:*)", + "Bash(gh auth:*)", + "Bash(1 <<'EOF'\n{\"visibility\":\"public\"}\nEOF)", + "Bash(findstr:*)", + "Bash(gh workflow run:*)", + "Bash(gh run watch:*)", + "Bash(gh run view:*)", + "Bash(gh release view:*)", + "Bash(gh run download:*)", + "Bash(gh release upload:*)", + "Bash(gh release delete-asset:*)", + "Bash(C:/msys64/mingw64/bin/mingw32-make.exe:*)", + "Bash(C:/msys64/usr/bin/env.exe MSYSTEM=MINGW64 PATH=\"/mingw64/bin:/usr/bin:/bin\" /usr/bin/bash -lc \"cd /e/repos/triangles/src && mingw32-make -f makefile.mingw -j8 all 2>&1 | tail -60\")", + "Bash(C:/msys64/usr/bin/env.exe MSYSTEM=MINGW64 /usr/bin/bash -lc \"ls /mingw64/lib/libboost_system*\")", + "Bash(C:/msys64/usr/bin/env.exe MSYSTEM=MINGW64 PATH=\"/mingw64/bin:/usr/bin:/bin\" /usr/bin/bash -lc \"cd /e/repos/triangles/src && mingw32-make -f makefile.mingw trianglesd.exe 2>&1 | tail -20\")", + "Bash(C:/msys64/usr/bin/env.exe MSYSTEM=MINGW64 PATH=\"/mingw64/bin:/usr/bin:/bin\" /usr/bin/bash -lc \"cd /e/repos/triangles/src && mingw32-make -f makefile.mingw trianglesd.exe 2>&1 | tail -10\")", + "Bash(gh run list:*)", + "Bash(git rm:*)", + "Bash(wc:*)", + "Bash(C:/msys64/usr/bin/bash.exe -lc \"cd /e/repos/triangles/src && make -f makefile.mingw obj/rest.o 2>&1 | head -80\")", + "Bash(C:/msys64/usr/bin/bash.exe -lc \"cd /e/repos/triangles/src && make -f makefile.mingw obj/trianglesrpc.o 2>&1 | tail -20\")", + "Bash(C:/msys64/usr/bin/bash.exe -lc \"cd /e/repos/triangles/src && make -f makefile.mingw 2>&1 | tail -30\")", + "Bash(node --version:*)", + "Bash(npm --version:*)", + "Bash(npm install:*)", + "Bash(npx svelte-kit sync:*)", + "Bash(npx vite build)", + "Bash(nslookup:*)", + "Bash(tailscale ping:*)", + "Bash(del /f \"%APPDATA%\\\\triangles\\\\peers.dat\")", + "Bash(C:msys64usrbinbash.exe -l -c \"cd ''e:/repos/triangles'' && qmake triangles-qt.pro ''USE_QRCODE=1'' ''USE_UPNP=-'' 2>&1 | tail -20\")", + "Bash(C:msys64msys2_shell.cmd -mingw64 -defterm -no-start -c \"cd /e/repos/triangles && qmake triangles-qt.pro ''USE_QRCODE=1'' ''USE_UPNP=-'' 2>&1 | tail -20\")", + "Bash(\"C:\\\\msys64\\\\mingw64\\\\bin\\\\bash.exe\" -c \"export PATH=/mingw64/bin:/usr/bin:$PATH && cd /e/repos/triangles && qmake triangles-qt.pro ''USE_QRCODE=1'' ''USE_UPNP=-'' 2>&1\")", + "Bash(C:/msys64/mingw64/bin/qmake.exe:*)", + "Bash(/c/msys64/mingw64/bin/qmake-qt5.exe:*)", + "Bash(/c/msys64/usr/bin/env.exe MSYSTEM=MINGW64 /c/msys64/usr/bin/bash.exe -l -c \"cd /e/repos/triangles && qmake triangles-qt.pro ''USE_QRCODE=1'' ''USE_UPNP=-'' 2>&1 | tail -5\")", + "Bash(export PATH=\"/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH\")", + "Bash(/c/msys64/usr/bin/env.exe MSYSTEM=MINGW64 /c/msys64/usr/bin/bash.exe:*)", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"cd /e/repos/triangles && make release 2>&1 | grep -E ''error|Error|undefined|cannot find'' | head -20\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"pacman -Qs qrencode\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"pacman -S --noconfirm mingw-w64-x86_64-qrencode\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"cd /e/repos/triangles && qmake-qt5 -o Makefile triangles-qt.pro USE_QRCODE=0 USE_UPNP=- 2>&1\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"ls /mingw64/lib/libqrencode*\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"pacman -S --noconfirm mingw-w64-x86_64-cmake\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"cd /tmp && pacman -Sp mingw-w64-x86_64-qrencode 2>/dev/null\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"cd /e/repos/triangles && mkdir -p dist && cp release/triangles-qt.exe dist/ && cd dist && strip triangles-qt.exe && ls -lh triangles-qt.exe\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"cd /e/repos/triangles/dist && ldd triangles-qt.exe | grep mingw64 | awk ''{print $3}''\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c 'cd /e/repos/triangles/dist && ldd triangles-qt.exe | grep mingw64 | awk \"\"{print \\\\$3}\"\"')", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"cd /e/repos/triangles/dist && ldd triangles-qt.exe | grep mingw64\")", + "Bash(C:/msys64/usr/bin/bash.exe -l -c 'cd /e/repos/triangles/dist && ldd triangles-qt.exe | grep mingw64 | sed \"\"s/.*=> //\"\" | sed \"\"s/ \\(.*//\"\"> dlls.txt && while read dll; do cp \"\"$dll\"\" .; done < dlls.txt && ls *.dll | wc -l && echo \"\"DLLs copied\"\"')", + "Bash(C:/msys64/usr/bin/bash.exe -l -c 'cd /e/repos/triangles/dist && mkdir -p platforms && cp /mingw64/share/qt5/plugins/platforms/qwindows.dll platforms/ && echo \"\"Qt platform plugin copied\"\"')", + "Bash(C:/msys64/usr/bin/bash.exe -l -c 'cd /e/repos/triangles && rm -f Triangles-v5.1.8-win-x64.zip && cd dist && 7z a ../Triangles-v5.1.8-win-x64.zip triangles-qt.exe *.dll platforms/ && echo \"\"ZIP created\"\"')", + "Bash(C:/msys64/usr/bin/bash.exe -l -c \"cd /e/repos/triangles && make -j4 2>&1 | tail -15\")", + "Bash(pacman:*)", + "Bash(tar:*)", + "WebFetch(domain:src-ref.docs.torproject.org)", + "WebFetch(domain:gitlab.torproject.org)", + "Bash(git status:*)", + "Bash(git stash:*)", + "Bash(git pull:*)", + "Bash(git stash pop:*)", + "Bash(find:*)", + "Read(//e/repos/triangles/**)", + "Bash(curl:*)", + "Bash(qmake:*)", + "Bash(/c/msys64/mingw64/bin/mingw32-make.exe:*)", + "Bash(C:/msys64/msys2_shell.cmd -mingw64 -defterm -no-start -here -c \"cd /e/repos/triangles && make -j1 2>&1 | tail -30\")", + "Bash(C:/msys64/msys2_shell.cmd -defterm -no-start -mingw64 -c \"cd /e/repos/triangles && rm -f build/main.o && mingw32-make -f Makefile.Release build/main.o 2>&1 | grep -E ''^\\(src/|.*error\\)'' | head -10\")", + "Bash(C:/msys64/msys2_shell.cmd -defterm -no-start -mingw64 -c \"cd /e/repos/triangles && ls -la build/main.o 2>&1\")", + "Bash(git -C \"e:\\\\repos\\\\triangles\" log --oneline -20)", + "Bash(git -C \"e:\\\\repos\\\\triangles\" describe --tags --abbrev=0)", + "Bash(git -C \"e:\\\\repos\\\\triangles\" rev-parse --short HEAD)", + "Bash(PATH=\"/c/msys64/mingw64/bin:$PATH\" which make:*)", + "Bash(export PATH=\"/mingw64/bin:$PATH\")", + "Bash(make:*)", + "Bash(C:/msys64/usr/bin/env.exe PATH=\"C:/msys64/mingw64/bin:C:/msys64/usr/bin\" C:/msys64/usr/bin/make.exe:*)", + "Bash(C:/msys64/msys2_shell.cmd -mingw64 -defterm -no-start -c \"cd /e/repos/triangles && make -f Makefile.Release -j8 2>&1 | tail -40\")", + "Bash(C:/msys64/msys2_shell.cmd -mingw64 -defterm -no-start -here -c \"make 2>&1 | tail -10\")", + "Bash(gh pr list:*)", + "Bash(gh pr view:*)", + "Bash(cmd //C \"powershell -NoProfile -Command \"\"Get-Process | Where-Object { $_.Path -like ''*triangles*'' } | Format-Table Id, ProcessName, Path\"\"\")", + "Bash(cmd //C \"tasklist /FI \"\"IMAGENAME eq triangles-qt.exe\"\"\")", + "Bash(MSYS_NO_PATHCONV=1 tasklist:*)" ] } } diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index a80f70e..1fa1e78 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -147,6 +147,12 @@ jobs: - name: Build run: make -j$(nproc) + - name: Run unit tests + run: | + cd src + make -f makefile.unix test_triangles -j$(nproc) + ./test_triangles --log_level=test_suite + - name: Strip binary run: strip --strip-all triangles-qt @@ -183,6 +189,12 @@ jobs: mkdir -p obj make -f makefile.unix -j$(nproc) + - name: Run unit tests + run: | + cd src + make -f makefile.unix test_triangles -j$(nproc) + ./test_triangles --log_level=test_suite + - name: Strip binary run: strip --strip-all src/trianglesd diff --git a/src/rpcnet.cpp b/src/rpcnet.cpp index 4f1cfa3..241fad0 100644 --- a/src/rpcnet.cpp +++ b/src/rpcnet.cpp @@ -8,19 +8,62 @@ #include "wallet.h" #include "db.h" #include "walletdb.h" +#include "net_bootstrap.h" using namespace json_spirit; using namespace std; +namespace { + +const char* BootstrapModeToString(NetBootstrap::BootstrapMode mode) +{ + switch (mode) + { + case NetBootstrap::BOOTSTRAP_LEGACY: + return "legacy"; + case NetBootstrap::BOOTSTRAP_TOR_MIXED: + return "tor_mixed"; + case NetBootstrap::BOOTSTRAP_TOR_ONLY: + return "tor_only"; + } + + return "unknown"; +} + +NetBootstrap::BootstrapMode GetBootstrapModeForRPC() +{ + const bool torEnabled = GetBoolArg("-tor", false) || GetBoolArg("-proxy", false); + const bool onlyTor = GetBoolArg("-onlynet", false) && GetArg("-onlynet", "") == "tor"; + + if (onlyTor) + return NetBootstrap::BOOTSTRAP_TOR_ONLY; + if (torEnabled) + return NetBootstrap::BOOTSTRAP_TOR_MIXED; + return NetBootstrap::BOOTSTRAP_LEGACY; +} + +} // namespace + Value getnetworkinfo(const Array& params, bool fHelp) { if (fHelp || params.size() != 0) throw runtime_error( "getnetworkinfo\n" - "Returns an object containing various state info regarding P2P networking."); + "Returns an object containing various state info regarding P2P networking,\n" + "including peer mix, bootstrap mode, and basic sync health."); proxyType proxy; GetProxy(NET_IPV4, proxy); + const NetBootstrap::NetworkHealth health = NetBootstrap::GetNetworkHealth(); + + Object healthObj; + healthObj.push_back(Pair("connectedpeers", health.connectedPeers)); + healthObj.push_back(Pair("torpeers", health.torPeers)); + healthObj.push_back(Pair("clearnetpeers", health.clearnetPeers)); + healthObj.push_back(Pair("bootstrapped", health.isBootstrapped)); + healthObj.push_back(Pair("syncing", health.isSyncing)); + healthObj.push_back(Pair("lastblocktime", static_cast(health.lastBlockTime))); + healthObj.push_back(Pair("bootstrapmode", BootstrapModeToString(GetBootstrapModeForRPC()))); Object obj; obj.push_back(Pair("version", FormatFullVersion())); @@ -30,6 +73,7 @@ Value getnetworkinfo(const Array& params, bool fHelp) obj.push_back(Pair("ip", addrSeenByPeer.ToStringIP())); obj.push_back(Pair("localservices", strprintf("%016"PRIx64, nLocalServices))); obj.push_back(Pair("testnet", fTestNet)); + obj.push_back(Pair("networkhealth", healthObj)); obj.push_back(Pair("errors", GetWarnings("statusbar"))); return obj; } diff --git a/src/test/Checkpoints_tests.cpp b/src/test/Checkpoints_tests.cpp index fdd53eb..d0911fb 100644 --- a/src/test/Checkpoints_tests.cpp +++ b/src/test/Checkpoints_tests.cpp @@ -1,32 +1,33 @@ -// -// Unit tests for block-chain checkpoints -// #include #include "../checkpoints.h" -#include "../util.h" - -using namespace std; +#include "../uint256.h" BOOST_AUTO_TEST_SUITE(Checkpoints_tests) -BOOST_AUTO_TEST_CASE(sanity) +BOOST_AUTO_TEST_CASE(hardened_checkpoints_match_current_chain) { - uint256 p11111 = uint256("0x05cbc42d0421207a39427f1b44610a30380e0e3dd593c154276d7c8d4ae8c9b2"); - uint256 p16666 = uint256("0x682d99d096441e046dae8dd86d5bdfaa32d4e29dea6cf38273bb2b261905f1e2"); - BOOST_CHECK(Checkpoints::CheckBlock(11111, p11111)); - BOOST_CHECK(Checkpoints::CheckBlock(16666, p16666)); + BOOST_CHECK(Checkpoints::CheckHardened(0, uint256("0x7e7a6e4dd5fe895106fca912dfbacaeaf2a89e76c6a588df8ff96e0e18b96021"))); + BOOST_CHECK(Checkpoints::CheckHardened(9000, uint256("0x00000000019ef6b2f5e7c324c7d083ee94502305aabc7e9cd73a7fb2a57bb8db"))); + BOOST_CHECK(Checkpoints::CheckHardened(9001, uint256("0x6d5c6c5f201cc9e59659ee0da30d1430dc6bf3b12a8ff4c3864ab8d6286b0007"))); + BOOST_CHECK(Checkpoints::CheckHardened(2186940, uint256("0xbd952e8d4a612e336d840ad924a7e09395e36bcd9d929b302e47e60b5c3098c0"))); +} - - // Wrong hashes at checkpoints should fail: - BOOST_CHECK(!Checkpoints::CheckBlock(11111, p16666)); - BOOST_CHECK(!Checkpoints::CheckBlock(16666, p11111)); +BOOST_AUTO_TEST_CASE(hardened_checkpoints_reject_wrong_hashes_and_allow_unknown_heights) +{ + const uint256 wrongHash("0x0000000000000000000000000000000000000000000000000000000000000001"); - // ... but any hash not at a checkpoint should succeed: - BOOST_CHECK(Checkpoints::CheckBlock(11111+1, p16666)); - BOOST_CHECK(Checkpoints::CheckBlock(16666+1, p11111)); + BOOST_CHECK(!Checkpoints::CheckHardened(9000, wrongHash)); + BOOST_CHECK(!Checkpoints::CheckHardened(9001, wrongHash)); + BOOST_CHECK(!Checkpoints::CheckHardened(2186940, wrongHash)); - BOOST_CHECK(Checkpoints::GetTotalBlocksEstimate() >= 16666); -} + BOOST_CHECK(Checkpoints::CheckHardened(2186941, wrongHash)); + BOOST_CHECK(Checkpoints::CheckHardened(42, wrongHash)); +} + +BOOST_AUTO_TEST_CASE(total_blocks_estimate_tracks_latest_hardened_checkpoint) +{ + BOOST_CHECK(Checkpoints::GetTotalBlocksEstimate() >= 2186940); +} BOOST_AUTO_TEST_SUITE_END()