Simple Proof-of-Work in JavaScript (without dependencies)
Tags: javascript, webdev, crypto, pow
Published: 2022-12-13 10:43:18

> 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](https://developer.mozilla.org/en-US/docs/Web/API/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:

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string)).

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. 

```javascript
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:

```javascript
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.](https://jsfiddle.net/sam_rat/60589ezk/)

{% embed https://jsfiddle.net/sam_rat/60589ezk/embed/ %}

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](https://stackoverflow.com/a/19201292) 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:

```javascript
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:

```javascript
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](https://jsfiddle.net/sam_rat/w0zb1ao9/4/)

{% embed https://jsfiddle.net/sam_rat/w0zb1ao9/5/embed/ %}

## 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.