mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
chains/cpp/core and quantumvm asked for LUX_GPU_DIR and stopped with a fatal error when nothing set it, so the public P, X and Q builds could not configure at all for anyone outside this machine. The seam is an ABI of four symbols over a complete CPU backend, not a kernel, so it belongs beside the chains defined over it; the kernels stay private in lux-gpu/gpu and are still only ever dlopen'd. The D and F ports move with it, to lux-cpp/chains and lux-rs/chains, both public. luxfi/compute held nothing its licence did not already cover, being the same BSD-3-Clause as this repository's. Co-authored-by: Hanzo Dev <[email protected]>
307 lines
12 KiB
C++
307 lines
12 KiB
C++
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: BSD-3-Clause-Eco
|
|
//
|
|
// gpu_test.cpp — the CPU backend against known answers, and both backends
|
|
// against each other.
|
|
//
|
|
// Run with LUX_GPU_LIB=<path to libluxgpu> to point at an installed kernel
|
|
// library. With none installed the differential says "skipped"; it does not
|
|
// print a green it did not earn.
|
|
|
|
#include "lux/gpu/gpu.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
using namespace lux::gpu;
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
int checks = 0;
|
|
|
|
void check(bool ok, const std::string& what) {
|
|
++checks;
|
|
if (!ok) {
|
|
++failures;
|
|
std::fprintf(stderr, "FAIL %s\n", what.c_str());
|
|
}
|
|
}
|
|
|
|
std::string hex(const Digest& d) {
|
|
std::string s;
|
|
char t[3];
|
|
for (auto b : d) {
|
|
std::snprintf(t, sizeof(t), "%02x", b);
|
|
s += t;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
std::string hex(const Address& a) {
|
|
std::string s;
|
|
char t[3];
|
|
for (auto b : a) {
|
|
std::snprintf(t, sizeof(t), "%02x", b);
|
|
s += t;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
Bytes view(const std::vector<std::uint8_t>& v) { return Bytes(v.data(), v.size()); }
|
|
|
|
std::vector<std::uint8_t> str(const char* s) {
|
|
return std::vector<std::uint8_t>(reinterpret_cast<const std::uint8_t*>(s),
|
|
reinterpret_cast<const std::uint8_t*>(s) + std::strlen(s));
|
|
}
|
|
|
|
// A cheap, seeded, reproducible byte stream. A differential wants many shapes,
|
|
// not a new dependency.
|
|
struct Rng {
|
|
std::uint64_t s;
|
|
std::uint64_t next() {
|
|
s += 0x9e3779b97f4a7c15ull;
|
|
std::uint64_t z = s;
|
|
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull;
|
|
z = (z ^ (z >> 27)) * 0x94d049bb133111ebull;
|
|
return z ^ (z >> 31);
|
|
}
|
|
std::vector<std::uint8_t> bytes(std::size_t n) {
|
|
std::vector<std::uint8_t> v(n);
|
|
for (auto& b : v) b = std::uint8_t(next() & 0xff);
|
|
return v;
|
|
}
|
|
};
|
|
|
|
bool plugin_present() { return backend().rfind("plugin:", 0) == 0; }
|
|
|
|
// ---- the CPU backend is right ----------------------------------------------
|
|
|
|
void the_hashes_are_the_ones_the_chain_is_defined_over() {
|
|
const auto abc = str("abc");
|
|
// Keccak-256, Ethereum's 0x01 pad. SHA3-256("abc") is
|
|
// 3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532; a
|
|
// backend answering with that would be answering a different question,
|
|
// which is exactly what the plugin's op_sha3_256_hash is for.
|
|
check(hex(keccak256(view(abc))) ==
|
|
"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45",
|
|
"keccak256(\"abc\")");
|
|
check(hex(empty_root()) ==
|
|
"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
|
|
"keccak256(\"\") — the empty root");
|
|
check(hex(sha256(view(abc))) ==
|
|
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
|
"sha256(\"abc\")");
|
|
check(hex(pubkey_to_address(view(abc))) == "bb1be98c142444d7a56aa3981c3942a978e4dc33",
|
|
"ripemd160(sha256(\"abc\"))");
|
|
}
|
|
|
|
void a_batch_answers_exactly_what_the_singles_answer() {
|
|
Rng rng{0x5eed};
|
|
const std::size_t sizes[] = {0, 1, 31, 32, 135, 136, 137, 271, 272, 1024, 4096};
|
|
for (std::size_t batch : {std::size_t(1), std::size_t(2), std::size_t(3), std::size_t(7),
|
|
std::size_t(64), std::size_t(257)}) {
|
|
std::vector<std::vector<std::uint8_t>> owned;
|
|
owned.reserve(batch);
|
|
for (std::size_t i = 0; i < batch; ++i) owned.push_back(rng.bytes(sizes[i % 11]));
|
|
std::vector<Bytes> ins;
|
|
ins.reserve(batch);
|
|
for (const auto& o : owned) ins.emplace_back(o.data(), o.size());
|
|
|
|
const auto got = keccak256_batch(std::span<const Bytes>(ins.data(), ins.size()));
|
|
check(got.size() == batch, "batch size");
|
|
for (std::size_t i = 0; i < batch; ++i) {
|
|
check(got[i] == keccak256(ins[i]),
|
|
"batch " + std::to_string(batch) + " element " + std::to_string(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
void a_batch_of_nothing_but_empty_inputs_still_agrees() {
|
|
const std::vector<std::uint8_t> empty;
|
|
for (std::size_t batch : {std::size_t(1), std::size_t(8), std::size_t(100)}) {
|
|
std::vector<Bytes> ins(batch, Bytes(empty.data(), empty.size()));
|
|
const auto span = std::span<const Bytes>(ins.data(), ins.size());
|
|
check(keccak256_batch(span) == cpu::keccak256_batch(span),
|
|
"empty-input batch of " + std::to_string(batch));
|
|
}
|
|
check(keccak256_batch(std::span<const Bytes>()).empty(), "a batch of nothing is nothing");
|
|
}
|
|
|
|
// ---- the fold ---------------------------------------------------------------
|
|
|
|
// The fold written the obvious way, with no batching and no dispatch. It is the
|
|
// thing the seam's version has to keep agreeing with.
|
|
Digest scalar_root(const std::vector<Digest>& leaves) {
|
|
if (leaves.empty()) return empty_root();
|
|
std::vector<Digest> level;
|
|
level.reserve(leaves.size());
|
|
for (const auto& d : leaves) level.push_back(leaf_hash(d));
|
|
while (level.size() > 1) {
|
|
const std::size_t cnt = level.size();
|
|
const std::size_t parents = (cnt + 1) / 2;
|
|
std::vector<Digest> next(parents);
|
|
for (std::size_t j = 0; j < cnt / 2; ++j) next[j] = node_hash(level[2 * j], level[2 * j + 1]);
|
|
if (cnt % 2 == 1) next[parents - 1] = level[cnt - 1];
|
|
level = next;
|
|
}
|
|
return level[0];
|
|
}
|
|
|
|
void the_batched_fold_equals_the_scalar_fold_at_every_size() {
|
|
Rng rng{0xf01d};
|
|
for (std::size_t n : {std::size_t(0), std::size_t(1), std::size_t(2), std::size_t(3),
|
|
std::size_t(5), std::size_t(8), std::size_t(17), std::size_t(64),
|
|
std::size_t(129), std::size_t(1000)}) {
|
|
std::vector<Digest> leaves(n);
|
|
for (auto& d : leaves) {
|
|
const auto b = rng.bytes(32);
|
|
std::copy(b.begin(), b.end(), d.begin());
|
|
}
|
|
check(merkle_root(leaves) == scalar_root(leaves), "merkle_root n=" + std::to_string(n));
|
|
}
|
|
}
|
|
|
|
void a_single_leaf_root_is_the_tagged_leaf_itself() {
|
|
Digest d{};
|
|
d.fill(1);
|
|
check(merkle_root({d}) == leaf_hash(d), "a single leaf");
|
|
}
|
|
|
|
void an_odd_level_promotes_the_last_node_unchanged() {
|
|
std::vector<Digest> d(3);
|
|
for (std::uint8_t i = 0; i < 3; ++i) d[i].fill(i);
|
|
const Digest want = node_hash(node_hash(leaf_hash(d[0]), leaf_hash(d[1])), leaf_hash(d[2]));
|
|
check(merkle_root(d) == want, "lone-right promotion");
|
|
}
|
|
|
|
// ---- recovery ---------------------------------------------------------------
|
|
|
|
void a_signature_that_is_not_one_recovers_nothing() {
|
|
Digest h{};
|
|
h.fill(7);
|
|
Signature sig{};
|
|
sig.fill(0);
|
|
// r = s = 0 is not a signature; and v = 4 is not one of the four recovery
|
|
// ids the curve defines.
|
|
check(!recover(h, sig).has_value(), "zero signature recovers nothing");
|
|
sig[64] = 4;
|
|
check(!recover(h, sig).has_value(), "recovery id 4 recovers nothing");
|
|
}
|
|
|
|
// A real signature, and the same bytes with the recovery id moved to 2.
|
|
//
|
|
// The vector was produced by k256 — the curve the Rust chains recover with —
|
|
// over hash = 0x07 repeated, key = 0x11 repeated.
|
|
constexpr const char* kKatHash = "0707070707070707070707070707070707070707070707070707070707070707";
|
|
constexpr const char* kKatSig =
|
|
"111f20b9521ba1924ecfb91595426246b152cc1187e83f798cbd61f95f2c4cb1"
|
|
"07da8d209539506429d1ecd4033b2c207b89267f7dd8674421737193cd84f1dc"
|
|
"00";
|
|
constexpr const char* kKatKey = "034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa";
|
|
|
|
std::vector<std::uint8_t> unhex(const char* s) {
|
|
std::vector<std::uint8_t> v;
|
|
for (const char* p = s; *p && p[1]; p += 2) {
|
|
auto d = [](char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; };
|
|
v.push_back(std::uint8_t(d(p[0]) * 16 + d(p[1])));
|
|
}
|
|
return v;
|
|
}
|
|
|
|
void the_recovery_ids_this_curve_cannot_do_are_refused_not_faked() {
|
|
Digest h{};
|
|
const auto hb = unhex(kKatHash);
|
|
std::copy(hb.begin(), hb.end(), h.begin());
|
|
Signature sig{};
|
|
const auto sb = unhex(kKatSig);
|
|
std::copy(sb.begin(), sb.end(), sig.begin());
|
|
|
|
// Recovery id 0, which this curve can do: the key comes back.
|
|
const auto want = unhex(kKatKey);
|
|
const auto got = recover(h, sig);
|
|
check(got.has_value(), "the KAT signature recovers");
|
|
if (got) {
|
|
check(std::equal(got->begin(), got->end(), want.begin()), "the KAT recovers ITS key");
|
|
}
|
|
|
|
// The SAME r and s with the recovery id moved to 2, which means R.x = r + n
|
|
// — a point this curve refuses at parse time. Go accepts ids 0..3 and
|
|
// recovers a DIFFERENT key for 2 (which then fails to match any owner);
|
|
// masking v & 1 here, as chains/cpp/xvm used to, recovered the id-0 key
|
|
// instead and let a C++ node admit a transaction Go and Rust both refuse.
|
|
// Refusing is the fail-closed half of that disagreement — see gpu.hpp.
|
|
Signature moved = sig;
|
|
moved[64] = 2;
|
|
check(!recover(h, moved).has_value(), "recovery id 2 is refused, not masked to 0");
|
|
moved[64] = 3;
|
|
check(!recover(h, moved).has_value(), "recovery id 3 is refused, not masked to 1");
|
|
}
|
|
|
|
// ---- the comparison LUX_GPU=verify makes ------------------------------------
|
|
|
|
void the_verify_comparison_can_fail() {
|
|
const auto a = str("a");
|
|
const auto bb = str("bb");
|
|
const Bytes ins[2] = {view(a), view(bb)};
|
|
const auto want = cpu::keccak256_batch(std::span<const Bytes>(ins, 2));
|
|
|
|
check(internal::same(want, want), "verify accepts two identical answers");
|
|
|
|
auto got = want;
|
|
got[1][31] ^= 1;
|
|
check(!internal::same(got, want), "verify refuses two different answers");
|
|
|
|
std::vector<Digest> shorter(want.begin(), want.begin() + 1);
|
|
check(!internal::same(shorter, want), "verify refuses an answer of the wrong length");
|
|
}
|
|
|
|
// ---- CPU against plugin ------------------------------------------------------
|
|
|
|
void both_backends_give_the_same_answer() {
|
|
if (!plugin_present()) {
|
|
std::fprintf(stderr,
|
|
"skipped: no kernel library installed (backend=%s). "
|
|
"Set LUX_GPU_LIB to run the differential.\n",
|
|
backend().c_str());
|
|
return;
|
|
}
|
|
std::fprintf(stderr, "differential against %s\n", backend().c_str());
|
|
|
|
Rng rng{0xd1ff};
|
|
const std::size_t sizes[] = {0, 1, 31, 32, 135, 136, 137, 271, 272, 1024, 4096};
|
|
for (std::size_t batch : {std::size_t(1), std::size_t(2), std::size_t(3), std::size_t(7),
|
|
std::size_t(64), std::size_t(257)}) {
|
|
std::vector<std::vector<std::uint8_t>> owned;
|
|
for (std::size_t i = 0; i < batch; ++i) owned.push_back(rng.bytes(sizes[i % 11]));
|
|
std::vector<Bytes> ins;
|
|
for (const auto& o : owned) ins.emplace_back(o.data(), o.size());
|
|
const auto span = std::span<const Bytes>(ins.data(), ins.size());
|
|
|
|
const auto want = cpu::keccak256_batch(span);
|
|
const auto got = keccak256_batch(span);
|
|
check(got == want, "cpu vs plugin, batch of " + std::to_string(batch));
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
the_hashes_are_the_ones_the_chain_is_defined_over();
|
|
a_batch_answers_exactly_what_the_singles_answer();
|
|
a_batch_of_nothing_but_empty_inputs_still_agrees();
|
|
the_batched_fold_equals_the_scalar_fold_at_every_size();
|
|
a_single_leaf_root_is_the_tagged_leaf_itself();
|
|
an_odd_level_promotes_the_last_node_unchanged();
|
|
a_signature_that_is_not_one_recovers_nothing();
|
|
the_recovery_ids_this_curve_cannot_do_are_refused_not_faked();
|
|
the_verify_comparison_can_fail();
|
|
both_backends_give_the_same_answer();
|
|
|
|
std::fprintf(stderr, "backend=%s %d checks, %d failed\n", backend().c_str(), checks, failures);
|
|
return failures == 0 ? 0 : 1;
|
|
}
|