Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

03_tests.md - VDF Differential Cryptography Tests

Crate: kinetic-vdf Stage: 6 Reading Time: 25 minutes Depends on: lib.rs main implementation, kyn-vdf crate, chiavdf C++ bindings


What Is This?

This documentation covers the comprehensive internal test suite for the kinetic-vdf crate. Specifically, it focuses on the code located at the bottom of src/lib.rs (Lines 197-600). In Rust, it is a common idiom to place tests in the same file as the source code they are testing. These are usually wrapped in a mod tests block. They are conditionally compiled only during the testing phase using the #[cfg(test)] attribute. This ensures that the test code and its dependencies do not bloat the final release binary.

While standard unit tests usually just check if a single function returns an expected value, the tests contained in this file are far more advanced and critical to the network’s safety. This suite implements a series of rigorous Differential Cryptography Tests. Differential testing is an advanced software engineering and cryptography technique. It involves taking two entirely separate, independent implementations of a complex algorithm, feeding both implementations the exact same input data, and verifying that they produce the exact same outputs every single time.

In the context of this crate, the test suite acts as a bridge and an impartial referee. It compares two distinct VDF (Verifiable Delay Function) engines: 1. The original C++ chiavdf implementation, accessed via FFI (Foreign Function Interface) bindings. 2. The new pure-Rust kyn-vdf implementation, designed specifically for memory safety and strict parsing.

The tests in this file prove unequivocally that these two different codebases agree on the fundamental mathematical rules of the Kinetic network. They ensure that both engines recognize valid mathematical proofs as valid. More importantly, they guarantee that both engines will reject maliciously crafted, corrupted, or invalid proofs in the exact same manner, leaving no room for consensus discrepancies.


Why Kinetic Needs This

In a decentralized blockchain network like Kinetic, the consensus mechanism is everything. This consensus relies entirely on deterministic, unyielding mathematics. Every single node in the peer-to-peer network must independently verify the blockchain’s history. > [!WARNING]

If two nodes in the network disagree on whether a specific block’s VDF proof is valid, the network will instantly suffer a consensus failure. This disagreement will cause the blockchain to split into a hard fork.

Kinetic currently utilizes a dual-engine architecture for its Verifiable Delay Functions:

1. The Timelords:2. The Validator Nodes:
These are specialized, high-performance prover nodes.These are the regular network participants.
They generate the intensive cryptographic proofs using the optimized C++ chiavdf library.They verify the proofs generated by the Timelords as they arrive over the gossip network.
This C++ library is fast and efficient at sequential squaring operations.They use the memory-safe, pure-Rust kyn-vdf engine to perform this verification. This protects the regular nodes from potential C++ memory vulnerabilities like buffer overflows.

Because the proofs are created in C++ but verified in Rust, they must have absolute alignment. Their mathematical processing and binary parsing must be identical down to the very last byte. Consider a scenario where the C++ Timelord generates a proof that it believes is valid. If the Rust validator node rejects that exact same proof due to a slight parsing difference, or a minor mathematical rounding error, the entire blockchain halts immediately. Conversely, consider if a malicious attacker broadcasts a flawed, carefully crafted proof. If the C++ engine would correctly reject it, but the Rust engine accidentally accepts it due to a parser bug, the attacker could forge the blockchain history and hijack the network.

Differential testing is Kinetic’s primary, safety net against these apocalyptic scenarios. Instead of just hoping the two implementations match, this test suite guarantees it. It achieves this by throwing an , punishing battery of tests at both engines simultaneously. It uses valid proofs, invalid proofs, randomly corrupted proofs, proofs with mismatched parameters, and truncated byte arrays to verify that parity is absolute in every conceivable edge case.

Furthermore, this test suite acts as a powerful guarantee for strict Backwards Compatibility. When a blockchain relies on upstream C++ libraries, a silent version update can be deadly. An update might slightly alter how cryptographic seeds (called discriminants) are generated. If a new version silently changes the underlying math, all older blocks would suddenly fail validation. These tests hardcode known, historically expected outputs to act as immutable anchors in the codebase. If the underlying math ever changes silently, these tests will loudly fail during the local build process. This prevents an accidental network fork before the breaking code is even merged into the repository.


How It Works

The Baseline: Happy Path Verification

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 227 to 261
}

The test_kyn_vdf_verifies_kinetic_proof function establishes the fundamental differential baseline. It proves that the two distinct engines can communicate and fundamentally understand each other’s data formats. The test begins by initializing a new C++ ChiaVdfEngine instance. It creates a mock challenge hash, which is simply a structured array of thirty-two 1 bytes. It then instructs the C++ engine to perform exactly 1,000 iterations to generate a real, valid VDF proof.

