From dd45fed7741f5b2e6550229d9bc3463a70069743 Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 14 Dec 2023 10:55:16 -0800 Subject: [PATCH] Add Ramhash C++ implementation This is the c/ subdirectory of https://gitlab.com/accumulatenetwork/ecosystem/LXRMining as of commit f32a124b70014b7bd5a967fb22d39b7ee333058a with no modifications. --- src/Ramhash/.gitignore | 32 +++++ src/Ramhash/include/lxrhash.h | 24 ++++ src/Ramhash/src/lxrhash.cpp | 233 ++++++++++++++++++++++++++++++++++ src/Ramhash/src/main.cpp | 68 ++++++++++ 4 files changed, 357 insertions(+) create mode 100644 src/Ramhash/.gitignore create mode 100644 src/Ramhash/include/lxrhash.h create mode 100644 src/Ramhash/src/lxrhash.cpp create mode 100644 src/Ramhash/src/main.cpp diff --git a/src/Ramhash/.gitignore b/src/Ramhash/.gitignore new file mode 100644 index 000000000..d99efa91a --- /dev/null +++ b/src/Ramhash/.gitignore @@ -0,0 +1,32 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app \ No newline at end of file diff --git a/src/Ramhash/include/lxrhash.h b/src/Ramhash/include/lxrhash.h new file mode 100644 index 000000000..3f71c1c2c --- /dev/null +++ b/src/Ramhash/include/lxrhash.h @@ -0,0 +1,24 @@ +// Copyright 2023 The Accumulate Authors +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +unsigned long long const Size = 0x40000000; // Size of the BitMap. Should be 1 GB (0x40000000) for production +int const Passes = 6; // +int const Loops = 16; // How many times the hash is looped +unsigned long long const checksum = 0xed0536be7833ca15; // Checksum of byteMap to ensure we don't use a corrupted file + +// Lookup XoR Hash (Ram Hash) +class LXRHash +{ + unsigned char byteMap[Size]; // 1 GB is 30 bits, 2^30, or hex 4 with 7 zeros + unsigned long long MapMask = Size - 1; // MapMask is byteMap length -1 +public: // + unsigned long long mix(char unsigned hash[32], unsigned long long nonce); // Mixes the hash with the nonce prior to + unsigned long long LxrPoW(char unsigned hash[32], unsigned long long nonce); // Returns the PoW for a hash + nonce + void init(); // Init the byteMap; do it only once. +private: // + bool load(); // Load the byteMap.dat file + unsigned long long check(); // Quick check of correct byteMap +}; diff --git a/src/Ramhash/src/lxrhash.cpp b/src/Ramhash/src/lxrhash.cpp new file mode 100644 index 000000000..bf26f4789 --- /dev/null +++ b/src/Ramhash/src/lxrhash.cpp @@ -0,0 +1,233 @@ +// Copyright 2023 The Accumulate Authors +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +#include +#include +#include +#include +#include +#include +#include "../include/lxrhash.h" + +// LXRHash uses a combination of shifts and exclusive or's to combine inputs and states to +// create a hash with strong avalanche properties. This means that if any one bit is changed +// in the inputs, on average half of the bits of the output change. This quality makes an +// algorithmic attack on mining very, very unlikely. +// +// First, the hash and nonce are "mixed" such that a reasonable avalanche property is obtained. +// +// Secondly, the resulting 48 bytes are mapped through a 1 GB byteMap where random accesses have +// an even chance of producing a value between 0-255. Cycling through lookups from random bytes in +// the 1 GB space produces a resulting value to be graded. +// +// The Shift XoR Random Number Generator (RNG) was invented by George Marsaglia and published +// in 2003. Please refer to the readme on the LXRHashing repository. +// https://gitlab.com/accumulatenetwork/ecosystem/LXRMining/-/blob/main/README.md +// +// * No elements of the triplets are zero +// * All elements are relatively prime to each other +// * All elements Are small relative to an unsigned long long (64) +// * The right shift generally smaller than the left shifts +// +// In my testing, and according to Marsaglia, no elements of these triplets fitting this criteria +// perform better or worse. Adding constants and other outside elements help the distribution of +// values. +// +// Further, two different sponge states are used, mix() and LXRPoW() +// +// mix() +// Combines the 32 byte hash with the unsigned long long nonce. This is done as rapidly as possible +// with an avalanche property only enough to ensure repeated hashes with minimal bit changes to +// nonces do not gain an advantage during mining by tracing the same paths through the byteMap, +// making better use of the cache. +// +// LXRPoW() +// Accepts a 32 byte hash with the unsigned long long nonce. The sponge is initialized using mix(). +// Then Each byte is translated using a sequenced Pseudo random sequenced index into the +// 1 GB byteMap. This process is repeated for a number of loops, building up a sponge value. +// More loops makes the hash slower and more dependent on the random access speed to the +// 1 GB byteMap. As processors grow in speed, and chips accommodate greater caches, the +// byteMap and the loop number increased. +// +// mix() is then used again to "squeeze out" the hash, where the high 8 bytes represent the +// possible PoW solution. + +// check() +// Quick checksum, with a bit of bit spreading to make the checksum stronger +unsigned long long LXRHash::check() +{ + unsigned long long state = 0; + for (int i = 0; i < Size; i += 8) + { + unsigned long long a = 0; // Pull a long Big Endian value from byteMap + for (int j = 0; j < 8; j++) + { + a <<= 8; + a ^= (unsigned long long)(byteMap[i + j]); + } + state ^= state << 23 ^ a << 11; // Randomize state, fold in the long from byteMap + state ^= state >> 3; // in the byteMap, running them through a + state ^= state << 17; // Marsaglia Xorshift RNG + } + return state; // Return resulting state as the checksum +} + +// init() +// initializes the 1 GB byteMap +// Please note that I just wrote cheap code to cache the byteMap. Since it takes about 5 minutes +// or more to generate the byteMap, this code looks for a byteMap.dat file and loads it if it +// exists. +void LXRHash::init() +{ + if (LXRHash::load()) + { + return; + } + + printf("\nbyteMap.dat file not found. Building byteMap.dat file is slow. This takes six passes:\n"); + // Initialize the byteMap with values from 0 to 255 repeated for the whole array + for (unsigned long long i = 0; i < Size; i++) + { + byteMap[i] = i; + } + printf("start cs %llx\n", check()); + unsigned long long offset = 0x7a43b26c611e135f; // Arbitrary 64 bit initial offset (no zero bytes) + unsigned long long cnt = 0; // Marsaglia found a counter improves Xorshift + for (unsigned long long i = 0; i < Passes; i++) // Passes. 2 passes at least are needed. + { // + for (unsigned long long j = 0; j < Size; j++) // Shuffle the byteMap by swapping every byte with + { // another randomly chosen byte. + offset ^= offset << 13 ^ cnt; // More classical Marsaglia Xorshift RNG + offset ^= offset >> 23; // + offset ^= offset << 5; // + unsigned long long r = offset & MapMask; // Mask r to an index within the byteMap + unsigned char a = byteMap[r]; // Get the random byte + byteMap[r] = byteMap[j]; // Put the ith value there + byteMap[j] = a; // Put the random byte at the ith position + cnt += a; // Walk cnt forward by a, (0-255) + if (j % 200000000 == 0) // Give feedback + { // + printf("."); // A period every 200M bytes swapped + fflush(stdout); // flush so user can see + } // + } // + printf("%llu", i + 1); // Count the passes + fflush(stdout); + } + printf("\n"); + + printf("Checksum: %llx\n", check()); // Print the file checksum + + std::fstream byteMapFile; + byteMapFile = std::fstream("byteMap.dat", std::ios::out | std::ios::binary); + byteMapFile.write((char *)(byteMap), Size); + byteMapFile.close(); +} + +bool LXRHash::load() +{ + // open the file: + std::streampos fileSize; + std::ifstream file("byteMap.dat", std::ios::binary); + + // get its size: + file.seekg(0, std::ios::end); // Seek to the end + fileSize = file.tellg(); // get the file size + if (fileSize != Size) // if not what we expect (exactly the length of byteMap) + { // + return (false); // then return false (to generate the right file) + } // + file.seekg(0, std::ios::beg); // Seek to the start of the file, + file.read((char *)(byteMap), Size); // and read the full byteMap + file.close(); // + unsigned long long cs = check(); // + if (check() != checksum) // Then check that the checksum is correct + { // + printf("checksum mismatch l:%llx != cs:%llx\n", cs, checksum); + std::filesystem::remove("byteMap.dat"); // Remove the bad file + return (false); // Return fail + } // + return true; // If so, all good, don't need to regenerate the byteMap +} + +unsigned long long LXRHash::LxrPoW(char unsigned hash[32], unsigned long long nonce) +{ + // Mix the hash with the nonce to ensure the path through the byteMap is unique even if one bit + unsigned long long state = LXRHash::mix(hash, nonce); // is changed from another submission. + + // Make the specified "loops" through the hash. The initial state is returned by Mix, + // bringing the total number of bits involved in the "sponge" to 48. Increasing the + // loops would slow down the hash, giving this mechanism an easy way to keep difficulty + // within the limits of the total hash power in a protocol + for (int i = 0; i < Loops; i++) // Make our loops over the + { // entire hash + for (int j = 0; j < 32; j++) // Process each byte + { // + unsigned long long b, v; // Do all math in unsigned long long + b = byteMap[state & MapMask]; // Pull a byte from the ByteMap + v = hash[j]; // Get a byte from the hash + state ^= state << 17 ^ b << 31; // A modified XorShift RNG + state ^= state >> 7 ^ v << 23; // Fold in a random byte from + state ^= state << 5; // the byteMap and the byte + hash[j] = state; // from the hash. + } // Then update the hash byte. + } + state = LXRHash::mix(hash, state); // Do a final mix() using state + return state; // as the nonce and return the state. +} + +// mix() +// Mix spreads the bits of the hash and nonce throughout the hash and a "state" +unsigned long long LXRHash::mix(char unsigned hash[32], unsigned long long nonce) +{ + unsigned long long state = 0xb32c25d16e362f32; // Set the state to an arbitrary unsigned long long with bits + nonce ^= nonce << 19 ^ state; // Run the nonce through a Xorshift + nonce ^= nonce >> 1; + nonce ^= nonce << 3; + + unsigned long long array[5]; // Array of longs, the nonce, and 4 longs holding hash + array[0] = nonce; // this array along with the state is the sponge for the LXRHash + for (int i = 1; i < 5; i++) // Walk through the array + { // + array[i] = 0; // Clear the array (not really needed, but it's cleaner) + for (int j = 0; j < 8; j++) // Get 8 bytes of the hash as array entry + { // + array[i] <<= 8; // Shift the array to the left a byte + array[i] |= hash[((i - 1) << 3) + j]; // Or in the next byte + } // + state ^= state << 7 ^ array[i]; // shift state combine the whole hash into + state ^= state >> 3; // the state while randomizing the state + state ^= state << 5; // through a Xorshift RNG. + } + + // mix what is effectively sponge of 5 elements and a state + for (int i = 0; i < 5; i++) + { + unsigned long long a, left1, right, left2; + a = array[i]; // Get each element of the array + left1 = (a & 0x1F) | 1; // Use the element to pick 3 random shifts for the Xorshift + right = ((a & 0xF0) >> 5) | 1; // Note that the right is smaller than the left + left2 = (((a & 0x1F0000) >> 17) << 1) ^ left1; // And xor-ing left1 with left2 ensures they are different + state ^= state << left1 ^ a << 5; // Randomize the state and array element + state ^= state >> right; // using the random shifts + state ^= state << left2; // + array[i] ^= state; // apply to the array + } + + // Extract the 32 byte hash into the *hash passed in. + for (int i = 0; i < 4; i++) + { + unsigned long long a = array[i] ^ state; + for (int j = 0; j < 8; j++) // for 8 bytes + { // Put each byte into hash, Big Endian order + hash[i * 8 + j] = a >> ((7 - j) * 8); // i indexes 8 byte group, j each byte + } // + state ^= state << 27 ^ i << 19; // Mix the state a bit more, fold in the counter + state ^= state >> 11; // + state ^= state << 13; // + } + return state; +} diff --git a/src/Ramhash/src/main.cpp b/src/Ramhash/src/main.cpp new file mode 100644 index 000000000..e052a5fa2 --- /dev/null +++ b/src/Ramhash/src/main.cpp @@ -0,0 +1,68 @@ +// Copyright 2023 The Accumulate Authors +// +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +#include +#include +#include +#include +#include +#include "../include/lxrhash.h" + +LXRHash lxrHash; + +int main() +{ + printf("Load byteMap\n"); + lxrHash.init(); // init() loads/computes byteMap + printf("The byteMap is loaded, mining begins!\n"); // + printf("%13s %13s %10s %16s %15s\n", // + "total(s)", "block(s)", "nonce", "PoW", "hashes/sec"); // + unsigned char hash[32]; // Start with a hash of all zeros + std::fill_n(hash, 32, 0); // + unsigned long long hashCnt = 0; // Count hashes to compute hash/second (hps) + unsigned long long ms = 0; // Set the current time passed to zero + for (int i = 0; i < 1000; i++) // + { // + printf("Now Mining: "); // Start mining the new hash + for (int i = 0; i < 32; i++) // Print the new hash in hex + { // + printf("%02x", hash[i]); // + } // + printf("\n"); // + auto last = std::chrono::system_clock::now(); // time + unsigned long long ms1 = 0; // block time + unsigned long long min = 0xFFFFFFFFFFFFFFFF; // set min to max unsigned long long value + bool done = false; // Done is set when a block difficulty is found + for (unsigned long long nonce = 0; !done; nonce++, hashCnt++) // Loop until done, count nonces and hashes + { // + unsigned char tmpHash[32]; // Copy hash since LxrPoW will overwrite + std::copy(hash, hash + 32, tmpHash); // + unsigned long long v = lxrHash.LxrPoW(tmpHash, nonce); // Get the LxrPow for current nonce + if (v < min && v < (unsigned long long)(1) << 52) // Print new solutions < some minimum difficulty + { // + min = v; // Set as the new highest solution found + auto end = std::chrono::system_clock::now(); // Calculate time stamps + unsigned long long ms2 = // + std::chrono::duration_cast(end - last).count(); + ms1 += ms2; // Add to the time in this block + ms += ms2; // Add to total time spent + last = std::chrono::system_clock::now(); // Reset timer for this block + printf("%13.3f %13.3f %10llu %016llx %15.3f \n", // Print out the new solution + float(ms) / 1000, // Total time + float(ms1) / 1000, // Time mining this block + nonce, // The nonce solution found + v, // The mining value using the nonce + float(hashCnt) / (float(ms) / 1000 + 1)); // Total hashes per second in this test + } // + done = v < (unsigned long long)(1) << 45; // Check against the difficulty for EOB + if (done) // If at EOB + { // then set up a new block to mine + std::copy(tmpHash, tmpHash + 32, hash); // Use the tmpHash as a new hash! + } + } + } + return 0; +}