Skip to content

Instantly share code, notes, and snippets.

@jcdickinson
Last active June 11, 2025 17:59
Show Gist options
  • Select an option

  • Save jcdickinson/5a6e158b3217575bf61e to your computer and use it in GitHub Desktop.

Select an option

Save jcdickinson/5a6e158b3217575bf61e to your computer and use it in GitHub Desktop.
Proof-of-work JS

POW?

The idea isn't new. It's a technique that asks the client to perform proof-of-work in order to increase the costs associated with spamming a website. For a single user the workload shouldn't be an issue at all, but a spammer (or indeed a brute-forcing hacker) might run into problems with being able to maintain a high throughput of requests. Validating the proof of work is trivially computational and hence will not put your server under the same amount of stress.

POWJS on my laptop takes 17 seconds to solve the problem in Chrome (50 seconds in IE 11) with 22 bits required to be zero. The workload can be started as soon as the user opens the page and will run asyncronously in the background (in a web worker if the browser supports them).

POWJS uses CryptoJS, but won't pollute your global namespace with it. This can't be used (as-is) to farm bitcoins or what-have-you, it merely performs a random POW.

Usage

Your server will need to generate a challenge and needs to be able to verify that it created a challenge, a trivial example could be an expiring challenge:

validUntil = utcfiletime() + 3600;
token = pack(validUntil, generateRandomBytes(32));
challenge = hmac(token, secretServerPassword);

Or just store a nonce in the user's session. Whatever you do, I recommend that you use HMAC to authenticate the token.

The server might then send something like this to the client (hexadecimal, please):

<input type="hidden" id="pow-token" value="<token>" />
<input type="hidden" id="pow-challenge" value="<challenge>" />
<input type="hidden" id="pow-solution" value="" />

The server can also indicate how many zero-bits it wants by including them in the challenge:

<!-- Expect the first byte to have only zeros. -->
<input type="hidden" id="pow-challenge" value="<challenge>;8" />

On the client POWJS is then used to calculate the proof of work:

$(function() {

	var pow = PowJS.start({
	    js: 'pow.js', // The path to the POWJS file. This is optional, but if omitted web workers will not be used.
	    challenge: $('#pow-challenge').val(), // The challenge from the server.
	    work: 800, // Only used when web workers are unavailable. How many attempts to make per invocation of the solver.
	    delay: 30, // Only used when web workers are unavailable. How many milliseconds between each call to the solver.
	    blurfactor: 30, // If the browser incurs limits on setTimeout for some reason (e.g. tab changed), multiply work by this.
	    complete: function(result) {
	    	if (result < 0) {
	    	  alert("cancelled!");
	    	  return;
	    	}
	    	$('#pow-solution').val(result);
	    	$('#submit-button').prop('disabled', false);
	    }
	});
	
	// Solves the problem drastically faster but risks lagging the web page, use this
	// if the user has reached the submit button and no solution has been found.
	if (pow.canSpeedUp) pow.speedUp();
	
	// For whatever reason.
	pow.cancel();
});

When the form is submitted the server can then validate it:

token = getinput('pow-token');
challenge = hmac(token, secretServerPassword); // Don't trust the challenge from the client!

// Has the token expired?
date = unpackint(token);
if (date < utcfiletime()) return false;

solutionCount = getinput('pow-solution'); //32bit uint
solution = sha1(pack(solutionCount, challenge));

