bcuckoo
Version:
Cuckoo Cycle verification and miner in pure javascript
680 lines (510 loc) • 14.4 kB
JavaScript
/*!
* cuckoo.js - cuckoo cycle implementation
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/bcuckoo
*
* Logic based on tromp/cuckoo:
* https://github.com/tromp/cuckoo/blob/master/src/cuckoo.h
* https://github.com/tromp/cuckoo/blob/master/src/simple_miner.cpp
*/
'use strict';
const {assert, enforce} = require('bsert');
const Struct = require('bufio/lib/struct');
const blake2b = require('bcrypto/lib/blake2b');
const sha3 = require('bcrypto/lib/sha3');
const {siphash32, siphash32k256} = require('bcrypto/lib/siphash');
/*
* Constants
*/
const MAX_PATH = 8192;
const MIN_SIZE = 4;
const MAX_SIZE = 254;
const EMPTY_PROOF = Buffer.alloc(MIN_SIZE * 4, 0x00);
const codes = {
POW_OK: 0,
POW_PROOF_SIZE: 1,
POW_TOO_BIG: 2,
POW_TOO_SMALL: 3,
POW_NON_MATCHING: 4,
POW_BRANCH: 5,
POW_DEAD_END: 6,
POW_SHORT_CYCLE: 7,
POW_UNKNOWN: 8
};
const codesByVal = [
'POW_OK',
'POW_PROOF_SIZE',
'POW_TOO_BIG',
'POW_TOO_SMALL',
'POW_NON_MATCHING',
'POW_BRANCH',
'POW_DEAD_END',
'POW_SHORT_CYCLE',
'POW_UNKNOWN'
];
const msgByVal = [
'OK.',
'Wrong proof size.',
'Nonce too big.',
'Nonces not ascending.',
'Endpoints don\'t match up.',
'Branch in cycle.',
'Cycle dead ends.',
'Cycle too short.',
'Unknown error.'
];
/**
* Cuckoo Cycle
*/
class Cuckoo {
/**
* Create a cuckoo context.
* @constructor
* @param {Number} bits - Size of graph (EDGEBITS + 1).
* @param {Number} size - Size of cycle (PROOFSIZE).
* @param {Number} [perc=50] - Percentage of easiness (easipct).
* @param {Boolean} [legacy=false] - Whether to use old-style
* hashing (SIPHASH_COMPAT).
*/
constructor(bits, size, perc = 50, legacy = false) {
enforce((bits >>> 0) === bits, 'bits', 'integer');
enforce((size >>> 0) === size, 'size', 'integer');
enforce((perc >>> 0) === perc, 'perc', 'integer');
enforce(typeof legacy === 'boolean', 'legacy', 'boolean');
if (bits < 1 || bits > 32)
throw new Error('Graph bits must be 1 to 32.');
if (size < MIN_SIZE || size > MAX_SIZE)
throw new Error(`Proof size must be ${MIN_SIZE} to ${MAX_SIZE}.`);
if (size & 1)
throw new Error('Proof size must be even.');
if (perc < 1 || perc > 100)
throw new Error('Percent must be 1 to 100.');
// Maximum number of nodes on the graph (NNODES).
this.nodes = Math.pow(2, bits);
// Mask of edges for convenience (EDGEMASK).
this.mask = (this.nodes / 2) - 1;
// Size of cycle to find (PROOFSIZE).
// The odds of a graph containing an
// L-cycle are 1 in L.
this.size = size;
// Maximum nonce size (easiness).
this.easiness = Math.floor((perc * this.nodes) / 100);
// Sanity check.
assert(perc !== 50 || this.easiness === (this.mask + 1));
// Which style of hashing to use (SIPHASH_COMPAT).
this.siphash32 = legacy ? siphash32 : siphash32k256;
}
/**
* Get cuckoo error code string.
* @param {Number} value
* @returns {String}
*/
static code(value) {
enforce((value & 0xff) === value, 'code', 'byte');
if (value >= codesByVal.length)
value = codes.POW_UNKNOWN;
return codesByVal[value];
}
/**
* Get cuckoo error code message.
* @param {Number} value
* @returns {String}
*/
static msg(value) {
enforce((value & 0xff) === value, 'value', 'byte');
if (value >= msgByVal.length)
value = codes.POW_UNKNOWN;
return msgByVal[value];
}
/**
* Compute a siphash key from a header.
* @param {Buffer} hdr
* @returns {Buffer}
*/
sipkey(hdr) {
const hash = blake2b.digest(hdr, 32);
// Legacy hashing only uses the first 128 bits.
if (this.siphash32 === siphash32)
return hash.slice(0, 16);
return hash;
}
/**
* Create a new siphash node.
* @param {Buffer} key - Siphash key.
* @param {Number} nonce
* @param {Number} uorv
* @returns {Number}
*/
sipnode(key, nonce, uorv) {
assert(Buffer.isBuffer(key));
assert((nonce >>> 0) === nonce);
assert(uorv === 0 || uorv === 1);
const num = ((nonce << 1) | uorv) >>> 0;
const node = this.siphash32(num, key) & this.mask;
return ((node << 1) | uorv) >>> 0;
}
/**
* Verify a cuckoo cycle solution against a key.
* @param {Buffer} key
* @param {Uint32Array} nonces
* @returns {Number} error code
*/
verify(key, nonces) {
enforce(Buffer.isBuffer(key), 'key', 'buffer');
enforce(nonces instanceof Uint32Array, 'nonces', 'array');
if (nonces.length !== this.size)
return codes.POW_PROOF_SIZE;
const uvs = new Uint32Array(this.size * 2);
let xor0 = 0;
let xor1 = 0;
for (let n = 0; n < this.size; n++) {
if (nonces[n] >= this.easiness)
return codes.POW_TOO_BIG;
if (n > 0 && nonces[n] <= nonces[n - 1])
return codes.POW_TOO_SMALL;
const x = this.sipnode(key, nonces[n], 0);
const y = this.sipnode(key, nonces[n], 1);
uvs[2 * n] = x;
uvs[2 * n + 1] = y;
xor0 ^= x;
xor1 ^= y;
}
if (xor0 | xor1)
return codes.POW_NON_MATCHING;
let n = 0;
let i = 0;
do {
let j = i;
let k = j;
for (;;) {
k = (k + 2) % (2 * this.size);
if (k === i)
break;
if (uvs[k] === uvs[i]) {
if (j !== i)
return codes.POW_BRANCH;
j = k;
}
}
if (j === i)
return codes.POW_DEAD_END;
i = j ^ 1;
n += 1;
} while (i !== 0);
if (n !== this.size)
return codes.POW_SHORT_CYCLE;
return codes.POW_OK;
}
/**
* Verify a header's cuckoo cycle solution.
* @param {Buffer} hdr - Raw header (minus the solution).
* @param {Solution} sol
* @returns {Number} error code
*/
verifyHeader(hdr, sol) {
enforce(Buffer.isBuffer(hdr), 'hdr', 'buffer');
enforce(sol instanceof Solution, 'sol', 'solution');
if (sol.size() !== this.size)
return codes.POW_PROOF_SIZE;
return this.verify(this.sipkey(hdr), sol.toArray());
}
}
Cuckoo.codes = codes;
Cuckoo.codesByVal = codesByVal;
Cuckoo.msgByVal = msgByVal;
/**
* Cuckoo Miner
* @extends {Cuckoo}
*/
class Miner extends Cuckoo {
/**
* Create a cuckoo miner.
* @constructor
* @param {Number} bits - Size of graph (EDGEBITS + 1).
* @param {Number} size - Size of cycle (PROOFSIZE).
* @param {Number} [perc=50] - Percentage of easiness (easipct).
* @param {Boolean} [legacy=false] - Whether to use old-style
* hashing (SIPHASH_COMPAT).
*/
constructor(bits, size, perc = 50, legacy = false) {
super(bits, size, perc, legacy);
this.bits = bits;
this.perc = perc;
this.buckets = new Uint32Array(this.nodes + 1);
this.debug = () => {};
}
/**
* Create a path.
* @param {Number} u
* @param {Uint32Array} us
* @returns {Number}
*/
path(u, us) {
let nu = 0;
while (u > 0) {
nu += 1;
if (nu >= MAX_PATH) {
while (nu > 0 && us[nu - 1] !== u)
nu -= 1;
if (nu === 0)
throw new Error('Maximum path length exceeded.');
throw new Error(`Illegal ${MAX_PATH - nu}-cycle.`);
}
us[nu] = u;
u = this.buckets[u];
}
return nu;
}
/**
* Find solution after detecting a cycle.
* @param {Buffer} key
* @param {Uint32Array} us
* @param {Number} nu
* @param {Uint32Array} vs
* @param {Number} nv
* @returns {Uint32Array} nonces
*/
solution(key, us, nu, vs, nv) {
const cycle = new Set();
cycle.add(edge(us[0], vs[0]));
// u's in even position; v's in odd
while (nu--)
cycle.add(edge(us[(nu + 1) & ~1], us[nu | 1]));
// u's in odd position; v's in even
while (nv--)
cycle.add(edge(vs[nv | 1], vs[(nv + 1) & ~1]));
const nonces = new Uint32Array(this.size);
let n = 0;
for (let nonce = 0; nonce < this.easiness; nonce++) {
const x = this.sipnode(key, nonce, 0);
const y = this.sipnode(key, nonce, 1);
const e = edge(x, y);
if (cycle.has(e)) {
if (this.size > 2)
cycle.delete(e);
nonces[n] = nonce;
n += 1;
}
}
assert(n === this.size);
return nonces;
}
/**
* Find a cycle for a given key.
* @param {Buffer} key
* @returns {Uint32Array|null} nonces
*/
mine(key) {
enforce(Buffer.isBuffer(key), 'key', 'buffer');
const us = new Uint32Array(MAX_PATH);
const vs = new Uint32Array(MAX_PATH);
for (let i = 0; i < this.buckets.length; i++)
this.buckets[i] = 0;
this.debug(
'Attempting to find %d-cycle for %s (bits=%d, perc=%d).',
this.size,
key.toString('hex'),
this.bits,
this.perc);
for (let nonce = 0; nonce < this.easiness; nonce++) {
const u0 = this.sipnode(key, nonce, 0);
if (u0 === 0)
continue;
const v0 = this.sipnode(key, nonce, 1);
const u = this.buckets[u0];
const v = this.buckets[v0];
us[0] = u0;
vs[0] = v0;
let nu = this.path(u, us);
let nv = this.path(v, vs);
if (us[nu] === vs[nv]) {
const min = nu < nv ? nu : nv;
nu -= min;
nv -= min;
while (us[nu] !== vs[nv]) {
nu += 1;
nv += 1;
}
const len = nu + nv + 1;
this.debug('%d-cycle found at %d.', len, nonce * 100 / this.easiness);
if (len === this.size)
return this.solution(key, us, nu, vs, nv);
continue;
}
if (nu < nv) {
while (nu--)
this.buckets[us[nu + 1]] = us[nu];
this.buckets[u0] = v0;
} else {
while (nv--)
this.buckets[vs[nv + 1]] = vs[nv];
this.buckets[v0] = u0;
}
}
this.debug('Traversed %d nodes without solution.', this.easiness);
return null;
}
mineHeader(hdr) {
enforce(Buffer.isBuffer(hdr), 'hdr', 'buffer');
const key = this.sipkey(hdr);
const nonces = this.mine(key);
if (nonces)
return Solution.fromArray(nonces);
return null;
}
}
/**
* Cuckoo Cycle Solution
*/
class Solution extends Struct {
/**
* Create a cuckoo solution.
* @param {Object?} options
* @constructor
*/
constructor(options) {
super();
this.proof = EMPTY_PROOF;
if (options != null)
this.fromOptions(options);
}
hash(enc) {
const hash = blake2b.digest(this.proof, 32);
if (enc === 'hex')
return hash.toString('hex');
return hash;
}
sha3(enc) {
const hash = sha3.digest(this.proof);
if (enc === 'hex')
return hash.toString('hex');
return hash;
}
size() {
return this.proof.length >>> 2;
}
set(proof) {
enforce(Buffer.isBuffer(proof), 'proof', 'buffer');
enforce((proof.length & 3) === 0, 'proof', 'multiple of 4');
const size = proof.length >>> 2;
if (size < MIN_SIZE || size > MAX_SIZE)
throw new Error('Proof size exceeds limits.');
if ((size & 1) === 1)
throw new Error('Proof size must be even.');
this.proof = proof;
return this;
}
getSize() {
return 1 + this.proof.length;
}
write(bw) {
bw.writeU8(this.size());
bw.writeBytes(this.proof);
return this;
}
read(br) {
const size = br.readU8();
if (size < MIN_SIZE || size > MAX_SIZE)
throw new Error('Proof size exceeds limits.');
if (size & 1)
throw new Error('Proof size must be even.');
this.proof = br.readBytes(size * 4);
return this;
}
toArrayLike(ArrayLike) {
enforce(typeof ArrayLike === 'function', 'ArrayLike', 'function');
const size = this.size();
const arr = new ArrayLike(size);
assert(arr.length === size);
for (let i = 0; i < arr.length; i++)
arr[i] = this.proof.readUInt32LE(i * 4);
return arr;
}
toArray() {
return this.toArrayLike(Uint32Array);
}
getJSON() {
return this.toArrayLike(Array);
}
toString() {
return this.toJSON().join(', ');
}
fromOptions(obj) {
enforce(obj && typeof obj === 'object', 'obj', 'object');
if (Buffer.isBuffer(obj))
return this.set(obj);
if (typeof obj.length === 'number')
return this.fromArrayLike(obj);
return this.set(obj.proof);
}
fromArrayLike(arr) {
enforce(arr && typeof arr === 'object', 'arr', 'array-like');
enforce((arr.length >>> 0) === arr.length, 'arr', 'array-like');
if (arr.length < MIN_SIZE || arr.length > MAX_SIZE)
throw new Error('Proof size exceeds limits.');
if (arr.length & 1)
throw new Error('Proof size must be even.');
const proof = Buffer.allocUnsafe(arr.length * 4);
for (let i = 0; i < arr.length; i++)
proof.writeUInt32LE(arr[i] >>> 0, i * 4);
this.proof = proof;
return this;
}
fromArray(arr) {
enforce(arr instanceof Uint32Array, 'arr', 'array');
return this.fromArrayLike(arr);
}
fromJSON(json) {
enforce(Array.isArray(json), 'json', 'array');
return this.fromArrayLike(json);
}
fromString(str) {
enforce(typeof str === 'string', 'str', 'string');
if (str.length > (MAX_SIZE * 10) + (MAX_SIZE - 1) * 2)
throw new Error('String too large.');
const parts = str.split(', ');
if (parts.length < MIN_SIZE || parts.length > MAX_SIZE)
throw new Error('Proof size exceeds limits.');
if (parts.length & 1)
throw new Error('Proof size must be even.');
const arr = new Uint32Array(parts.length);
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
assert(part.length >= 1);
assert(part.length <= 10);
let word = 0;
for (let j = 0; j < part.length; j++) {
const ch = part.charCodeAt(j) - 0x30;
assert(ch >= 0 && ch <= 9);
word *= 10;
word += ch;
}
arr[i] = word;
}
return this.fromArray(arr);
}
format() {
return `<Solution: ${this.toString()}>`;
}
static fromArrayLike(arr) {
return new this().fromArrayLike(arr);
}
static fromArray(arr) {
return new this().fromArray(arr);
}
}
/*
* Helpers
*/
function edge(x, y) {
return `${x},${y}`;
}
/*
* Expose
*/
exports.codes = codes;
exports.codesByVal = codesByVal;
exports.msgByVal = msgByVal;
exports.Cuckoo = Cuckoo;
exports.Miner = Miner;
exports.Solution = Solution;