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.