// The solution from the client must have 22 zero bits at the start.
return firstBitsAreZero(22, solution));
/* The MIT License (MIT)
Copyright (c) 2014 Jonathan Dickinson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. */
var PowJS = PowJS || function (undefined) {
/*
CryptoJS v3.1.2
code.google.com/p/crypto-js
(c) 2009-2013 by Jeff Mott. All rights reserved.
code.google.com/p/crypto-js/wiki/License
*/
var CryptoJS = CryptoJS || function (e, m) {
var p = {}, j = p.lib = {}, l = function () { }, f = j.Base = { extend: function (a) { l.prototype = this; var c = new l; a && c.mixIn(a); c.hasOwnProperty("init") || (c.init = function () { c.$super.init.apply(this, arguments) }); c.init.prototype = c; c.$super = this; return c }, create: function () { var a = this.extend(); a.init.apply(a, arguments); return a }, init: function () { }, mixIn: function (a) { for (var c in a) a.hasOwnProperty(c) && (this[c] = a[c]); a.hasOwnProperty("toString") && (this.toString = a.toString) }, clone: function () { return this.init.prototype.extend(this) } },
n = j.WordArray = f.extend({
init: function (a, c) { a = this.words = a || []; this.sigBytes = c != m ? c : 4 * a.length }, toString: function (a) { return (a || h).stringify(this) }, concat: function (a) { var c = this.words, q = a.words, d = this.sigBytes; a = a.sigBytes; this.clamp(); if (d % 4) for (var b = 0; b < a; b++) c[d + b >>> 2] |= (q[b >>> 2] >>> 24 - 8 * (b % 4) & 255) << 24 - 8 * ((d + b) % 4); else if (65535 < q.length) for (b = 0; b < a; b += 4) c[d + b >>> 2] = q[b >>> 2]; else c.push.apply(c, q); this.sigBytes += a; return this }, clamp: function () {
var a = this.words, c = this.sigBytes; a[c >>> 2] &= 4294967295 <<
32 - 8 * (c % 4); a.length = e.ceil(c / 4)
}, clone: function () { var a = f.clone.call(this); a.words = this.words.slice(0); return a }, random: function (a) { for (var c = [], b = 0; b < a; b += 4) c.push(4294967296 * e.random() | 0); return new n.init(c, a) }
}), b = p.enc = {}, h = b.Hex = {
stringify: function (a) { var c = a.words; a = a.sigBytes; for (var b = [], d = 0; d < a; d++) { var f = c[d >>> 2] >>> 24 - 8 * (d % 4) & 255; b.push((f >>> 4).toString(16)); b.push((f & 15).toString(16)) } return b.join("") }, parse: function (a) {
for (var c = a.length, b = [], d = 0; d < c; d += 2) b[d >>> 3] |= parseInt(a.substr(d,
2), 16) << 24 - 4 * (d % 8); return new n.init(b, c / 2)
}
}, g = b.Latin1 = { stringify: function (a) { var c = a.words; a = a.sigBytes; for (var b = [], d = 0; d < a; d++) b.push(String.fromCharCode(c[d >>> 2] >>> 24 - 8 * (d % 4) & 255)); return b.join("") }, parse: function (a) { for (var c = a.length, b = [], d = 0; d < c; d++) b[d >>> 2] |= (a.charCodeAt(d) & 255) << 24 - 8 * (d % 4); return new n.init(b, c) } }, r = b.Utf8 = { stringify: function (a) { try { return decodeURIComponent(escape(g.stringify(a))) } catch (c) { throw Error("Malformed UTF-8 data"); } }, parse: function (a) { return g.parse(unescape(encodeURIComponent(a))) } },
k = j.BufferedBlockAlgorithm = f.extend({
reset: function () { this._data = new n.init; this._nDataBytes = 0 }, _append: function (a) { "string" == typeof a && (a = r.parse(a)); this._data.concat(a); this._nDataBytes += a.sigBytes }, _process: function (a) { var c = this._data, b = c.words, d = c.sigBytes, f = this.blockSize, h = d / (4 * f), h = a ? e.ceil(h) : e.max((h | 0) - this._minBufferSize, 0); a = h * f; d = e.min(4 * a, d); if (a) { for (var g = 0; g < a; g += f) this._doProcessBlock(b, g); g = b.splice(0, a); c.sigBytes -= d } return new n.init(g, d) }, clone: function () {
var a = f.clone.call(this);
a._data = this._data.clone(); return a
}, _minBufferSize: 0
}); j.Hasher = k.extend({
cfg: f.extend(), init: function (a) { this.cfg = this.cfg.extend(a); this.reset() }, reset: function () { k.reset.call(this); this._doReset() }, update: function (a) { this._append(a); this._process(); return this }, finalize: function (a) { a && this._append(a); return this._doFinalize() }, blockSize: 16, _createHelper: function (a) { return function (c, b) { return (new a.init(b)).finalize(c) } }, _createHmacHelper: function (a) {
return function (b, f) {
return (new s.HMAC.init(a,
f)).finalize(b)
}
}
}); var s = p.algo = {}; return p
}(Math);
(function () {
var e = CryptoJS, m = e.lib, p = m.WordArray, j = m.Hasher, l = [], m = e.algo.SHA1 = j.extend({
_doReset: function () { this._hash = new p.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]) }, _doProcessBlock: function (f, n) {
for (var b = this._hash.words, h = b[0], g = b[1], e = b[2], k = b[3], j = b[4], a = 0; 80 > a; a++) {
if (16 > a) l[a] = f[n + a] | 0; else { var c = l[a - 3] ^ l[a - 8] ^ l[a - 14] ^ l[a - 16]; l[a] = c << 1 | c >>> 31 } c = (h << 5 | h >>> 27) + j + l[a]; c = 20 > a ? c + ((g & e | ~g & k) + 1518500249) : 40 > a ? c + ((g ^ e ^ k) + 1859775393) : 60 > a ? c + ((g & e | g & k | e & k) - 1894007588) : c + ((g ^ e ^
k) - 899497514); j = k; k = e; e = g << 30 | g >>> 2; g = h; h = c
} b[0] = b[0] + h | 0; b[1] = b[1] + g | 0; b[2] = b[2] + e | 0; b[3] = b[3] + k | 0; b[4] = b[4] + j | 0
}, _doFinalize: function () { var f = this._data, e = f.words, b = 8 * this._nDataBytes, h = 8 * f.sigBytes; e[h >>> 5] |= 128 << 24 - h % 32; e[(h + 64 >>> 9 << 4) + 14] = Math.floor(b / 4294967296); e[(h + 64 >>> 9 << 4) + 15] = b; f.sigBytes = 4 * e.length; this._process(); return this._hash }, clone: function () { var e = j.clone.call(this); e._hash = this._hash.clone(); return e }
}); e.SHA1 = j._createHelper(m); e.HmacSHA1 = j._createHmacHelper(m)
})();
var isWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
var hasWebWorker = !isWebWorker && window.Worker;
function Solver(challenge, work, delay, blurfactor) {
var iter = 0;
var previous = Date.now();
var challengeParts = challenge.split(';');
var bits = typeof (challengeParts[1]) == 'undefined'
? (32 - 22)
: (32 - parseInt(challengeParts[1]));
challenge = CryptoJS.enc.Hex.parse(challengeParts[0]);
this.search = function () {
// Do more work if our setTimeout did not return on time, there is a
// good chance it was caused the window going out of focus. We don't
// follow the best practice of slowing down here because the user
// has probably tabbed away so do something else during the
// calculation. This means we are taking too long so must go faster.
var end = (delay > 30) && ((Date.now() - previous) > (delay * 3)) ? iter + (work * blurfactor) : iter + work;
for (; iter < end; iter++) {
var wordArray = CryptoJS.lib.WordArray
.create([(iter | 0)])
.concat(challenge);
var hashWord = CryptoJS.SHA1(wordArray);
var shifted = hashWord.words[0] >>> bits;
if ((shifted & 0xFFFFFFFF) === 0) {
var hash = hashWord.toString();
return iter;
}
}
previous = Date.now();
return -1;
};
};
this.start = function (params) {
if (hasWebWorker && typeof (params.js) === 'string') {
// Spawn a web worker to do the work.
var cb = params.complete || function () { };
try
{
var worker = new Worker(params.js);
worker.onmessage = function (evt) {
cb(evt.data);
};
worker.postMessage({
op: 'start', params: {
'challenge': params.challenge
}
});
return {
canSpeedUp: false,
speedUp: function () { },
cancel: function () { worker.terminate(); cb(-1); }
};
}
catch (e)
{
// Most likely you are developing the page from a directory,
// most browsers will not create a worker from a file:// uri.
console.warn(e.message);
}
} else if (isWebWorker) {
// Inside the web worker.
onmessage = function (evt) {
if (evt.data.op == 'start') {
var solver = new Solver(evt.data.params.challenge, 1000, 10000, 1);
while (true) {
var result = solver.search();
if (result >= 0) {
postMessage(result);
return;
}
}
}
};
return;
}
// Web workers not supported, or JS file failed to load.
var challenge = params.challenge;
var delay = params.delay || 30;
var work = params.work || 800;
var blurfactor = params.blurfactor || 30;
var cb = params.complete || function () { };
var solver = new Solver(challenge, work, delay);
function pulse() {
var result = solver.search();
if (result >= 0) {
cb(result);
} else if (delay >= 0) {
setTimeout(pulse, delay);
} else {
cb(-1);
}
}
pulse();
return {
canSpeedUp: true,
speedUp: function () { delay = 0; work = 10000; },
cancel: function () { delay = -1; }
};
};
return this;
}();
// Spawned worker.
if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
PowJS.start();
}
@rangpec
Copy link

rangpec commented May 24, 2025

I have similar requirement. But I need to write in C++. could you please help me on this? im giving pseudo code here.

authdata = args[1]; // input argument to my program.
while(true) {
# generate short random string, utf−8 characters,
# except [\n\r\t ], it means that the suffix should not contain the characters: newline, carriege return, tab and space
suffix = random_string()
cksum_in_hex = SHA1(authdata + suffix)
check if the checksum has enough leading zeros
# (length of leading zeros should be equal to the nine 0s("000000000")
if cksum_in_hex.startswith("000000000"):
cout << suffix << endl;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment