PoW (proof-of-work) is a pretty terrifying way to prove someone that you are really serious, by wasting raw CPU cycles doing pointless cryptographic computations.
You're probably already familiar with proof-of-work, or similar techniques, or at least have already heard about PoW when it comes to cryptocurrencies. If you didn't, let's stay with the following really basic - not quite complete - explanation:
Proof-of-Work (PoW) is a mechanism on which your computer is solving cryptographic puzzles to simple proof, that it is able to do it. Well, what's the point, you may ask? Imagine a burglar: He has to pick 15 locks on your neighbor's front door, but only 2 on yours. Which way do you think he will go? This is perhaps a crude example, but basically PoW and similar techniques are intended to separate the serious and "real" people from the rest, by requiring far more (technical) effort than the possible benefit. That's how the integrity of the blockchain is ensured, some security and protection libraries (such as CAPTCHAs) are using such techniques as well.
Of course, you cannot compare the following example with the pow technique of your favourite C|GPU generated money provider, but it simple shows how it could theoretically work.
1. Hashing in JavaScript
To create our own PoW function we first need a fast, unpredictable hashing algorithm which can be used in the JavaScript environment of at least a more modern browser. I'm pretty sure some readers have already thought of some crypto-js package available on npm, but nope, we're going NATIVE with SubtleCrypto!
The crypto.subtle.digest
function allows us to use SHA-1 to SHA-512 cryptographic hash functions from within the browser without relying on any external dependency. Let's try it out with a basic example:
function sha512(string) {
return new Promise((resolve, reject) => {
let buffer = (new TextEncoder).encode(string);
crypto.subtle.digest('SHA-512', buffer.buffer).then(result => {
resolve(Array.from(new Uint8Array(result)).map(
c => c.toString(16).padStart(2, '0')
).join(''));
}, reject);
});
}
sha512('somedata').then(result => {
console.log(result); // Output: a053...6f416
}, err => {
console.error(err);
});
Our sha512
function takes our string and turns it into an UTF8 bytes array, which we can use on the already mentioned crypto.subtle.digest
function, as second argument after we declared our desired hashing algorithm (SHA-512
). Finally, we turn the bytes array, holding our hash digest result, into a hex-string (you can find a similar example on MDN).
Perfect, we're now able to hash, but how do we pow?
2. Basic Proof-of-Work function
The most basic cryptographic puzzle I can think of, and many other tutorials and explanation articles out there as well, is calculating a specific amount of zeros (or any other hex-character) on start (or end) of the hash digest. Sounds complicated? Na, we just need some (random) data and a nonce, as well as our sha512
function above.
Let's create a process
function, which takes 2 arguments: Some random data to be used for our hashing and a difficulty value with a default value of 5, which should already take round about 10.000 - 50.000 ms on most (common) computers.
async function process(data, difficulty = 5) {
let hash;
let nonce = 0;
do {
hash = await sha512(data + nonce++);
} while(hash.substr(0, difficulty) !== Array(difficulty + 1).join('0'));
return hash;
}
Of course, we can further improve and extend this function in many different ways. However, it's enough to demonstrate, so let us execute it with something like:
async function main() {
console.time('pow-test');
let hash = await process('somedata');
console.log(`Result: ${hash}`);
console.timeEnd('pow-test');
}
main();
Here is a working jsfiddle example.
It works, but we've one major issue on many browsers: The website is completely in-responsible while the proof-of-work test runs. That's a horrible user experience, especially for older devices, thus we've to find a way to execute our pow function in the background.
3. Using the WebWorker API
JavaScript provides an easy solution to move long running tasks and scripts in a background thread. Typically, this process requires the main code, which is used for the task itself, to be separated in an external JavaScript file. But, however, stackoverflow already found a way around.
Let's start with our process()
function, it should now initialize and execute the WebWorker instead of the task itself:
function process(data, difficulty = 5) {
return new Promise((resolve, reject) => {
let webWorkerURL = URL.createObjectURL(new Blob([
'(', processTask(), ')()'
], { type: 'application/javascript' }));
// Create WebWorker
let worker = new Worker(webWorkerURL);
worker.onmessage = (event) => {
worker.terminate();
resolve(event.data);
};
worker.onerror = (event) => {
worker.terminate();
reject();
};
// Execute WebWorker Task
worker.postMessage({
data,
difficulty
});
// Destroy URL Object
URL.revokeObjectURL(webWorkerURL);
});
}
We first start "externalizing" our process task, which has now been moved to the processTask()
function as described below, by using the URL.createObjectURL
method. Technically, we create our own page, containing just the desired JavaScript code, and are now able to pass this instead of the usually-used JavaScript file path in the constructor of the Worker
object. Let us now attach the typical event listeners on our own WebWorker instance, and post a message with the random data and set difficulty to start our pow process.
Now, let us take a look at the creepy part: Our processTask()
function returns the "stringified" body of an anonymous function. That's the pure content of our WebWorker script:
function processTask() {
return function () {
function sha512(text) {
return new Promise((resolve, reject) => {
let buffer = (new TextEncoder).encode(text);
crypto.subtle.digest('SHA-512', buffer.buffer).then(result => {
resolve(Array.from(new Uint8Array(result)).map(
c => c.toString(16).padStart(2, '0')
).join(''));
}, reject);
});
}
addEventListener('message', async (event) => {
let data = event.data.data;
let difficulty = event.data.difficulty;
let hash;
let nonce = 0;
do {
hash = await sha512(data + nonce++);
} while(hash.substr(0, difficulty) !== Array(difficulty + 1).join('0'));
postMessage({
hash,
data,
difficulty
});
});
}.toString();
}
As you probably have already noticed, our sha512()
function has found his new home within the WebWorker environment, followed by an event listener which executes our pow code and responds with the calculated hash and passed data and difficulty when done.
Here is a working jsfiddle example
4. Conclusion
Using SubtleCrypto for hashing, WebWorker for multi-threading, URL Blobs, UInt8Arrays, ... JavaScript has turned out into a way more powerful language over the past years, even if some parts are a bit more complicated to write compared with Python or PHP (which is silly with the Fact in mind, that JavaScript was designed to be as easy as possible).
However, we created a minimalist proof-of-work function, using the WebWorker API to run it as a background task. While you should definitively extend and improve the code above, I still hope you could read or learn something useful today.
Thanks for reading.
Top comments (0)