Skip to content

Commit 4772283

Browse files
Add GoogleTest regressions for vdf_client session parsing (#313)
* 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> * 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 <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a49a500 commit 4772283

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
@@ -219,7 +219,7 @@ jobs:
219219
if (-not $gnuAsmEnabled) {
220220
throw "ENABLE_GNU_ASM is not ON on Windows CI build"
221221
}
222-
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
222+
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
223223
$asmFiles = @(
224224
"build/asm_compiled.s",
225225
"build/avx2_asm_compiled.s",
@@ -317,6 +317,20 @@ jobs:
317317
.\prover_test.exe
318318
if ($LASTEXITCODE -ne 0) { throw "prover_test failed with exit code $LASTEXITCODE" }
319319
320+
- name: Run vdf_client_session regression (GoogleTest, Unix)
321+
if: matrix.config == 'optimized=1' && matrix.os != 'windows-latest'
322+
run: |
323+
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
324+
cmake --build build-regression --target vdf_client_session_test
325+
ctest --test-dir build-regression --output-on-failure -R '^regression\.'
326+
327+
- name: Run vdf_client_session regression (GoogleTest, Windows)
328+
if: matrix.os == 'windows-latest' && matrix.config == 'optimized=1'
329+
shell: pwsh
330+
run: |
331+
cd build
332+
ctest -C Release --output-on-failure -R '^regression\.'
333+
320334
- name: Benchmark vdf_bench square (Ubuntu/Mac)
321335
if: matrix.config == 'optimized=1' && matrix.os != 'windows-latest'
322336
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
@@ -293,6 +293,19 @@ if(BUILD_VDF_BENCH)
293293
endif()
294294
295295
if(BUILD_VDF_TESTS)
296+
enable_testing()
297+
include(GoogleTest)
298+
if(WIN32)
299+
# Match parent CRT choice on Windows to avoid link/runtime mismatches.
300+
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
301+
endif()
302+
FetchContent_Declare(
303+
googletest
304+
GIT_REPOSITORY https://github.com/google/googletest.git
305+
GIT_TAG v1.14.0
306+
)
307+
FetchContent_MakeAvailable(googletest)
308+
296309
add_executable(1weso_test
297310
${CMAKE_CURRENT_SOURCE_DIR}/1weso_test.cpp
298311
)
@@ -320,6 +333,14 @@ if(BUILD_VDF_TESTS)
320333
target_link_libraries(prover_test PRIVATE ${GMP_LIBRARIES} ${GMPXX_LIBRARIES} Threads::Threads)
321334
vdf_add_windows_clang_opts(prover_test)
322335
336+
add_executable(vdf_client_session_test
337+
${CMAKE_CURRENT_SOURCE_DIR}/vdf_client_session_test.cpp
338+
)
339+
vdf_add_boost_includes(vdf_client_session_test)
340+
target_link_libraries(vdf_client_session_test PRIVATE GTest::gtest_main Threads::Threads)
341+
vdf_add_windows_clang_opts(vdf_client_session_test)
342+
gtest_discover_tests(vdf_client_session_test TEST_PREFIX "regression.")
343+
323344
endif()
324345
325346
if(BUILD_HW_TOOLS)

src/vdf_client.cpp

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

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

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

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

155125
void SessionFastAlgorithm(tcp::socket& sock) {
156126
InitSession(sock);
127+
ConfigureSessionRuntime();
157128
try {
158129
integer D(disc);
159130
integer L = root(-D, 4);
@@ -202,6 +173,7 @@ void SessionFastAlgorithm(tcp::socket& sock) {
202173

203174
void SessionOneWeso(tcp::socket& sock) {
204175
InitSession(sock);
176+
ConfigureSessionRuntime();
205177
try {
206178
integer D(disc);
207179
integer L = root(-D, 4);
@@ -238,6 +210,7 @@ void SessionOneWeso(tcp::socket& sock) {
238210
void SessionTwoWeso(tcp::socket& sock) {
239211
const int kMaxProcessesAllowed = 100;
240212
InitSession(sock);
213+
ConfigureSessionRuntime();
241214
try {
242215
integer D(disc);
243216
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 uint8_t initial_form_s[BQFC_FORM_SIZE];
13+
14+
inline void InitSession(boost::asio::ip::tcp::socket& sock) {
15+
boost::system::error_code error;
16+
char disc_size[5];
17+
int disc_int_size;
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)