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’ve probably heard of Proof-of-Work, especially in the cryptocurrency world. But if not, let’s break it down with a simple, albeit not entirely comprehensive, explanation:
Proof-of-Work (PoW) is when your computer solves cryptographic puzzles to prove that it can. Why go through all that trouble, you ask? Imagine a burglar who needs to pick 15 locks on your neighbor's door, but only 2 on yours. Which house do you think he’ll target? This oversimplified example illustrates the point: PoW requires significant (and often unnecessary) effort, which in turn weeds out the uncommitted, ensuring integrity. This is what secures the blockchain and gives CAPTCHAs that lovely "prove you’re human" vibe.
Though we can't directly compare this to the power-hungry, C|GPU-crunching mining rigs, it's enough to get the gist of how PoW works.
1. Hashing in JavaScript
To build our own PoW function, we need a fast and unpredictable hashing algorithm that works in modern JavaScript environments. You might have thought of the popular crypto-js
package, but today we’re going NATIVE with SubtleCrypto!
The crypto.subtle.digest
function provides us with access to cryptographic hashing algorithms like SHA-1 through SHA-512 directly in the browser. No dependencies required. Let’s dive into 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);
});
In this function, we turn a string into a UTF-8 byte array, pass it to the crypto.subtle.digest
function, and specify the algorithm (SHA-512
). We then convert the resulting hash digest into a hexadecimal string. Magic ( you can find a similar example on MDN ).
Now, we’re ready to hash, but... how do we do Proof-of-Work?
2. Basic Proof-of-Work function
The most basic PoW puzzle I can think of is one that calculates a specific number of zeros (or any other hex character) at the beginning (or end) of the hash digest. Sound complex? Nah! We just need some (random) data, a nonce, and the sha512
function we made earlier.
Here’s the process
function, which accepts two arguments: some random data and a difficulty value (default: 5). At this difficulty, it should take about 10,000 to 50,000 ms on a regular computer:
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;
}
Now, let's execute it with a test run:
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.
So, it works... but here’s the catch: when running this, the whole browser freezes while PoW runs. This is a terrible user experience, especially on older devices. So, how do we keep things smooth while still crunching those numbers?
3. Using the WebWorker API
Thankfully, JavaScript offers a handy solution to move long-running tasks to the background via the WebWorker API. Normally, you’d have to separate your task logic into an external file, but thanks to StackOverflow, we can avoid that and do things inline.
Let’s modify the process()
function to run inside a WebWorker:
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);
});
}
In this updated function, we’re moving the actual PoW logic into a WebWorker, using URL.createObjectURL
to create a URL for an inline Blob (the WebWorker script). We then post the data and difficulty to the worker and listen for messages with the result.
Now, the creepy part: here’s the actual WebWorker code:
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 can see, the sha512()
function is now inside the WebWorker, and it does its thing without blocking the UI thread.
Here is a working jsfiddle example
4. Conclusion
By leveraging SubtleCrypto for hashing, WebWorkers for background processing, and a bit of JavaScript wizardry, we’ve created a minimalist Proof-of-Work function. Sure, you could extend and improve this code in many ways, but this simple example shows how far JavaScript has come - it's no longer just for DOM manipulation using jQuery, Prototype.JS or MooTools (weird, I know).
Now, JavaScript is a powerful, multi-faceted language, even if parts of it are a bit more verbose compared to Python or PHP (which, let’s be honest, is a bit ironic considering JavaScript was originally created to be as simple as possible).
Thanks for reading. Don’t forget to hash responsibly, and may your nonce always find a way!
Top comments (0)