Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .github/workflows/vdf-client-hw.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ src/hw_vdf_client
/verifier
/verifier_test
/build/*
/build-*/
*.whl
*.egg-info
*.o
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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),
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions README_ASIC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 5 additions & 32 deletions src/vdf_client.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <boost/asio.hpp>
#include "vdf.h"
#include "version.hpp"
#include "vdf_client_session.h"
#include <atomic>

using boost::asio::ip::tcp;
Expand All @@ -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];
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
47 changes: 47 additions & 0 deletions src/vdf_client_session.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include <boost/asio.hpp>
#include "bqfc.h"

#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <string>

inline char disc[350];
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);
} 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");
}
85 changes: 85 additions & 0 deletions src/vdf_client_session_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "vdf_client_session.h"

#include <boost/asio.hpp>
#include <gtest/gtest.h>

#include <cstdint>
#include <exception>
#include <string>
#include <thread>
#include <vector>

namespace {

std::string run_init_session_with_payload(const std::vector<uint8_t>& 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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);
}
Loading