Files
wownero/tests/hash/wbag_test.cpp
T
2025-09-22 13:03:50 +03:00

810 lines
29 KiB
C++

#include <array>
#include <cerrno>
#include <chrono>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <ios>
#include <sstream>
#include <string>
#include <unordered_map>
#include <vector>
#include <algorithm>
#include <cctype>
#include <stdexcept>
#include <thread>
#include <atomic>
#include <random>
#include <mutex>
#if defined(_MSC_VER) && !defined(__clang__)
#include <intrin.h>
#pragma intrinsic(__popcnt)
#endif
static inline unsigned pc8(unsigned x) {
#if defined(_MSC_VER) && !defined(__clang__)
return __popcnt(x);
#elif defined(__GNUC__) || defined(__clang__)
return __builtin_popcount(x);
#else
// SW fallback
x = x - ((x >> 1) & 0x55555555u);
x = (x & 0x33333333u) + ((x >> 2) & 0x33333333u);
return (((x + (x >> 4)) & 0x0F0F0F0Fu) * 0x01010101u) >> 24;
#endif
}
#include "misc_log_ex.h"
#include "warnings.h"
#include "string_tools.h"
#include "common/util.h"
#include "crypto/hash.h"
#include "crypto/wbag-hash.h"
#include "cryptonote_basic/wbag_cache.h"
#include "wbag_test.h"
extern "C" {
void WBAG_set_aes_mode(int mode);
int WBAG_get_aes_mode(void);
int WBAG_hw_aes_available(void);
}
using namespace std;
using namespace crypto;
// Hardcoded data
uint32_t params_signature = 0xb8e4f207u;
struct wbag_anchor_sample {
uint64_t height;
const char *block_id_hex;
};
static constexpr wbag_anchor_sample k_wbag_anchor_samples[] = {
{770976, "731cabafc8626ce567be7d2ce161d0fefdaa0e6a4f88275513f199362b8d53fd"},
{770832, "c8710d1ff2b98134c4777e1bbc8dc90ddb6d55a2be850dc904c20218cdb5a970"},
{770688, "f54b797ed2c44e2c37a455118df2eef4656d159ac18ed8aa5de1affac2f5c20b"},
{770544, "5197fb807e62e83a5e50e155ddb9975066aca574e22aa6e2f5ccc0a380f1618a"},
{770400, "4b2981fd2b65aeeb28181ba37c1e49181db4123060f1fa205124aa78e77e4963"},
};
static const std::unordered_map<uint64_t, std::string> kGoldenCacheSHA256 = {
{770976, "add71b5d2713a5c03b20b37e070ef2955211de7392537a8ed2250a8dcf0fa49d"},
{770832, "64f2ebe4fc0ccdb7236eb20887db508f04265da7ead73928d436eb59d92d673d"},
{770688, "720bab8f69866dc7fc0ea277e76494139c5f70da6d3b54096639b4e1b85cac8b"},
{770544, "d2001885d690843454f24391ab9f025b1ceaafe3d348c82e26c20d16127d3c43"},
{770400, "ef75d47acfaa24e810fb87228947fc3e0547938bf824c254d093de64d3dd1590"},
};
// Helpers
static inline bool is_hex_digit(char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
static std::string trim(const std::string& s) {
const auto a = s.find_first_not_of(" \t\r\n");
if (a == std::string::npos) return {};
const auto b = s.find_last_not_of(" \t\r\n");
return s.substr(a, b - a + 1);
}
static std::string lower_hex(const uint8_t* p, size_t n) {
std::ostringstream o;
o << std::hex << std::setfill('0');
for (size_t i = 0; i < n; ++i) o << std::setw(2) << unsigned(p[i]);
return o.str();
}
static inline int hex_nibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static std::vector<uint8_t> hex_to_bytes(const std::string& hex) {
if ((hex.size() & 1) != 0) {
std::cerr << "vectors: odd-length hex input\n"; std::exit(1);
}
std::vector<uint8_t> out(hex.size()/2);
for (size_t i = 0, j = 0; i < hex.size(); i += 2, ++j) {
const int hi = hex_nibble(hex[i]);
const int lo = hex_nibble(hex[i+1]);
if (hi < 0 || lo < 0) { std::cerr << "vectors: non-hex char in input\n"; std::exit(1); }
out[j] = static_cast<uint8_t>((hi << 4) | lo);
}
return out;
}
static void test_wbag_params_signature(uint32_t got, uint32_t expect = params_signature) {
if (got != expect) {
std::cerr << "params: signature mismatch\n"
<< " expected: 0x" << std::hex << expect
<< "\n got: 0x" << got << std::dec << "\n";
std::exit(1);
}
}
static bool ensure_wbag_cache()
{
static std::once_flag once;
static bool ready = false;
std::call_once(once, []{
for (const auto &sample : k_wbag_anchor_samples) {
crypto::hash anchor{};
if (!epee::string_tools::hex_to_pod(sample.block_id_hex, anchor)) continue;
const uint64_t ep = cryptonote::wbag::epoch(sample.height);
if (!cryptonote::wbag::ensure_cache(ep, anchor)) continue;
auto view = cryptonote::wbag::cache_view();
if (view && view.data()) { ready = true; break; }
}
});
auto view = cryptonote::wbag::cache_view();
return ready && view && view.data();
}
void wbag_hash(const void *data, size_t length, char *hash)
{
if (!ensure_wbag_cache())
throw std::runtime_error("WBAG cache unavailable");
auto cached = cryptonote::wbag::cache_view();
if (!cached || !cached.data())
throw std::runtime_error("WBAG cache view invalid");
if (WBAG_hash(data, length, cached.data(), reinterpret_cast<uint8_t*>(hash)) != 0)
throw std::runtime_error("WBAG_hash failed");
}
static bool write_cache_and_sha256(const cryptonote::wbag::CacheView& view, std::string& sha_hex, std::string& note) {
static constexpr const char *k_cache_file = "wbag_cache.dat";
{
std::ofstream out(k_cache_file, std::ios::binary | std::ios::trunc);
if (!out) { note = "write cache file failed"; return false; }
out.write(reinterpret_cast<const char*>(view.data()), static_cast<std::streamsize>(view.size()));
if (!out.good()) { note = "write cache bytes failed"; return false; }
}
crypto::hash file_hash{};
if (!tools::sha256sum(k_cache_file, file_hash)) {
note = "sha256sum failed";
(void)std::remove(k_cache_file);
return false;
}
sha_hex = epee::string_tools::pod_to_hex(file_hash);
(void)std::remove(k_cache_file);
return true;
}
static bool sha256_file(const char* path, std::string& sha_hex, std::string& note) {
crypto::hash h{};
if (!tools::sha256sum(path, h)) { note = "sha256sum(file) failed"; return false; }
sha_hex = epee::string_tools::pod_to_hex(h);
return true;
}
static bool sha256_bytes_tmp(const uint8_t* data, size_t len, std::string& sha_hex, std::string& note) {
static constexpr const char *k_tmp = "wbag_cache.tmp";
{
std::ofstream out(k_tmp, std::ios::binary | std::ios::trunc);
if (!out) { note = "tmp file open failed"; return false; }
out.write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(len));
if (!out.good()) { note = "tmp file write failed"; (void)std::remove(k_tmp); return false; }
}
const bool ok = sha256_file(k_tmp, sha_hex, note);
(void)std::remove(k_tmp);
return ok;
}
static bool build_cache_with_threads(uint64_t height, const crypto::hash& anchor, unsigned T, std::vector<uint8_t>& buf, std::string& note, double* build_sec /*=nullptr*/)
{
note.clear();
uint8_t seed[32];
WBAG_epoch_seed(cryptonote::wbag::epoch(height), reinterpret_cast<const uint8_t*>(&anchor), seed);
const uint64_t bytes64 = WBAG_cache_bytes();
const uint32_t entry_b = WBAG_entry_bytes();
if (entry_b == 0 || bytes64 == 0 || (bytes64 % entry_b) != 0) { note = "geometry invalid"; return false; }
const uint64_t n_entries = bytes64 / entry_b;
if (T < 1) T = 1;
if (T > n_entries) T = static_cast<unsigned>(n_entries);
try { buf.resize(static_cast<size_t>(bytes64)); }
catch (...) { note = "alloc failed"; return false; }
const auto start_build = std::chrono::high_resolution_clock::now();
if (T == 1) { WBAG_build_cache(seed, buf.data(), 0, n_entries); }
else {
std::atomic<bool> failed{false};
std::vector<std::thread> workers;
workers.reserve(T);
for (unsigned i = 0; i < T; ++i) {
const uint64_t start = (static_cast<uint64_t>(i) * n_entries) / T;
const uint64_t end = (static_cast<uint64_t>(i + 1) * n_entries) / T;
workers.emplace_back([&, start, end]() noexcept {
try { WBAG_build_cache(seed, buf.data(), start, end); }
catch (...) { failed.store(true, std::memory_order_relaxed); }
});
}
for (auto &th : workers) {
try { th.join(); } catch (...) { failed.store(true, std::memory_order_relaxed); }
}
if (failed.load(std::memory_order_relaxed)) { note = "worker failure"; return false; }
}
if (build_sec) *build_sec = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - start_build).count();
return true;
}
static std::vector<unsigned> choose_thread_counts() {
unsigned L = std::thread::hardware_concurrency(); if (!L) L = 1;
unsigned P = (L >= 2) ? (L / 2) : 1;
const uint32_t entry_b = WBAG_entry_bytes();
const uint64_t cache_b = WBAG_cache_bytes();
if (entry_b == 0 || cache_b == 0) return {1u};
const uint64_t n_entries = cache_b / entry_b;
if (n_entries == 0) return {1u};
std::vector<unsigned> ts = {1u, P, std::min<unsigned>(P + 1, L), L};
std::sort(ts.begin(), ts.end());
ts.erase(std::unique(ts.begin(), ts.end()), ts.end());
ts.erase(std::remove_if(ts.begin(), ts.end(), [&](unsigned t){ return t == 0 || t > n_entries; }), ts.end());
if (ts.empty()) ts.push_back(1u);
return ts;
}
static void benchmark_hash(const cryptonote::wbag::CacheView& view, int trials, const std::vector<uint8_t>& block, double& perf_khs_out, std::string& last_digest_hex_out) {
std::vector<uint8_t> out(crypto::HASH_SIZE);
// Warm-up
for (int i = 0; i < 64; ++i)
WBAG_hash(block.data(), block.size(), view.data(), out.data());
// Timed runs: median of 3
double times[3];
for (int r = 0; r < 3; ++r) {
const auto t0 = std::chrono::high_resolution_clock::now();
if (trials <= 512) {
for (int i = 0; i < trials; ++i) {
WBAG_hash(block.data(), block.size(), view.data(), out.data());
if (((i + 1) % 8) == 0) std::cerr << "." << std::flush;
}
} else {
for (int i = 0; i < trials; ++i)
WBAG_hash(block.data(), block.size(), view.data(), out.data());
}
times[r] = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - t0).count();
}
std::sort(std::begin(times), std::end(times));
const double elapsed = times[1];
perf_khs_out = (trials / elapsed) / 1000.0;
crypto::hash h{};
static_assert(sizeof(h) == crypto::HASH_SIZE, "hash size mismatch");
std::memcpy(&h, out.data(), crypto::HASH_SIZE);
last_digest_hex_out = epee::string_tools::pod_to_hex(h);
std::cerr << " - Hash rate: " << std::fixed << std::setprecision(2) << perf_khs_out << " kH/s\n";
}
// Test units
static void test_wbag_check_vectors(const std::string& path = "tests/hash/tests-wbag.txt") {
std::ifstream in(path);
if (!in) {
std::cerr << "vectors: cannot open " << path << "\n"; std::exit(1);
}
size_t cases = 0;
std::string line, pending_hash;
auto process_pair = [&](const std::string& expect_hex, const std::string& input_hex) {
const std::string expect = trim(expect_hex);
const std::string input = trim(input_hex);
if (expect.size() != 64) {
std::cerr << "vectors: expected-hash len != 64\n"; std::exit(1);
}
for (char c : expect) if (!is_hex_digit(c)) {
std::cerr << "vectors: expected-hash not hex\n"; std::exit(1);
}
const auto msg = input.empty() ? std::vector<uint8_t>{} : hex_to_bytes(input);
crypto::hash out{};
wbag_hash(msg.empty() ? nullptr : msg.data(), msg.size(),
reinterpret_cast<char*>(&out));
const std::string got = lower_hex(reinterpret_cast<const uint8_t*>(&out), sizeof(out));
if (!std::equal(expect.begin(), expect.end(), got.begin(), got.end(),
[](char a, char b){
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
})) {
std::cerr << "vectors: mismatch at case " << (cases+1) << "\n" << " expected: " << expect << "\n" << " got: " << got << "\n";
std::exit(1);
}
++cases;
};
while (std::getline(in, line)) {
std::string s = trim(line);
if (s.empty() || s[0] == '#') continue;
const auto sp = s.find(' ');
if (sp != std::string::npos) {
const std::string left = s.substr(0, sp);
const std::string right = s.substr(sp + 1);
process_pair(left, right);
continue;
}
if (pending_hash.empty()) {
pending_hash = s;
continue;
} else {
process_pair(pending_hash, s);
pending_hash.clear();
}
}
if (!pending_hash.empty()) {
process_pair(pending_hash, "");
pending_hash.clear();
}
}
static void test_wbag_determinism(const cryptonote::wbag::CacheView& view) {
std::array<uint8_t, crypto::HASH_SIZE> out0{}, out1{};
const std::vector<uint8_t> msg(80, 0x42);
if (WBAG_hash(msg.data(), msg.size(), view.data(), out0.data()) != 0) {
std::cerr << "determinism: first hash failed\n"; std::exit(1);
}
for (int i = 0; i < 31; ++i) {
if (WBAG_hash(msg.data(), msg.size(), view.data(), out1.data()) != 0) {
std::cerr << "determinism: repeat hash failed\n"; std::exit(1);
}
if (memcmp(out0.data(), out1.data(), crypto::HASH_SIZE) != 0) {
std::cerr << "determinism: mismatch on repeat " << i << "\n"; std::exit(1);
}
}
std::cerr << " - Determinism: OK\n";
}
static void test_wbag_edge_inputs(const cryptonote::wbag::CacheView& view) {
std::array<uint8_t, crypto::HASH_SIZE> out{};
if (WBAG_hash(nullptr, 0, view.data(), out.data()) != 0) {
std::cerr << "edge: len=0 failed\n"; std::exit(1);
}
uint8_t buf[66]; for (int i = 0; i < 66; ++i) buf[i] = static_cast<uint8_t>(i);
const size_t lens[] = {1, 63, 64, 65};
for (size_t L : lens) {
if (WBAG_hash(buf + 1, L, view.data(), out.data()) != 0) {
std::cerr << "edge: len=" << L << " failed\n"; std::exit(1);
}
}
std::cerr << " - Edge inputs: OK\n";
}
static void test_wbag_api_errors(const cryptonote::wbag::CacheView& view) {
std::array<uint8_t, crypto::HASH_SIZE> out{};
errno = 0;
if (WBAG_hash(nullptr, 4, view.data(), out.data()) != -1 || errno != EINVAL) {
std::cerr << "api: expected EINVAL for bd=null,len>0\n"; std::exit(1);
}
errno = 0;
if (WBAG_hash(nullptr, 0, nullptr, out.data()) != -1 || errno != EINVAL) {
std::cerr << "api: expected EINVAL for cache=null\n"; std::exit(1);
}
errno = 0;
if (WBAG_hash(nullptr, 0, view.data(), nullptr) != -1 || errno != EINVAL) {
std::cerr << "api: expected EINVAL for out32=null\n"; std::exit(1);
}
std::cerr << " - API errors check: OK\n";
}
static void test_wbag_cache_sha_stability(uint64_t height,
const std::string& sha_hex_again) {
static std::unordered_map<uint64_t, std::string> seen;
auto it = seen.find(height);
if (it == seen.end()) {
seen.emplace(height, sha_hex_again);
} else if (it->second != sha_hex_again) {
std::cerr << "cache-sha: instability for height " << height << "\n"; std::exit(1);
}
for (const auto& kv : seen) {
if (kv.first != height && kv.second == sha_hex_again) {
std::cerr << "cache-sha: SHA collision across anchors " << kv.first << " and " << height << "\n"; std::exit(1);
}
}
std::cerr << " - Cache stability/distinctness: OK\n";
}
static void test_wbag_cache_hash_check(uint64_t height, const std::string& sha256_hex) {
auto it = kGoldenCacheSHA256.find(height);
if (it == kGoldenCacheSHA256.end()) {
std::cerr << " - Cache SHA-256 hash match: skipped (no golden)\n";
return;
}
if (sha256_hex != it->second) {
std::cerr << " - Cache SHA-256 hash match: MISMATCH\n"
<< " expected: " << it->second << "\n"
<< " got: " << sha256_hex << "\n";
std::exit(1);
}
std::cerr << " - Cache SHA-256 hash match: OK\n";
}
static void test_wbag_aes_parity_check(const cryptonote::wbag::CacheView& view, const std::vector<uint8_t>& block) {
if (!view || !view.data() || view.size() == 0) {
std::cerr << " - AES HW vs SW parity: invalid cache view\n";
std::exit(1);
}
if (!WBAG_hw_aes_available()) {
std::cerr << " - AES HW vs SW parity: skipped (no AES present)\n";
return;
}
std::array<uint8_t, crypto::HASH_SIZE> out_hw{}, out_sw{};
const int prev_mode = WBAG_get_aes_mode();
WBAG_set_aes_mode(1);
if (WBAG_hash(block.data(), block.size(), view.data(), out_hw.data()) != 0) {
std::cerr << " - AES HW vs SW parity: hash(hardware) failed\n";
WBAG_set_aes_mode(prev_mode);
std::exit(1);
}
WBAG_set_aes_mode(0);
if (WBAG_hash(block.data(), block.size(), view.data(), out_sw.data()) != 0) {
std::cerr << " - AES HW vs SW parity: hash(software) failed\n";
WBAG_set_aes_mode(prev_mode);
std::exit(1);
}
WBAG_set_aes_mode(prev_mode);
if (std::memcmp(out_hw.data(), out_sw.data(), crypto::HASH_SIZE) != 0) {
std::cerr << " - AES HW vs SW parity: MISMATCH\n";
std::exit(1);
}
std::cerr << " - AES HW vs SW parity: OK\n";
}
static void test_wbag_fuzz(const cryptonote::wbag::CacheView& view, uint32_t iters=2000) {
std::vector<uint8_t> buf(1u<<20);
std::mt19937_64 rng(0xC0FFEEULL);
std::uniform_int_distribution<size_t> len_dist(0, buf.size());
std::uniform_int_distribution<size_t> off_dist(0, 63);
for (uint32_t i=0; i<iters; ++i) {
for (auto& b : buf) b = static_cast<uint8_t>(rng());
size_t L = len_dist(rng);
size_t off = std::min(off_dist(rng), L);
std::array<uint8_t, crypto::HASH_SIZE> out{};
if (WBAG_hash(buf.data()+off, L-off, view.data(), out.data()) != 0) {
std::cerr << "fuzz: hash failed at iter " << i << "\n"; std::exit(1);
}
}
std::cerr << " - Fuzz: OK\n";
}
static void test_wbag_avalanche(const cryptonote::wbag::CacheView& view) {
std::array<uint8_t,64> msg{};
std::array<uint8_t, crypto::HASH_SIZE> base{}, alt{};
if (WBAG_hash(msg.data(), msg.size(), view.data(), base.data()) != 0) {
std::cerr << "avalanche: base hash failed\n"; std::exit(1);
}
auto hdist = [](const uint8_t* a, const uint8_t* b){
unsigned c=0;
for (size_t i=0;i<crypto::HASH_SIZE;i++) c += pc8(unsigned(a[i]^b[i]));
return c;
};
double sum=0;
for (int bit=0; bit<64*8; ++bit) {
msg[bit>>3] ^= (1u<<(bit&7));
if (WBAG_hash(msg.data(), msg.size(), view.data(), alt.data()) != 0) {
std::cerr << "avalanche: alt hash failed at bit " << bit << "\n"; std::exit(1);
}
sum += hdist(base.data(), alt.data());
msg[bit>>3] ^= (1u<<(bit&7));
}
double avg = sum / (64*8);
if (avg < 112 || avg > 144) {
std::cerr << "avalanche: weak diffusion, avg bit flip=" << avg << "\n"; std::exit(1);
}
std::cerr << " - Avalanche: OK (avg " << std::fixed << std::setprecision(1) << avg << " bits)\n";
}
static void test_wbag_unaligned_inputs(const cryptonote::wbag::CacheView& view) {
std::vector<uint8_t> buf(4096);
for (size_t i = 0; i < buf.size(); ++i) buf[i] = uint8_t(i);
const size_t L = 1024, off = 37; // misaligned
std::array<uint8_t, crypto::HASH_SIZE> a{}, b{};
if (WBAG_hash(buf.data() + off, L, view.data(), a.data()) != 0) {
std::cerr << "unaligned: hash(slice) failed\n"; std::exit(1);
}
std::vector<uint8_t> tmp(buf.begin() + off, buf.begin() + off + L);
if (WBAG_hash(tmp.data(), tmp.size(), view.data(), b.data()) != 0) {
std::cerr << "unaligned: hash(copy) failed\n"; std::exit(1);
}
if (std::memcmp(a.data(), b.data(), crypto::HASH_SIZE) != 0) {
std::cerr << "unaligned: mismatch\n"; std::exit(1);
}
std::cerr << " - Unaligned inputs: OK\n";
}
static void test_wbag_large_sizes_smoke(const cryptonote::wbag::CacheView& view) {
const size_t sizes[] = {0,1,63,64,65,1024, 65536, 1048576, 1048583};
std::vector<uint8_t> buf(1048583);
for (size_t i = 0; i < buf.size(); ++i) buf[i] = uint8_t((i * 1315423911u) >> 24);
std::array<uint8_t, crypto::HASH_SIZE> out{}, out2{};
for (size_t s : sizes) {
if (WBAG_hash(buf.data(), s, view.data(), out.data()) != 0) {
std::cerr << "large: hash failed at size " << s << "\n"; std::exit(1);
}
}
// Diffusion smoke: flip byte and expect output change
const size_t pos = 1048576 - 1;
if (WBAG_hash(buf.data(), 1048576, view.data(), out.data()) != 0) {
std::cerr << "large: baseline hash failed\n"; std::exit(1);
}
buf[pos] ^= 0x80;
if (WBAG_hash(buf.data(), 1048576, view.data(), out2.data()) != 0) {
std::cerr << "large: alt hash failed\n"; std::exit(1);
}
if (std::memcmp(out.data(), out2.data(), crypto::HASH_SIZE) == 0) {
std::cerr << "large: no change after byte flip\n"; std::exit(1);
}
std::cerr << " - Large-size smoke: OK\n";
}
static void test_wbag_mode_toggle_stability(const cryptonote::wbag::CacheView& view, const std::vector<uint8_t>& block) {
std::array<uint8_t, crypto::HASH_SIZE> base{}, cur{};
const int prev = WBAG_get_aes_mode();
WBAG_set_aes_mode(-1);
if (WBAG_hash(block.data(), block.size(), view.data(), base.data()) != 0) {
std::cerr << "mode-toggle: base hash failed\n"; std::exit(1);
}
for (int i = 0; i < 64; ++i) {
WBAG_set_aes_mode((i & 1) ? 0 : 1);
if (WBAG_hash(block.data(), block.size(), view.data(), cur.data()) != 0) {
std::cerr << "mode-toggle: hash failed at iter " << i << "\n"; std::exit(1);
}
if (std::memcmp(base.data(), cur.data(), crypto::HASH_SIZE) != 0) {
std::cerr << "mode-toggle: mismatch at iter " << i << "\n"; std::exit(1);
}
}
WBAG_set_aes_mode(prev);
std::cerr << " - AES mode toggle stability: OK\n";
}
static void test_wbag_multithread_smoke(const cryptonote::wbag::CacheView& view) {
const int T = 8, I = 256;
std::vector<uint8_t> common(80, 0x42);
std::array<uint8_t, crypto::HASH_SIZE> ref{};
if (WBAG_hash(common.data(), common.size(), view.data(), ref.data()) != 0) {
std::cerr << "threaded: ref hash failed\n"; std::exit(1);
}
std::atomic<int> err{0};
std::vector<std::thread> th;
th.reserve(T);
for (int t = 0; t < T; ++t) {
th.emplace_back([&, t]{
std::vector<uint8_t> msg(1000 + t);
for (size_t i = 0; i < msg.size(); ++i) msg[i] = uint8_t((i + t) & 0xFF);
std::array<uint8_t, crypto::HASH_SIZE> a{}, b{}, c{};
for (int i = 0; i < I; ++i) {
if (WBAG_hash(msg.data(), msg.size(), view.data(), a.data()) != 0) { ++err; return; }
if (WBAG_hash(msg.data(), msg.size(), view.data(), b.data()) != 0) { ++err; return; }
if (std::memcmp(a.data(), b.data(), crypto::HASH_SIZE) != 0) { ++err; return; }
if (WBAG_hash(common.data(), common.size(), view.data(), c.data()) != 0) { ++err; return; }
if (std::memcmp(ref.data(), c.data(), crypto::HASH_SIZE) != 0) { ++err; return; }
}
});
}
for (auto& x : th) x.join();
if (err.load() != 0) { std::cerr << "threaded: mismatch\n"; std::exit(1); }
std::cerr << " - Multithread smoke: OK\n";
}
static void test_wbag_multithread_cache_test() {
const wbag_anchor_sample* sample = nullptr;
for (const auto& s : k_wbag_anchor_samples) { if (s.height == 770976) { sample = &s; break; } }
if (!sample) { std::cerr << " - Multithread cache test: missing sample 770976\n"; std::exit(1); }
crypto::hash anchor{};
if (!epee::string_tools::hex_to_pod(sample->block_id_hex, anchor)) {
std::cerr << " - Multithread cache test: bad anchor hex\n"; std::exit(1);
}
const auto Ts = choose_thread_counts();
std::vector<std::pair<unsigned,double>> times;
std::vector<std::string> shas;
std::string note;
for (unsigned T : Ts) {
std::vector<uint8_t> buf;
double build_sec = 0.0;
if (!build_cache_with_threads(sample->height, anchor, T, buf, note, &build_sec)) {
std::cerr << " - Multithread cache test: build(" << T << ") failed: " << note << "\n"; std::exit(1);
}
std::string sha;
if (!sha256_bytes_tmp(buf.data(), buf.size(), sha, note)) {
std::cerr << " - Multithread cache test: sha(" << T << ") failed: " << note << "\n"; std::exit(1);
}
times.emplace_back(T, build_sec);
shas.emplace_back(std::move(sha));
}
for (size_t i = 1; i < shas.size(); ++i) {
if (shas[i] != shas[0]) {
std::cerr << " - Multithread cache test: MISMATCH between T=" << Ts[0] << " and T=" << Ts[i] << "\n"
<< " sha(" << Ts[0] << "): " << shas[0] << "\n"
<< " sha(" << Ts[i] << "): " << shas[i] << "\n";
std::exit(1);
}
}
std::ostringstream line;
line << "Multithread cache test: ";
for (size_t i = 0; i < times.size(); ++i) {
line << times[i].first << " (" << std::fixed << std::setprecision(3) << times[i].second << " s)";
if (i + 1 < times.size()) line << ", ";
}
std::cerr << line.str() << "\n\n";
}
static void test_wbag_first_use_race() {
constexpr int T = 16;
std::atomic<bool> go{false};
std::atomic<int> err{0};
std::vector<std::thread> th; th.reserve(T);
for (int i=0;i<T;++i) {
th.emplace_back([&]{
while (!go.load(std::memory_order_acquire)) {}
std::array<uint8_t, crypto::HASH_SIZE> out{};
uint8_t b = 0x42;
try { wbag_hash(&b, 1, reinterpret_cast<char*>(out.data())); }
catch (...) { ++err; }
});
}
go.store(true, std::memory_order_release);
for (auto& x: th) x.join();
if (err.load() != 0) { std::cerr << "first-use race: fail\n"; std::exit(1); }
}
static void test_wbag_geometry() {
const uint64_t cb = WBAG_cache_bytes();
const uint32_t eb = WBAG_entry_bytes();
if (cb == 0 || eb == 0 || (cb % eb) != 0) {
std::cerr << "geometry: invalid\n"; std::exit(1);
}
}
static void run_wbag_tests(const cryptonote::wbag::CacheView& view, uint64_t height, const std::string& sha256_hex, const std::vector<uint8_t>& block) {
if (!sha256_hex.empty()) {
test_wbag_cache_sha_stability(height, sha256_hex);
test_wbag_cache_hash_check(height, sha256_hex);
}
test_wbag_determinism(view);
test_wbag_edge_inputs(view);
test_wbag_api_errors(view);
test_wbag_aes_parity_check(view, block);
test_wbag_fuzz(view);
test_wbag_avalanche(view);
test_wbag_unaligned_inputs(view);
test_wbag_large_sizes_smoke(view);
test_wbag_mode_toggle_stability(view, block);
test_wbag_multithread_smoke(view);
}
// Start WBG tests
int wbag_test() {
struct Row {
uint64_t height = 0;
uint64_t epoch = 0;
size_t cache_size = 0;
double ensure_sec = 0.0;
double perf_khs = 0.0;
std::string sha256_hex;
std::string last_digest_hex;
std::string note;
};
// Params
const int trials = 2048;
const std::vector<uint8_t> block(80, 0x42);
const size_t cache_bytes = WBAG_cache_bytes();
const double cache_mb = static_cast<double>(cache_bytes) / (1024.0 * 1024.0);
const uint32_t params_sig = WBAG_params_signature();
const int total = static_cast<int>(sizeof(k_wbag_anchor_samples) / sizeof(k_wbag_anchor_samples[0]));
test_wbag_params_signature(params_sig);
test_wbag_first_use_race();
test_wbag_geometry();
test_wbag_check_vectors();
std::cerr << "Params: n_anchors=" << total
<< ", cache_size=" << std::fixed << std::setprecision(2) << cache_mb
<< " MB, sig=0x" << std::hex << params_sig << std::dec
<< ", sig=OK, vectors=OK, first_use=OK, geometry=OK\n\n";
test_wbag_multithread_cache_test();
// Report
std::ostringstream report;
report << "\n\n";
report << std::left << std::setw(12) << "Height" << std::setw(66) << "SHA256 (cache)" << "Digest\n";
report << std::string(12+66+64, '-') << "\n";
for (int idx = 0; idx < total; ++idx) {
const auto &sample = k_wbag_anchor_samples[idx];
Row r;
r.height = sample.height;
r.epoch = cryptonote::wbag::epoch(sample.height);
crypto::hash anchor{};
if (!epee::string_tools::hex_to_pod(sample.block_id_hex, anchor)) {
r.note = "bad anchor hex";
} else {
std::cerr << "[" << (idx+1) << "/" << total << "] height " << r.height << " epoch " << r.epoch << ": generating cache..." << std::flush;
if (idx == 0 && total > 1) {
crypto::hash other{};
if (epee::string_tools::hex_to_pod(k_wbag_anchor_samples[1].block_id_hex, other)) {
const uint64_t other_ep = cryptonote::wbag::epoch(k_wbag_anchor_samples[1].height);
(void)cryptonote::wbag::ensure_cache(other_ep, other);
}
}
const auto t0 = std::chrono::high_resolution_clock::now();
if (!cryptonote::wbag::ensure_cache(r.epoch, anchor)) {
r.note = "ensure_cache failed";
std::cerr << " FAILED\n";
} else {
r.ensure_sec = std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - t0).count();
std::cerr << " " << std::fixed << std::setprecision(3) << r.ensure_sec << " s\n";
auto view = cryptonote::wbag::cache_view();
if (!view || !view.data() || view.size() == 0) {
r.note = "cache_view invalid";
} else {
r.cache_size = view.size();
if (!write_cache_and_sha256(view, r.sha256_hex, r.note)) {
}
// Run tests
run_wbag_tests(view, r.height, r.sha256_hex, block);
// Benchmark
if (r.note.empty()) {
benchmark_hash(view, trials, block, r.perf_khs, r.last_digest_hex);
}
}
}
}
report << std::left
<< std::setw(12) << (r.height ? std::to_string(r.height) : std::string("-"))
<< std::setw(66) << (r.sha256_hex.empty() ? std::string("-") : r.sha256_hex)
<< (r.last_digest_hex.empty() ? std::string("-") : r.last_digest_hex)
<< (r.note.empty() ? "" : (" [" + r.note + "]"))
<< "\n";
}
std::cout << report.str();
return 0;
}