Once the proof is successfully generated, the test enforces a strict, wire-format check: assert_eq!(proof.proof_bytes.len(), 200) Kinetic relies on the standard Chia wire format for all VDF proofs transmitted over the network. This standard requires the serialized proof to be exactly 200 bytes in length. The first 100 bytes represent the y coordinate of the proof. The second 100 bytes represent the pi structural proof.

Finally, the test takes those exact 200 bytes, straight from the C++ memory allocator, and passes them directly into the pure-Rust kyn_vdf::verify_chia_vdf() function. When the pure-Rust verifier successfully processes them and returns Ok(true), it establishes a critical fact. It proves that the core mathematics and the strict binary serialization format are aligned.

Concurrency and Thread Safety

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 263 to 286
}

The test_concurrent_evaluate function is crucial for testing the integrity and safety of the FFI boundary. C++ libraries are notoriously prone to severe thread-safety issues, especially when called from Rust. Rust is concurrent by design and expects fearless concurrency from all of its underlying dependencies. This test wraps the C++ engine in a Rust Arc (Atomic Reference Counted pointer). It then rapidly spawns multiple native OS threads using the thread::spawn API.

Each spawned thread independently and simultaneously attempts to generate a new VDF proof. They all use the exact same shared C++ engine concurrently. If the underlying C++ code had any unsafe global state, this test would catch it immediately. If it had hidden race conditions, or lacked proper thread-local memory isolation, this concurrent access would definitively trigger a segmentation fault or a memory corruption panic. By passing this test consistently, we guarantee that the kinetic-vdf crate is fully thread-safe. It is safe to use in a parallel, asynchronous environment like a high-throughput blockchain node.

Anchoring the Discriminant for Backwards Compatibility

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 321 to 340
}

The test_discriminant_consistency_across_versions function is our primary safeguard against dependency drift. In VDF cryptography, the “discriminant” is a number securely derived from the initial challenge hash. This discriminant defines the overarching mathematical group used for the sequential squaring operations. The test provides a specific, deterministic challenge hash: [42u8; 32]. It then asks the C++ engine to generate the 128-byte discriminant for this exact hash.

Crucially, the test then compares the first 16 bytes of the newly generated discriminant array against a hardcoded byte array that lives directly in the test source code: [237, 89, 165, 1, 5, 76, 207, 152, 207, 134, 182, 117, 254, 184, 124, 248] > [!IMPORTANT]

This hardcoded array acts as an immutable, unbreakable cryptographic anchor. If a future update to the chiavdf C++ library silently changes its internal hash algorithm, or if it subtly tweaks the derivation logic for the discriminant, the output will no longer match this array. The test will fail immediately and loudly during local development or in the CI pipeline. This warns the core developers that integrating the new C++ version will catastrophically break compatibility. It ensures that we never accidentally deploy a change that would invalidate historical blocks and fork the live network.

The Differential Fuzzer: Testing Failure Modes

#![allow(unused)]
fn main() {
-> See: src/lib.rs — Lines 343 to 598
}

The test_chiavdf_and_kyn_vdf_differential_compatibility function is the absolute crown jewel of the test suite. It acts as an , multi-step, relentless differential fuzzer. It runs a punishing gauntlet of seven rigorous checks over multiple different deterministic challenge hashes. Each of these mathematical challenges is evaluated for a realistic 10,000 iterations to ensure mathematical depth.

Step 1: Proof Generation The test uses the native C++ engine to generate a brand new, valid 200-byte proof. This generated proof is specific to the current deterministic challenge hash in the fuzzer loop.

Step 2 & 3: Dual Verification The test passes the newly generated proof back into the C++ verifier engine. This acts as a basic sanity check, ensuring the C++ engine actually accepts its own valid work. Then, it passes the exact same proof byte array into the pure-Rust kyn-vdf verifier engine. This ensures the Rust engine also accepts the valid proof, re-establishing the differential baseline.

Step 4: Single Bit Corruption -> See: src/lib.rs — Lines 427 to 470 The test intentionally and surgically corrupts the proof to ensure both engines detect the tampering. It clones the original proof bytes into a new, mutable array. It then cleverly uses the bitwise XOR assignment operator (^=) to flip exactly one single bit in the array: corrupted_bytes[50] ^= 0x01; Using XOR to selectively flip a bit in a valid proof is a standard, effective cryptographic testing technique. An attacker can easily provide purely random garbage data to a node. However, robust parsers often reject pure garbage data instantly, before ever reaching the complex mathematical logic. By flipping a single bit in an otherwise perfect and structurally sound proof, we ensure that the test bypasses these early, superficial parsing checks. This forces the verification engine to run the deep mathematical validation algorithms on corrupted data. The mathematical validation must ultimately and decisively fail. The C++ engine is expected to simply return a boolean false. The Rust engine (kyn_vdf) uses a Rust match statement to handle the rejection safely and gracefully. It correctly considers both Ok(false) (the math failed) and Err(_) (deserialization failed) as valid rejections. Both engines must universally reject the corrupted proof without crashing or panicking.

