From cdb532d2603a2db53daf8e64666b7a2deba6cdda Mon Sep 17 00:00:00 2001 From: Gene Hoffman Date: Tue, 17 Feb 2026 19:57:06 -0800 Subject: [PATCH 1/2] Add GoogleTest regressions for vdf_client session parsing. Extract session read/validation logic for reuse, add targeted regression coverage for discriminant/form size edge cases, and wire regression execution into the unified CI workflow. Co-authored-by: Cursor --- .github/workflows/vdf-client-hw.yml | 16 +++++- .gitignore | 1 + README.md | 28 +++++++++- README_ASIC.md | 4 ++ src/CMakeLists.txt | 21 +++++++ src/vdf_client.cpp | 37 ++----------- src/vdf_client_session.h | 47 ++++++++++++++++ src/vdf_client_session_test.cpp | 85 +++++++++++++++++++++++++++++ 8 files changed, 204 insertions(+), 35 deletions(-) create mode 100644 src/vdf_client_session.h create mode 100644 src/vdf_client_session_test.cpp diff --git a/.github/workflows/vdf-client-hw.yml b/.github/workflows/vdf-client-hw.yml index d3ad6418..c5d8f433 100644 --- a/.github/workflows/vdf-client-hw.yml +++ b/.github/workflows/vdf-client-hw.yml @@ -219,7 +219,7 @@ jobs: if (-not $gnuAsmEnabled) { throw "ENABLE_GNU_ASM is not ON on Windows CI build" } - cmake --build build --target vdf_client vdf_bench 1weso_test 2weso_test prover_test emu_hw_test hw_test emu_hw_vdf_client hw_vdf_client + cmake --build build --target vdf_client vdf_bench 1weso_test 2weso_test prover_test vdf_client_session_test emu_hw_test hw_test emu_hw_vdf_client hw_vdf_client $asmFiles = @( "build/asm_compiled.s", "build/avx2_asm_compiled.s", @@ -317,6 +317,20 @@ jobs: .\prover_test.exe if ($LASTEXITCODE -ne 0) { throw "prover_test failed with exit code $LASTEXITCODE" } + - name: Run vdf_client_session regression (GoogleTest, Unix) + if: matrix.config == 'optimized=1' && matrix.os != 'windows-latest' + run: | + cmake -S src -B build-regression -DBUILD_PYTHON=OFF -DBUILD_CHIAVDFC=OFF -DBUILD_VDF_CLIENT=OFF -DBUILD_VDF_BENCH=OFF -DBUILD_VDF_TESTS=ON -DBUILD_HW_TOOLS=OFF -DENABLE_GNU_ASM=ON + cmake --build build-regression --target vdf_client_session_test + ctest --test-dir build-regression --output-on-failure -R '^regression\.' + + - name: Run vdf_client_session regression (GoogleTest, Windows) + if: matrix.os == 'windows-latest' && matrix.config == 'optimized=1' + shell: pwsh + run: | + cd build + ctest -C Release --output-on-failure -R '^regression\.' + - name: Benchmark vdf_bench square (Ubuntu/Mac) if: matrix.config == 'optimized=1' && matrix.os != 'windows-latest' run: | diff --git a/.gitignore b/.gitignore index a454ba35..15b54e97 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ src/hw_vdf_client /verifier /verifier_test /build/* +/build-*/ *.whl *.egg-info *.o diff --git a/README.md b/README.md index 35a9bec5..a7390896 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ For direct CMake builds, the following options are available: - `BUILD_VDF_CLIENT` - build `vdf_client` - `BUILD_VDF_BENCH` - build `vdf_bench` -- `BUILD_VDF_TESTS` - build `1weso_test`, `2weso_test`, and `prover_test` +- `BUILD_VDF_TESTS` - build test binaries (`1weso_test`, `2weso_test`, `prover_test`) and CTest/GoogleTest targets (for example `vdf_client_session_test`) - `BUILD_HW_TOOLS` - build hardware timelord tools - `ENABLE_GNU_ASM` - enable GNU-style asm pipeline on x86/x64 (enabled by default) - `GENERATE_ASM_TRACKING_DATA` - enable `track_asm()` instrumentation in generated asm (off by default to avoid hot-loop overhead) @@ -58,7 +58,7 @@ cmake -S src -B build \ -DBUILD_VDF_CLIENT=ON \ -DBUILD_VDF_BENCH=ON \ -DBUILD_VDF_TESTS=ON -cmake --build build --target vdf_client vdf_bench 1weso_test 2weso_test prover_test +cmake --build build --target vdf_client vdf_bench 1weso_test 2weso_test prover_test vdf_client_session_test ``` For the legacy `setup.py` + `Makefile.vdf-client` flow (used by wheel hooks), @@ -96,6 +96,30 @@ Those tests will simulate the vdf_client and verify for correctness the produced Note: `./prover_test` defaults to a long soak/stress run. Set `CHIAVDF_PROVER_TEST_FAST=1` to run a short, CI-friendly correctness check. +Regression tests for specific bugs are now added with GoogleTest and run via +CTest. Example: + +```bash +cmake -S src -B build \ + -DBUILD_PYTHON=OFF \ + -DBUILD_CHIAVDFC=OFF \ + -DBUILD_VDF_CLIENT=OFF \ + -DBUILD_VDF_BENCH=OFF \ + -DBUILD_VDF_TESTS=ON \ + -DBUILD_HW_TOOLS=OFF +cmake --build build --target vdf_client_session_test +ctest --test-dir build --output-on-failure -R '^regression\.' +``` + +### Testing matrix + +- Binary integration tests (existing): `1weso_test`, `2weso_test`, + `prover_test`; these simulate `vdf_client` and validate proof correctness. +- Regression tests (new): GoogleTest targets executed via CTest (for example + `vdf_client_session_test`, typically filtered with `ctest -R '^regression\.'`). +- Hardware tests: standalone binaries such as `hw_test` and `emu_hw_test` + described in `README_ASIC.md`. + ## Fuzzing Fuzz targets live under `rust_bindings/fuzz`. The `prove` target includes an diff --git a/README_ASIC.md b/README_ASIC.md index 7a925f06..f9c42f4a 100644 --- a/README_ASIC.md +++ b/README_ASIC.md @@ -17,6 +17,10 @@ cd src make -f Makefile.vdf-client emu_hw_test hw_test emu_hw_vdf_client hw_vdf_client ``` +Note: the software regression tests in this repository use CMake + CTest + +GoogleTest (for example `vdf_client_session_test`). The ASIC hardware checks in +this guide (`hw_test`, `emu_hw_test`) remain standalone binaries. + Connect the Chia VDF ASIC device and verify that it is detected: ```bash # in chiavdf/src/ directory diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8a522945..e3f82528 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -293,6 +293,19 @@ if(BUILD_VDF_BENCH) endif() if(BUILD_VDF_TESTS) + enable_testing() + include(GoogleTest) + if(WIN32) + # Match parent CRT choice on Windows to avoid link/runtime mismatches. + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + endif() + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 + ) + FetchContent_MakeAvailable(googletest) + add_executable(1weso_test ${CMAKE_CURRENT_SOURCE_DIR}/1weso_test.cpp ) @@ -320,6 +333,14 @@ if(BUILD_VDF_TESTS) target_link_libraries(prover_test PRIVATE ${GMP_LIBRARIES} ${GMPXX_LIBRARIES} Threads::Threads) vdf_add_windows_clang_opts(prover_test) + add_executable(vdf_client_session_test + ${CMAKE_CURRENT_SOURCE_DIR}/vdf_client_session_test.cpp + ) + vdf_add_boost_includes(vdf_client_session_test) + target_link_libraries(vdf_client_session_test PRIVATE GTest::gtest_main Threads::Threads) + vdf_add_windows_clang_opts(vdf_client_session_test) + gtest_discover_tests(vdf_client_session_test TEST_PREFIX "regression.") + endif() if(BUILD_HW_TOOLS) diff --git a/src/vdf_client.cpp b/src/vdf_client.cpp index 757788a4..05aa4f13 100644 --- a/src/vdf_client.cpp +++ b/src/vdf_client.cpp @@ -1,6 +1,7 @@ #include #include "vdf.h" #include "version.hpp" +#include "vdf_client_session.h" #include using boost::asio::ip::tcp; @@ -18,12 +19,6 @@ void PrintInfo(std::string input) { std::cout << std::flush; } -char disc[350]; -char disc_size[5]; -int disc_int_size; - -uint8_t initial_form_s[BQFC_FORM_SIZE]; - void WriteProof(uint64_t iteration, Proof& result, tcp::socket& sock) { // Writes the number of iterations uint8_t int_bytes[8]; @@ -80,32 +75,7 @@ void CreateAndWriteProofTwoWeso(integer& D, form f, uint64_t iters, TwoWesolowsk WriteProof(iters, result, sock); } -void InitSession(tcp::socket& sock) { - boost::system::error_code error; - - memset(disc, 0x00, sizeof(disc)); // For null termination - memset(disc_size, 0x00, sizeof(disc_size)); // For null termination - - boost::asio::read(sock, boost::asio::buffer(disc_size, 3), error); - disc_int_size = atoi(disc_size); - if (disc_int_size <= 0 || disc_int_size >= (int)sizeof(disc)) { - throw std::runtime_error("Invalid discriminant size"); - } - boost::asio::read(sock, boost::asio::buffer(disc, disc_int_size), error); - - // Signed char is intentional: values 128-255 wrap negative, caught by the <= 0 check below - char form_size; - boost::asio::read(sock, boost::asio::buffer(&form_size, 1), error); - if (form_size <= 0 || form_size > (int)sizeof(initial_form_s)) { - throw std::runtime_error("Invalid form size"); - } - boost::asio::read(sock, boost::asio::buffer(initial_form_s, form_size), error); - - if (error == boost::asio::error::eof) - return ; // Connection closed cleanly by peer. - else if (error) - throw boost::system::system_error(error); // Some other error. - +void ConfigureSessionRuntime() { if (getenv("warn_on_corruption_in_production") != nullptr) { warn_on_corruption_in_production = true; } @@ -154,6 +124,7 @@ uint64_t ReadIteration(tcp::socket& sock) { void SessionFastAlgorithm(tcp::socket& sock) { InitSession(sock); + ConfigureSessionRuntime(); try { integer D(disc); integer L = root(-D, 4); @@ -202,6 +173,7 @@ void SessionFastAlgorithm(tcp::socket& sock) { void SessionOneWeso(tcp::socket& sock) { InitSession(sock); + ConfigureSessionRuntime(); try { integer D(disc); integer L = root(-D, 4); @@ -238,6 +210,7 @@ void SessionOneWeso(tcp::socket& sock) { void SessionTwoWeso(tcp::socket& sock) { const int kMaxProcessesAllowed = 100; InitSession(sock); + ConfigureSessionRuntime(); try { integer D(disc); integer L = root(-D, 4); diff --git a/src/vdf_client_session.h b/src/vdf_client_session.h new file mode 100644 index 00000000..e11b1cd7 --- /dev/null +++ b/src/vdf_client_session.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include "bqfc.h" + +#include +#include +#include +#include + +inline char disc[350]; +inline char disc_size[5]; +inline int disc_int_size; +inline uint8_t initial_form_s[BQFC_FORM_SIZE]; + +inline void InitSession(boost::asio::ip::tcp::socket& sock) { + boost::system::error_code error; + auto check_read_error = [&](const char* field_name) { + if (error == boost::asio::error::eof) { + throw std::runtime_error(std::string("Connection closed while reading ") + field_name); + } else if (error) { + throw boost::system::system_error(error); + } + }; + + memset(disc, 0x00, sizeof(disc)); // For null termination + memset(disc_size, 0x00, sizeof(disc_size)); // For null termination + + boost::asio::read(sock, boost::asio::buffer(disc_size, 3), error); + check_read_error("discriminant size"); + disc_int_size = atoi(disc_size); + if (disc_int_size <= 0 || disc_int_size >= (int)sizeof(disc)) { + throw std::runtime_error("Invalid discriminant size"); + } + boost::asio::read(sock, boost::asio::buffer(disc, disc_int_size), error); + check_read_error("discriminant"); + + // Signed char is intentional: values 128-255 wrap negative, caught by the <= 0 check below + char form_size; + boost::asio::read(sock, boost::asio::buffer(&form_size, 1), error); + check_read_error("form size"); + if (form_size <= 0 || form_size > (int)sizeof(initial_form_s)) { + throw std::runtime_error("Invalid form size"); + } + boost::asio::read(sock, boost::asio::buffer(initial_form_s, form_size), error); + check_read_error("initial form"); +} diff --git a/src/vdf_client_session_test.cpp b/src/vdf_client_session_test.cpp new file mode 100644 index 00000000..868f85e9 --- /dev/null +++ b/src/vdf_client_session_test.cpp @@ -0,0 +1,85 @@ +#include "vdf_client_session.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +std::string run_init_session_with_payload(const std::vector& payload) { + boost::asio::io_context io; + using boost::asio::ip::tcp; + + tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 0)); + const uint16_t port = acceptor.local_endpoint().port(); + + std::thread server([&]() { + boost::system::error_code server_error; + tcp::socket peer(io); + acceptor.accept(peer, server_error); + if (server_error) { + return; + } + if (!payload.empty()) { + boost::asio::write(peer, boost::asio::buffer(payload), server_error); + } + peer.shutdown(tcp::socket::shutdown_send, server_error); + peer.close(server_error); + }); + + std::string error_message; + try { + tcp::socket client(io); + client.connect(tcp::endpoint(boost::asio::ip::address_v4::loopback(), port)); + InitSession(client); + } catch (const std::exception& e) { + error_message = e.what(); + } + + server.join(); + return error_message; +} + +} // namespace + +TEST(VdfClientSessionRegressionTest, TruncatedDiscriminantSizeFailsBeforeParse) { + const std::vector payload = {'0', '3'}; + const std::string error = run_init_session_with_payload(payload); + EXPECT_NE(error.find("Connection closed while reading discriminant size"), std::string::npos); +} + +TEST(VdfClientSessionRegressionTest, ZeroDiscriminantSizeIsRejected) { + const std::vector payload = {'0', '0', '0'}; + const std::string error = run_init_session_with_payload(payload); + EXPECT_NE(error.find("Invalid discriminant size"), std::string::npos); +} + +TEST(VdfClientSessionRegressionTest, OversizedDiscriminantSizeIsRejected) { + const std::vector payload = {'3', '5', '0'}; + const std::string error = run_init_session_with_payload(payload); + EXPECT_NE(error.find("Invalid discriminant size"), std::string::npos); +} + +TEST(VdfClientSessionRegressionTest, TruncatedFormSizeFailsBeforeValidation) { + const std::vector payload = {'0', '0', '3', 'a', 'b', 'c'}; + const std::string error = run_init_session_with_payload(payload); + EXPECT_NE(error.find("Connection closed while reading form size"), std::string::npos); +} + +TEST(VdfClientSessionRegressionTest, ZeroFormSizeIsRejected) { + const std::vector payload = {'0', '0', '3', 'a', 'b', 'c', 0x00}; + const std::string error = run_init_session_with_payload(payload); + EXPECT_NE(error.find("Invalid form size"), std::string::npos); +} + +TEST(VdfClientSessionRegressionTest, WrappedNegativeFormSizeIsRejected) { + // 0xFF arrives as -1 in signed char and must be rejected by the <= 0 check. + const std::vector payload = {'0', '0', '3', 'a', 'b', 'c', 0xFF}; + const std::string error = run_init_session_with_payload(payload); + EXPECT_NE(error.find("Invalid form size"), std::string::npos); +} From 5cdcb12b2782e7f771cb008240735465071fb574 Mon Sep 17 00:00:00 2001 From: Gene Hoffman Date: Tue, 17 Feb 2026 20:39:49 -0800 Subject: [PATCH 2/2] Scope discriminant size temporaries to InitSession locals. Keep disc_size and disc_int_size local to reduce exported header globals and make InitSession-owned state explicit. Co-authored-by: Cursor --- src/vdf_client_session.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vdf_client_session.h b/src/vdf_client_session.h index e11b1cd7..99eeac16 100644 --- a/src/vdf_client_session.h +++ b/src/vdf_client_session.h @@ -9,12 +9,12 @@ #include inline char disc[350]; -inline char disc_size[5]; -inline int disc_int_size; inline uint8_t initial_form_s[BQFC_FORM_SIZE]; inline void InitSession(boost::asio::ip::tcp::socket& sock) { boost::system::error_code error; + char disc_size[5]; + int disc_int_size; auto check_read_error = [&](const char* field_name) { if (error == boost::asio::error::eof) { throw std::runtime_error(std::string("Connection closed while reading ") + field_name);