0

I am working on a project and as a part of it, I have to roughly simulate the Bitcoin Proof of Work computation. This involves iteratively computing SHA256 twice on a concatenation of a fixed "BlockHash" string and a 32-bit int nonce which is incremented every iteration. If the computed hash is less than a "TargetHash" string, we break the loop and print the nonce value.

I am trying to compare two sequential implementations, one written using C++ using OpenSSL's SHA256 implementation, and the other in Java using JDK's internal SHA256 implementation. I was expecting the OpenSSL implementation to be much faster than JDK, but the opposite is happening.

Here is my Java code:

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public class SHA256 {
    /**
     * convert byte[] to hex string
     *
     * @param hash
     * @return hex string
     */
    private static String bytesToHex(byte[] hash) {
        StringBuffer hexString = new StringBuffer();
        for (int i = 0; i < hash.length; i++) {
            String hex = Integer.toHexString(0xff & hash[i]);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    /**
     * get a sha256 of the input string
     *
     * @param inputString
     * @return resulting hash in hex string
     */
    public static String SHA256(String inputString) {
        try {
            MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
            return bytesToHex(sha256.digest(inputString.getBytes(StandardCharsets.UTF_8)));
        } catch (NoSuchAlgorithmException ex) {
            System.err.println(ex.toString());
            return null;
        }
    }

    public static void main(String[] args){
        String blockHash = SHA256("Some random string to generate a block hash.");
        System.out.println("blockHash: " + blockHash);
        String targetHash = "000000938023b712892a41e8438e3ff2242a68747105de0395826f60b38d88dc";
        String tmp_hash="undefined";
        int nonce = 0;
        for(nonce=Integer.MIN_VALUE; nonce<=Integer.MAX_VALUE; nonce++) {
            tmp_hash = SHA256(SHA256(blockHash+String.valueOf(nonce)));
            if(targetHash.compareTo(tmp_hash)>0)
                break;
        }
        System.out.println("Resulting Hash: " + tmp_hash);
        System.out.println("Nonce:" + nonce);
    }
}

And this is my C++ implementation:

#include <iostream>
#include <climits>
#include <cstring>
#include <sstream>
#include <string>
#include <iomanip>
#include "format.h"

using namespace std;

#include <openssl/sha.h>

string sha256(const string str)
{
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256_CTX sha256;
    SHA256_Init(&sha256);
    SHA256_Update(&sha256, str.c_str(), str.size());
    SHA256_Final(hash, &sha256);
    stringstream ss;
    for(int i = 0; i < SHA256_DIGEST_LENGTH; i++)
    {
        ss << hex << setw(2) << setfill('0') << (int)hash[i];
    }
    return ss.str();
}


int main(int argc, char *argv[])
{   
    string input = "Some random string to generate a block hash.";
    string blockHash = sha256(input);
    cout << "blockHash: " << blockHash << endl;
    string targetHash = "000000938023b712892a41e8438e3ff2242a68747105de0395826f60b38d88dc";
    string tmp_hash="undefined";
    int nonce = 0;

    for(nonce = INT_MIN; nonce <= INT_MAX; nonce++){
        tmp_hash = sha256(sha256(fmt::format("{}{}", blockHash, nonce)));
        if(strcmp(tmp_hash.c_str(), targetHash.c_str()) < 0)
            break;
    }

    cout<<"Resulting Hash: "<<tmp_hash<<endl;
    cout<<"Nonce: "<<nonce<<endl;

    return 0;
}

The outputs using linux 'time' utility to measure runtime:

javac SHA256.java
time java SHA256
blockHash: 596143a6a70a23c86e4b218afeb05d151ed45a39e96368e213d17e0a491d894a
Resulting Hash: 0000008ce61c628ffb00b6668687504fd5d44da0a57adb40d6ff59f8e4af0a4a
Nonce:-2135751361

real    0m22.258s
user    0m22.977s
sys 0m0.097s

g++ -O2 -DFMT_HEADER_ONLY main.cpp -lcrypto -lssl
time ./a.out
blockHash: 596143a6a70a23c86e4b218afeb05d151ed45a39e96368e213d17e0a491d894a
Resulting Hash: 0000008ce61c628ffb00b6668687504fd5d44da0a57adb40d6ff59f8e4af0a4a
Nonce: -2135751361

real    0m35.703s
user    0m35.693s
sys 0m0.005s

This is just for an easy TargetHash, for more difficult ones, the difference is even greater. I am pretty sure here openssl sha256 implementation isn't a bottleneck and something else is, but being new to C++ I am not sure what. I was earlier using to_string(nonce) and s1.compare(s2), which I replaced with fmt::format and strcmp because they're faster, but still could only gain a few seconds. Any ideas will be really appreciated.

aayushARM
  • 41
  • 1
  • 10
  • Why not use `0000008ce61c628ffb00b6668687504fd5d44da0a57adb40d6ff59f8e4af0a4a` as `targetHash` and compare for equality (in both programs)? – Ted Lyngmo Oct 29 '19 at 19:08
  • I'm not sure why I'd do that. Considering that both implementations sequentially increment the nonce, the faster code should reach the resulting hash earlier, so benchmarking shouldn't be affected irrespective of whether I use targetHash or resultant hash. – aayushARM Oct 29 '19 at 21:26
  • Just because it is strange to do `<` or `>` on hashes. It doesn't make sense. They are either equal or not. Btw `if(targetHash == tmp_hash)` or `if(tmp_hash < targetHash)`, instead of doing `if(strcmp(tmp_hash.c_str(), targetHash.c_str()) < 0)` is the C++ way of comparing `std::string`s. – Ted Lyngmo Oct 29 '19 at 21:39

1 Answers1

1

The bottleneck for your c++ code is your custom bytes_to_string function. Calling stringstream functions in a loop simply hits the performance.

You might want to look at this answer to another question.


replace the stringstream functions with the following code snippet. It is faster because it manipulates the string memory directly.

static const char characters[] = "0123456789ABCDEF";
std::string result (SHA256_DIGEST_LENGTH * 2, ' ');
for(int i = 0; i < SHA256_DIGEST_LENGTH; i++)
{
    result[2*i] = characters[(unsigned int) hash[i] >> 4];
    result[2*i+1] = characters[(unsigned int) hash[i] & 0x0F];
}
return result;
Bruce Shen
  • 572
  • 3
  • 13
  • I would like to know why this answer was downvoted. – eike Oct 29 '19 at 18:41
  • @eike the answer was edited twice, the original being a bit terse. That might've invited the downvote from someone. – Kayaman Oct 29 '19 at 18:57
  • With this change, the C++ program is ready in 1/3 of the time it takes for the java program. A pretty good improvement I would say. There are more improvements to be done, but this should be the most important one. – Ted Lyngmo Oct 29 '19 at 19:10
  • 1
    I wonder how `fmt::format("{}{}", blockHash, nonce)` compares to `blockHash + std::to_string(nonce)` when it comes to speed. – Ted Lyngmo Oct 29 '19 at 19:30
  • @Bruce Shen That was it! Replacing stringstream code with above code makes it run under 10 seconds instead of 35. Thank you :) Do you think there can be any other optimizations done on it? – aayushARM Oct 29 '19 at 19:31
  • 1
    @aayushARM It depends on what you want to achieve. Now you are comparing based on string value. You can actually compare the raw data directly, by casting the hash value to uint64_t array or uint32_t array depending on 64 or 32-bit system. But this is different from what Java code did. So the speedup ratio is not relevant anymore. – Bruce Shen Oct 29 '19 at 19:48
  • @Bruce Shen Okay, that makes sense. I accepted this as the answer. – aayushARM Oct 29 '19 at 21:21