Step 5: Mismatched Iteration Counts -> See: src/lib.rs — Lines 472 to 508 The test takes the valid proof, but it lies to the verifiers about the required computational work. It passes iterations + 1 to both the C++ and Rust engines instead of the correct value. VDF proofs are , purposefully sensitive to the exact iteration count. Even being off by a single iteration means the resulting y coordinate should be , entirely different. This step ensures that both engines correctly and enforce the iteration parameter during verification. They must ruthlessly reject the proof if the parameter does not match the computational work encapsulated in the proof bytes.

Step 6: The Wrong Challenge Hash -> See: src/lib.rs — Lines 510 to 551 The test specifically mutates the original challenge hash. It again uses the XOR operator to flip the very first bit of the hash: wrong_hash[0] ^= 0x80;. However, it presents the original, valid proof bytes to the verifiers alongside this new, incorrect challenge. The challenge hash dictates the discriminant, which alters the mathematical group for the VDF. Therefore, a valid proof for Block A is meaningless garbage when evaluated for Block B. Both engines must firmly and immediately reject this attempt to replay a valid proof in the wrong context.

Step 7: Array Truncation -> See: src/lib.rs — Lines 553 to 590 The test removes the final byte of the serialized proof array. This artificially shrinks the array from the required 200 bytes down to an invalid 199 bytes. This step specifically tests the memory safety and boundary checking of the binary parsers in both languages. In C++, reading beyond the bounds of an array can easily cause a fatal segmentation fault or read uninitialized memory. The test verifies that the chiavdf C++ engine gracefully handles the truncated input array without any issues. It must return false cleanly without crashing the host process or corrupting the heap. For the Rust kyn-vdf engine, it verifies that the deserializer is enforcing the rigid wire format. It must return an explicit Err rather than attempting to pad the array with zeros or triggering a panic.


Key Pieces

The #[cfg(test)] Attribute

  • What it is: A conditional compilation attribute directive built directly into the Rust compiler.
  • Location: src/lib.rs — Line 197
  • Why it matters: It tells the Rust compiler to ignore this module during standard release builds. The code is only compiled and included in the binary when the cargo test command is run. This ensures that heavy testing dependencies, mock data, and extensive fuzzing logic are never shipped to production nodes.

test_kyn_vdf_verifies_kinetic_proof

  • What it does: Generates a real VDF proof using C++ and verifies it immediately using the pure Rust verifier.
  • Location: src/lib.rs — Lines 228 to 261
  • Why it matters: It proves that the “happy path” operates flawlessly across language boundaries and execution environments. It guarantees that the 200-byte binary serialization format translates between the C++ prover and the Rust verifier. It proves that the C++ prover and the Rust verifier speak the exact same mathematical language.

test_discriminant_consistency_across_versions

  • What it does: Generates a discriminant from a known challenge and asserts an exact, byte-for-byte match.
  • Location: src/lib.rs — Lines 321 to 340
  • Why it matters: This is the primary, immovable defense mechanism against upstream dependency drift. It ensures that silent, undocumented changes to the C++ cryptographic library cannot inadvertently fork the blockchain.

test_chiavdf_and_kyn_vdf_differential_compatibility

  • What it does: An extensive, multi-step, rigorous differential fuzzing test loop.
  • Location: src/lib.rs — Lines 343 to 598
  • Why it matters: It proves that the C++ and Rust engines have identical security profiles. They do not just agree on what a valid proof is when everything is correct and well-formed. They agree on exactly why and how invalid, malicious, or corrupted proofs should be rejected.

Bitwise XOR Mutation (^=)

  • What it does: The bitwise XOR (exclusive OR) assignment operator, a staple in low-level programming. It compares the bits of a byte and flips the bit if they differ. For example, corrupted_bytes[50] ^= 0x01; takes the 50th byte and precisely flips its lowest bit.
  • Location: src/lib.rs — Lines 438 and 518
  • Why it matters: In cryptographic testing, providing invalid arrays is often entirely insufficient. Parsers will reject malformed arrays immediately without ever exercising the core mathematical validation logic. By using XOR to flip a single bit in a valid proof, the data looks legitimate enough to bypass the parser. This securely forces the engine to evaluate the complex math, ensuring the deep validation logic correctly catches the error.

