Skip to content

Commit b2dc9c2

Browse files
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 <cursoragent@cursor.com>
1 parent b04de93 commit b2dc9c2

8 files changed

Lines changed: 204 additions & 35 deletions

File tree

.github/workflows/vdf-client-hw.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ jobs:
206206
if (-not $gnuAsmEnabled) {
207207
throw "ENABLE_GNU_ASM is not ON on Windows CI build"
208208
}
209-
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
209+
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
210210
$asmFiles = @(
211211
"build/asm_compiled.s",
212212
"build/avx2_asm_compiled.s",
@@ -304,6 +304,20 @@ jobs:
304304
.\prover_test.exe
305305
if ($LASTEXITCODE -ne 0) { throw "prover_test failed with exit code $LASTEXITCODE" }
306306
307+
- name: Run vdf_client_session regression (GoogleTest, Unix)
308+
if: matrix.config == 'optimized=1' && matrix.os != 'windows-latest'
309+
run: |
310+
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
311+
cmake --build build-regression --target vdf_client_session_test
312+
ctest --test-dir build-regression --output-on-failure -R '^regression\.'
313+
314+
- name: Run vdf_client_session regression (GoogleTest, Windows)
315+
if: matrix.os == 'windows-latest' && matrix.config == 'optimized=1'
316+
shell: pwsh
317+
run: |
318+
cd build
319+
ctest -C Release --output-on-failure -R '^regression\.'
320+
307321
- name: Benchmark vdf_bench square (Ubuntu/Mac)
308322
if: matrix.config == 'optimized=1' && matrix.os != 'windows-latest'
309323
run: |

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ src/hw_vdf_client
3737
/verifier
3838
/verifier_test
3939
/build/*
40+
/build-*/
4041
*.whl
4142
*.egg-info
4243
*.o

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ For direct CMake builds, the following options are available:
4444

4545
- `BUILD_VDF_CLIENT` - build `vdf_client`
4646
- `BUILD_VDF_BENCH` - build `vdf_bench`
47-
- `BUILD_VDF_TESTS` - build `1weso_test`, `2weso_test`, and `prover_test`
47+
- `BUILD_VDF_TESTS` - build test binaries (`1weso_test`, `2weso_test`, `prover_test`) and CTest/GoogleTest targets (for example `vdf_client_session_test`)
4848
- `BUILD_HW_TOOLS` - build hardware timelord tools
4949
- `ENABLE_GNU_ASM` - enable GNU-style asm pipeline on x86/x64 (enabled by default)
5050
- `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 \
5858
-DBUILD_VDF_CLIENT=ON \
5959
-DBUILD_VDF_BENCH=ON \
6060
-DBUILD_VDF_TESTS=ON
61-
cmake --build build --target vdf_client vdf_bench 1weso_test 2weso_test prover_test
61+
cmake --build build --target vdf_client vdf_bench 1weso_test 2weso_test prover_test vdf_client_session_test
6262
```
6363

6464
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
9696
Note: `./prover_test` defaults to a long soak/stress run. Set
9797
`CHIAVDF_PROVER_TEST_FAST=1` to run a short, CI-friendly correctness check.
9898

99+
Regression tests for specific bugs are now added with GoogleTest and run via
100+
CTest. Example:
101+
102+
```bash
103+
cmake -S src -B build \
104+
-DBUILD_PYTHON=OFF \
105+
-DBUILD_CHIAVDFC=OFF \
106+
-DBUILD_VDF_CLIENT=OFF \
107+
-DBUILD_VDF_BENCH=OFF \
108+
-DBUILD_VDF_TESTS=ON \
109+
-DBUILD_HW_TOOLS=OFF
110+
cmake --build build --target vdf_client_session_test
111+
ctest --test-dir build --output-on-failure -R '^regression\.'
112+
```
113+
114+
### Testing matrix
115+
116+
- Binary integration tests (existing): `1weso_test`, `2weso_test`,
117+
`prover_test`; these simulate `vdf_client` and validate proof correctness.
118+
- Regression tests (new): GoogleTest targets executed via CTest (for example
119+
`vdf_client_session_test`, typically filtered with `ctest -R '^regression\.'`).
120+
- Hardware tests: standalone binaries such as `hw_test` and `emu_hw_test`
121+
described in `README_ASIC.md`.
122+
99123
## Fuzzing
100124

101125
Fuzz targets live under `rust_bindings/fuzz`. The `prove` target includes an

README_ASIC.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ cd src
1717
make -f Makefile.vdf-client emu_hw_test hw_test emu_hw_vdf_client hw_vdf_client
1818
```
1919

20+
Note: the software regression tests in this repository use CMake + CTest +
21+
GoogleTest (for example `vdf_client_session_test`). The ASIC hardware checks in
22+
this guide (`hw_test`, `emu_hw_test`) remain standalone binaries.
23+
2024
Connect the Chia VDF ASIC device and verify that it is detected:
2125
```bash
2226
# in chiavdf/src/ directory

src/CMakeLists.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,19 @@ if(BUILD_VDF_BENCH)
274274
endif()
275275

276276
if(BUILD_VDF_TESTS)
277+
enable_testing()
278+
include(GoogleTest)
279+
if(WIN32)
280+
# Match parent CRT choice on Windows to avoid link/runtime mismatches.
281+
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
282+
endif()
283+
FetchContent_Declare(
284+
googletest
285+
GIT_REPOSITORY https://github.com/google/googletest.git
286+
GIT_TAG v1.14.0
287+
)
288+
FetchContent_MakeAvailable(googletest)
289+
277290
add_executable(1weso_test
278291
${CMAKE_CURRENT_SOURCE_DIR}/1weso_test.cpp
279292
)
@@ -301,6 +314,14 @@ if(BUILD_VDF_TESTS)
301314
target_link_libraries(prover_test PRIVATE ${GMP_LIBRARIES} ${GMPXX_LIBRARIES} Threads::Threads)
302315
vdf_add_windows_clang_opts(prover_test)
303316

317+
add_executable(vdf_client_session_test
318+
${CMAKE_CURRENT_SOURCE_DIR}/vdf_client_session_test.cpp
319+
)
320+
vdf_add_boost_includes(vdf_client_session_test)
321+
target_link_libraries(vdf_client_session_test PRIVATE GTest::gtest_main Threads::Threads)
322+
vdf_add_windows_clang_opts(vdf_client_session_test)
323+
gtest_discover_tests(vdf_client_session_test TEST_PREFIX "regression.")
324+
304325
endif()
305326

306327
if(BUILD_HW_TOOLS)

src/vdf_client.cpp

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <boost/asio.hpp>
22
#include "vdf.h"
3+
#include "vdf_client_session.h"
34
#include <atomic>
45

56
using boost::asio::ip::tcp;
@@ -17,12 +18,6 @@ void PrintInfo(std::string input) {
1718
std::cout << std::flush;
1819
}
1920

20-
char disc[350];
21-
char disc_size[5];
22-
int disc_int_size;
23-
24-
uint8_t initial_form_s[BQFC_FORM_SIZE];
25-
2621
void WriteProof(uint64_t iteration, Proof& result, tcp::socket& sock) {
2722
// Writes the number of iterations
2823
uint8_t int_bytes[8];
@@ -79,32 +74,7 @@ void CreateAndWriteProofTwoWeso(integer& D, form f, uint64_t iters, TwoWesolowsk
7974
WriteProof(iters, result, sock);
8075
}
8176

82-
void InitSession(tcp::socket& sock) {
83-
boost::system::error_code error;
84-
85-
memset(disc, 0x00, sizeof(disc)); // For null termination
86-
memset(disc_size, 0x00, sizeof(disc_size)); // For null termination
87-
88-
boost::asio::read(sock, boost::asio::buffer(disc_size, 3), error);
89-
disc_int_size = atoi(disc_size);
90-
if (disc_int_size <= 0 || disc_int_size >= (int)sizeof(disc)) {
91-
throw std::runtime_error("Invalid discriminant size");
92-
}
93-
boost::asio::read(sock, boost::asio::buffer(disc, disc_int_size), error);
94-
95-
// Signed char is intentional: values 128-255 wrap negative, caught by the <= 0 check below
96-
char form_size;
97-
boost::asio::read(sock, boost::asio::buffer(&form_size, 1), error);
98-
if (form_size <= 0 || form_size > (int)sizeof(initial_form_s)) {
99-
throw std::runtime_error("Invalid form size");
100-
}
101-
boost::asio::read(sock, boost::asio::buffer(initial_form_s, form_size), error);
102-
103-
if (error == boost::asio::error::eof)
104-
return ; // Connection closed cleanly by peer.
105-
else if (error)
106-
throw boost::system::system_error(error); // Some other error.
107-
77+
void ConfigureSessionRuntime() {
10878
if (getenv("warn_on_corruption_in_production") != nullptr) {
10979
warn_on_corruption_in_production = true;
11080
}
@@ -153,6 +123,7 @@ uint64_t ReadIteration(tcp::socket& sock) {
153123

154124
void SessionFastAlgorithm(tcp::socket& sock) {
155125
InitSession(sock);
126+
ConfigureSessionRuntime();
156127
try {
157128
integer D(disc);
158129
integer L = root(-D, 4);
@@ -201,6 +172,7 @@ void SessionFastAlgorithm(tcp::socket& sock) {
201172

202173
void SessionOneWeso(tcp::socket& sock) {
203174
InitSession(sock);
175+
ConfigureSessionRuntime();
204176
try {
205177
integer D(disc);
206178
integer L = root(-D, 4);
@@ -237,6 +209,7 @@ void SessionOneWeso(tcp::socket& sock) {
237209
void SessionTwoWeso(tcp::socket& sock) {
238210
const int kMaxProcessesAllowed = 100;
239211
InitSession(sock);
212+
ConfigureSessionRuntime();
240213
try {
241214
integer D(disc);
242215
integer L = root(-D, 4);

src/vdf_client_session.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
3+
#include <boost/asio.hpp>
4+
#include "bqfc.h"
5+
6+
#include <cstdlib>
7+
#include <cstring>
8+
#include <stdexcept>
9+
#include <string>
10+
11+
inline char disc[350];
12+
inline char disc_size[5];
13+
inline int disc_int_size;
14+
inline uint8_t initial_form_s[BQFC_FORM_SIZE];
15+
16+
inline void InitSession(boost::asio::ip::tcp::socket& sock) {
17+
boost::system::error_code error;
18+
auto check_read_error = [&](const char* field_name) {
19+
if (error == boost::asio::error::eof) {
20+
throw std::runtime_error(std::string("Connection closed while reading ") + field_name);
21+
} else if (error) {
22+
throw boost::system::system_error(error);
23+
}
24+
};
25+
26+
memset(disc, 0x00, sizeof(disc)); // For null termination
27+
memset(disc_size, 0x00, sizeof(disc_size)); // For null termination
28+
29+
boost::asio::read(sock, boost::asio::buffer(disc_size, 3), error);
30+
check_read_error("discriminant size");
31+
disc_int_size = atoi(disc_size);
32+
if (disc_int_size <= 0 || disc_int_size >= (int)sizeof(disc)) {
33+
throw std::runtime_error("Invalid discriminant size");
34+
}
35+
boost::asio::read(sock, boost::asio::buffer(disc, disc_int_size), error);
36+
check_read_error("discriminant");
37+
38+
// Signed char is intentional: values 128-255 wrap negative, caught by the <= 0 check below
39+
char form_size;
40+
boost::asio::read(sock, boost::asio::buffer(&form_size, 1), error);
41+
check_read_error("form size");
42+
if (form_size <= 0 || form_size > (int)sizeof(initial_form_s)) {
43+
throw std::runtime_error("Invalid form size");
44+
}
45+
boost::asio::read(sock, boost::asio::buffer(initial_form_s, form_size), error);
46+
check_read_error("initial form");
47+
}

src/vdf_client_session_test.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include "vdf_client_session.h"
2+
3+
#include <boost/asio.hpp>
4+
#include <gtest/gtest.h>
5+
6+
#include <cstdint>
7+
#include <exception>
8+
#include <string>
9+
#include <thread>
10+
#include <vector>
11+
12+
namespace {
13+
14+
std::string run_init_session_with_payload(const std::vector<uint8_t>& payload) {
15+
boost::asio::io_context io;
16+
using boost::asio::ip::tcp;
17+
18+
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 0));
19+
const uint16_t port = acceptor.local_endpoint().port();
20+
21+
std::thread server([&]() {
22+
boost::system::error_code server_error;
23+
tcp::socket peer(io);
24+
acceptor.accept(peer, server_error);
25+
if (server_error) {
26+
return;
27+
}
28+
if (!payload.empty()) {
29+
boost::asio::write(peer, boost::asio::buffer(payload), server_error);
30+
}
31+
peer.shutdown(tcp::socket::shutdown_send, server_error);
32+
peer.close(server_error);
33+
});
34+
35+
std::string error_message;
36+
try {
37+
tcp::socket client(io);
38+
client.connect(tcp::endpoint(boost::asio::ip::address_v4::loopback(), port));
39+
InitSession(client);
40+
} catch (const std::exception& e) {
41+
error_message = e.what();
42+
}
43+
44+
server.join();
45+
return error_message;
46+
}
47+
48+
} // namespace
49+
50+
TEST(VdfClientSessionRegressionTest, TruncatedDiscriminantSizeFailsBeforeParse) {
51+
const std::vector<uint8_t> payload = {'0', '3'};
52+
const std::string error = run_init_session_with_payload(payload);
53+
EXPECT_NE(error.find("Connection closed while reading discriminant size"), std::string::npos);
54+
}
55+
56+
TEST(VdfClientSessionRegressionTest, ZeroDiscriminantSizeIsRejected) {
57+
const std::vector<uint8_t> payload = {'0', '0', '0'};
58+
const std::string error = run_init_session_with_payload(payload);
59+
EXPECT_NE(error.find("Invalid discriminant size"), std::string::npos);
60+
}
61+
62+
TEST(VdfClientSessionRegressionTest, OversizedDiscriminantSizeIsRejected) {
63+
const std::vector<uint8_t> payload = {'3', '5', '0'};
64+
const std::string error = run_init_session_with_payload(payload);
65+
EXPECT_NE(error.find("Invalid discriminant size"), std::string::npos);
66+
}
67+
68+
TEST(VdfClientSessionRegressionTest, TruncatedFormSizeFailsBeforeValidation) {
69+
const std::vector<uint8_t> payload = {'0', '0', '3', 'a', 'b', 'c'};
70+
const std::string error = run_init_session_with_payload(payload);
71+
EXPECT_NE(error.find("Connection closed while reading form size"), std::string::npos);
72+
}
73+
74+
TEST(VdfClientSessionRegressionTest, ZeroFormSizeIsRejected) {
75+
const std::vector<uint8_t> payload = {'0', '0', '3', 'a', 'b', 'c', 0x00};
76+
const std::string error = run_init_session_with_payload(payload);
77+
EXPECT_NE(error.find("Invalid form size"), std::string::npos);
78+
}
79+
80+
TEST(VdfClientSessionRegressionTest, WrappedNegativeFormSizeIsRejected) {
81+
// 0xFF arrives as -1 in signed char and must be rejected by the <= 0 check.
82+
const std::vector<uint8_t> payload = {'0', '0', '3', 'a', 'b', 'c', 0xFF};
83+
const std::string error = run_init_session_with_payload(payload);
84+
EXPECT_NE(error.find("Invalid form size"), std::string::npos);
85+
}

0 commit comments

Comments
 (0)