The Commitment Struct

  • What it is: A struct from kinetic_core::types used to represent a 32-byte hash.
  • Location: src/lib.rs — Lines 205, 231, 323
  • Why it matters: It encapsulates the challenge hash in a type-safe manner, ensuring that raw byte arrays are not accidentally passed around as valid challenge seeds.

How This Connects to the Rest of Kinetic

CROSS-CRATE DEPENDENCY: Validating kyn-vdf

This entire rigorous test suite is located inside the kinetic-vdf crate (which handles the legacy C++ FFI bindings). However, its true architectural purpose is to bless and validate the new kyn-vdf crate. When the main Kinetic consensus engine inside the kinetic-node crate calls the Rust verifier, it relies entirely, with full trust, on the absolute guarantees provided by this exact differential test suite. Without these tests passing consistently, kyn-vdf cannot be trusted to verify mainnet blocks.

FORWARD DEPENDENCY: Planned Consensus Hard Forks

If the Kinetic network ever decides to purposefully upgrade its cryptography in the future, such as switching to a significantly faster VDF class group or changing the underlying hashing algorithm, the hardcoded discriminant anchors within this file will intentionally and immediately fail. A core protocol developer will be required to manually calculate the new expected byte arrays. They will then have to purposefully update the hardcoded arrays in the source code themselves. This failure acts as a mandatory, unavoidable review gate for all developers. It ensures that any consensus hard fork is planned, thoroughly documented, and publicly communicated to node operators.

FORWARD DEPENDENCY: The Timelord Network Operations

In the live, production Kinetic network, Timelords will constantly execute the C++ chiavdf code. They will generate thousands of proofs every single day to secure the network. Every single validator node on the network will concurrently run the kyn-vdf Rust code to verify them. This test suite directly simulates and validates that exact, critical network interaction. Passing these tests is the primary indicator that the live network will be able to reach and maintain secure consensus.


Quick Reference

  • Differential Testing: A strict testing methodology that runs identical inputs through two entirely distinct implementations. It guarantees identical outputs across different languages (C++ and Rust).
  • Wire Format Requirement: Both engines must adhere to the 200-byte format constraint. This consists of exactly 100 bytes for the y coordinate and exactly 100 bytes for the pi proof structure.
  • Discriminant Anchor: The 128-byte mathematical seed derived from a challenge hash is deeply anchored. It is anchored to hardcoded byte arrays to prevent silent upstream mathematical changes from causing accidental forks.
  • Robust Failure Modes: The fuzzer tests how both engines react to adversarial network conditions. These conditions include specific bit corruption, incorrect iteration counts, mismatched challenge hashes, and array truncation.

Open Questions / Things to Revisit

  • Test Execution Time Constraints: The differential fuzzer currently runs 10,000 iterations. It runs this heavy computational workload for multiple different challenge hashes in a loop. While thorough, this might significantly slow down execution times in debug builds. It could become a major bottleneck in continuous integration (CI) environments for every commit. We should strongly consider conditionalizing the iteration count based on the #[cfg(debug_assertions)] flag. This would allow local unit tests to run blazing fast with fewer iterations for developers. Meanwhile, the heavy 10,000 iteration test would be preserved for release builds and nightly CI runs.
  • Property-Based Fuzzing Vectors: Currently, the bit corruption test only flips one single, specific bit. Specifically, it reliably executes corrupted_bytes[50] ^= 0x01;. We should deeply evaluate integrating a robust property-based testing framework like proptest. This would allow us to randomly and corrupt bits and bytes across the entire 200-byte proof payload. This advanced fuzzing would ensure that there are no specific, undiscovered byte offsets that could trigger a panic, segmentation fault, or unhandled exception in either the C++ or Rust parsers.
  • Zero Iterations Differential Edge Case: The suite currently includes a standalone test_edge_cases function. This function (at Line 306) specifically verifies the C++ engine correctly rejects a proof with zero iterations. We should expand the main differential fuzzer suite to include this zero-iteration scenario as well. This will definitively ensure that the pure-Rust kyn-vdf engine handles this specific edge case identically to C++.
  • Handling of C++ chiavdf Panics: The differential suite operates under a critical, optimistic assumption. It assumes that the C++ chiavdf library will gracefully return false or an error when presented with severe garbage. If the C++ library were to encounter a state that causes a hard segmentation fault or process abort, the Rust test runner would simply crash instantly without a clear Rust-level error trace or stack. This heavy reliance on the C++ library’s internal safety and error handling is precisely the reason why the Kinetic node architecture is actively migrating to the pure-Rust kyn-vdf engine. The Rust engine provides much stronger guarantees against fatal process crashes when handling untrusted remote network data.