src/main/generic/consensus/base/blockchain/BaseChain.js
/**
* @abstract
*/
class BaseChain extends IBlockchain {
/**
* @param {ChainDataStore} store
*/
constructor(store) {
super();
this._store = store;
}
/**
* @param {Hash} hash
* @param {boolean} [includeForks]
* @param {boolean} [includeBody]
* @returns {Promise.<?Block>}
*/
async getBlock(hash, includeForks = false, includeBody = false) {
const chainData = await this._store.getChainData(hash, includeBody);
return chainData && (chainData.onMainChain || includeForks) ? chainData.head : null;
}
/**
* @param {Hash} hash
* @param {boolean} [includeForks]
* @returns {Promise.<?Uint8Array>}
*/
getRawBlock(hash, includeForks = false) {
return this._store.getRawBlock(hash, includeForks);
}
/**
* @param {number} height
* @param {boolean} [includeBody]
* @returns {Promise.<?Block>}
*/
getBlockAt(height, includeBody = false) {
return this._store.getBlockAt(height, includeBody) || null;
}
/**
* @param {number} height
* @param {boolean} [lower]
* @returns {Promise.<?Block>}
*/
getNearestBlockAt(height, lower = true) {
return this._store.getNearestBlockAt(height, lower) || null;
}
/**
* @returns {Promise.<Array.<Hash>>}
*/
async getBlockLocators() {
// Push top 10 hashes first, then back off exponentially.
/** @type {Array.<Hash>} */
const locators = [this.headHash];
let block = this.head;
for (let i = Math.min(10, this.height) - 1; i > 0; i--) {
if (!block) {
break;
}
locators.push(block.prevHash);
block = await this.getBlock(block.prevHash); // eslint-disable-line no-await-in-loop
}
let step = 2;
for (let i = this.height - 10 - step; i > 0; i -= step) {
block = await this.getBlockAt(i); // eslint-disable-line no-await-in-loop
if (block) {
locators.push(await block.hash()); // eslint-disable-line no-await-in-loop
}
step *= 2;
// Respect max size for GetBlocksMessages
if (locators.length >= GetBlocksMessage.LOCATORS_MAX_COUNT) break;
}
// Push the genesis block hash.
if (locators.length === 0 || !locators[locators.length - 1].equals(GenesisConfig.GENESIS_HASH)) {
// Respect max size for GetBlocksMessages, make space for genesis hash if necessary
if (locators.length >= GetBlocksMessage.LOCATORS_MAX_COUNT) {
locators.pop();
}
locators.push(GenesisConfig.GENESIS_HASH);
}
return locators;
}
/**
* Computes the target value for the block after the given block or the head of this chain if no block is given.
* @param {Block} [block]
* @returns {Promise.<number>}
*/
async getNextTarget(block) {
/** @type {ChainData} */
let headData;
if (block) {
const hash = block.hash();
headData = await this._store.getChainData(hash);
Assert.that(!!headData);
} else {
block = this.head;
headData = this._mainChain;
}
// Retrieve the timestamp of the block that appears DIFFICULTY_BLOCK_WINDOW blocks before the given block in the chain.
// The block might not be on the main chain.
const tailHeight = Math.max(block.height - Policy.DIFFICULTY_BLOCK_WINDOW, 1);
/** @type {ChainData} */
let tailData;
if (headData.onMainChain) {
tailData = await this._store.getChainDataAt(tailHeight);
} else {
let prevData = headData;
for (let i = 0; i < Policy.DIFFICULTY_BLOCK_WINDOW && !prevData.onMainChain; i++) {
prevData = await this._store.getChainData(prevData.head.prevHash);
if (!prevData) {
// Not enough blocks are available to compute the next target, fail.
return -1;
}
}
if (prevData.onMainChain && prevData.head.height > tailHeight) {
tailData = await this._store.getChainDataAt(tailHeight);
} else {
tailData = prevData;
}
}
if (!tailData || tailData.totalDifficulty < 1) {
// Not enough blocks are available to compute the next target, fail.
return -1;
}
const deltaTotalDifficulty = headData.totalDifficulty - tailData.totalDifficulty;
return BlockUtils.getNextTarget(headData.head.header, tailData.head.header, deltaTotalDifficulty);
}
/* NIPoPoW Prover functions */
/**
* MUST be synchronized with .pushBlock() and variants!
* @returns {Promise.<ChainProof>}
* @protected
*/
_getChainProof() {
return this._prove(Policy.M, Policy.K, Policy.DELTA);
}
/**
* The "Prove" algorithm from the NIPoPow paper.
* @param {number} m
* @param {number} k
* @param {number} delta
* @returns {Promise.<ChainProof>}
* @private
*/
async _prove(m, k, delta) {
Assert.that(m >= 1, 'm must be >= 1');
Assert.that(delta > 0, 'delta must be > 0');
let prefix = new BlockChain([]);
// B <- C[0]
let startHeight = 1;
/** @type {ChainData} */
const headData = await this._store.getChainDataAt(Math.max(this.height - k, 1)); // C[-k]
const maxDepth = headData.superBlockCounts.getCandidateDepth(m);
// for mu = |C[-k].interlink| down to 0 do
for (let depth = maxDepth; depth >= 0; depth--) {
// alpha = C[:-k]{B:}|^mu
/** @type {Array.<ChainData>} */
const alpha = await this._getSuperChain(depth, headData, startHeight); // eslint-disable-line no-await-in-loop
// pi = pi (union) alpha
prefix = BlockChain.merge(prefix, new BlockChain(alpha.map(data => data.head.toLight())));
// if good_(delta,m)(C, alpha, mu) then
if (BaseChain._isGoodSuperChain(alpha, depth, m, delta)) {
Assert.that(alpha.length >= m, `Good superchain expected to be at least ${m} long`);
Log.v(BaseChain, () => `Found good superchain at depth ${depth} with length ${alpha.length} (#${startHeight} - #${headData.head.height})`);
// B <- alpha[-m]
startHeight = alpha[alpha.length - m].head.height;
}
}
// X <- C[-k:]
const suffix = await this._getHeaderChain(this.height - headData.head.height);
// return piX
return new ChainProof(prefix, suffix);
}
/**
* @param {number} depth
* @param {ChainData} headData
* @param {number} [tailHeight]
* @returns {Promise.<Array.<ChainData>>}
* @private
*/
async _getSuperChain(depth, headData, tailHeight = 1) {
Assert.that(tailHeight >= 1, 'tailHeight must be >= 1');
/** @type {Array.<ChainData>} */
const chain = [];
// Include head if it is at the requested depth or below.
const headDepth = BlockUtils.getHashDepth(await headData.head.pow());
if (headDepth >= depth) {
chain.push(headData);
}
// Follow the interlink pointers back at the requested depth.
/** @type {ChainData} */
let chainData = headData;
let j = Math.max(depth - BlockUtils.getTargetDepth(chainData.head.target), -1);
while (j < chainData.head.interlink.hashes.length && chainData.head.height > tailHeight) {
const reference = j < 0 ? chainData.head.prevHash : chainData.head.interlink.hashes[j];
chainData = await this._store.getChainData(reference); // eslint-disable-line no-await-in-loop
if (!chainData) {
// This can happen in the light/nano client if chain superquality is harmed.
// Return a best-effort chain in this case.
Log.w(BaseChain, `Failed to find block ${reference} while constructing SuperChain at depth ${depth} - returning truncated chain`);
break;
}
chain.push(chainData);
j = Math.max(depth - BlockUtils.getTargetDepth(chainData.head.target), -1);
}
if ((chain.length === 0 || chain[chain.length - 1].head.height > 1) && tailHeight === 1) {
chain.push(await ChainData.initial(GenesisConfig.GENESIS_BLOCK));
}
return chain.reverse();
}
/**
* @param {Array.<ChainData>} superchain
* @param {number} depth
* @param {number} m
* @param {number} delta
* @returns {boolean}
*/
static _isGoodSuperChain(superchain, depth, m, delta) {
return BaseChain._hasSuperQuality(superchain, depth, m, delta)
&& BaseChain._hasMultiLevelQuality(superchain, depth, m, delta);
}
/**
* @param {Array.<ChainData>} superchain
* @param {number} depth
* @param {number} m
* @param {number} delta
* @returns {boolean}
* @private
*/
static _hasSuperQuality(superchain, depth, m, delta) {
Assert.that(m >= 1, 'm must be >= 1');
if (superchain.length < m) {
return false;
}
for (let i = m; i <= superchain.length; i++) {
const underlyingLength = superchain[superchain.length - 1].head.height - superchain[superchain.length - i].head.height + 1;
if (!BaseChain._isLocallyGood(i, underlyingLength, depth, delta)) {
return false;
}
}
return true;
}
/**
*
* @param {Array.<ChainData>} superchain
* @param {number} depth
* @param {number} k1
* @param {number} delta
* @returns {boolean}
* @private
*/
static _hasMultiLevelQuality(superchain, depth, k1, delta) {
if (depth <= 0) {
return true;
}
for (let i = 0; i < superchain.length - k1; i++) {
const tailData = superchain[i];
const headData = superchain[i + k1];
for (let mu = depth; mu >= 1; mu--) {
const upperChainLength = headData.superBlockCounts.get(mu) - tailData.superBlockCounts.get(mu);
switch (BaseChain.MULTILEVEL_STRATEGY) {
case BaseChain.MultilevelStrategy.STRICT: {
const lowerChainLength = headData.superBlockCounts.get(mu - 1) - tailData.superBlockCounts.get(mu - 1);
/*
// Original paper badness check:
if (lowerChainLength > Math.pow(1 + delta, 1 / depth) * 2 * upperChainLength) {
Log.d(BaseChain, `Chain badness detected at depth ${depth}, failing at ${mu}/${mu - 1}`
+ ` with ${upperChainLength}/${Math.pow(1 + delta, 1 / depth) * 2 * upperChainLength}/${lowerChainLength} blocks`);
return false;
}
*/
// Alternative badness check:
if (2 * upperChainLength < Math.pow(1 - delta, 1 / depth) * lowerChainLength) {
Log.d(BaseChain, `Chain badness detected at depth ${depth}, failing at ${mu}/${mu - 1}`
+ ` with ${upperChainLength}/${Math.pow(1 - delta, 1 / depth) * lowerChainLength}/${lowerChainLength} blocks`);
return false;
}
break;
}
default:
case BaseChain.MultilevelStrategy.MODERATE: {
// Relaxed badness check:
for (let j = mu - 1; j >= 0; j--) {
const lowerChainLength = headData.superBlockCounts.get(j) - tailData.superBlockCounts.get(j);
if (!BaseChain._isLocallyGood(upperChainLength, lowerChainLength, mu - j, delta)) {
Log.d(BaseChain, `Chain badness detected at depth ${depth}[${i}:${i + k1}], failing at ${mu}/${j}`);
return false;
}
}
break;
}
case BaseChain.MultilevelStrategy.RELAXED: {
// Local goodness only:
const lowerChainLength = headData.superBlockCounts.get(mu - 1) - tailData.superBlockCounts.get(mu - 1);
const underlyingLength = headData.head.height - tailData.head.height + 1;
if (!BaseChain._isLocallyGood(lowerChainLength, underlyingLength, depth, delta)) {
Log.d(BaseChain, `Chain badness detected at depth ${depth}[${i}:${i + k1}], failing at ${mu}`);
return false;
}
break;
}
}
}
}
return true;
}
/**
* @param {number} superLength
* @param {number} underlyingLength
* @param {number} depth
* @param {number} delta
* @returns {boolean}
* @private
*/
static _isLocallyGood(superLength, underlyingLength, depth, delta) {
// |C'| > (1 - delta) * 2^(-mu) * |C|
return superLength > (1 - delta) * Math.pow(2, -depth) * underlyingLength;
}
/**
* @param {number} length
* @param {Block} [head]
* @returns {Promise.<HeaderChain>}
* @private
*/
async _getHeaderChain(length, head = this.head) {
const headers = [];
while (head && headers.length < length) {
headers.push(head.header);
head = await this.getBlock(head.prevHash); // eslint-disable-line no-await-in-loop
}
return new HeaderChain(headers.reverse());
}
/**
* @param {ChainProof} proof
* @param {BlockHeader} header
* @param {boolean} [failOnBadness]
* @returns {Promise.<ChainProof>}
* @protected
*/
async _extendChainProof(proof, header, failOnBadness = true) {
// Append new header to proof suffix.
const suffix = proof.suffix.headers.slice();
suffix.push(header);
// If the suffix is not long enough (short chain), we're done.
const prefix = proof.prefix.blocks.slice();
if (suffix.length <= Policy.K) {
return new ChainProof(new BlockChain(prefix), new HeaderChain(suffix));
}
// Cut the tail off the suffix.
const suffixTail = suffix.shift();
// Construct light block out of the old suffix tail.
const interlink = await proof.prefix.head.getNextInterlink(suffixTail.target, suffixTail.version);
const prefixHead = new Block(suffixTail, interlink);
// Append old suffix tail block to prefix.
prefix.push(prefixHead);
// Extract layered superchains from prefix. Make a copy because we are going to change the chains array.
const chains = (await proof.prefix.getSuperChains()).slice();
// Append new prefix head to chains.
const depth = BlockUtils.getHashDepth(await prefixHead.pow());
for (let i = depth; i >= 0; i--) {
// Append block. Don't modify the chain, create a copy.
if (!chains[i]) {
chains[i] = new BlockChain([prefixHead]);
} else {
chains[i] = new BlockChain([...chains[i].blocks, prefixHead]);
}
}
// If the new header isn't a superblock, we're done.
if (depth - BlockUtils.getTargetDepth(prefixHead.target) <= 0) {
return new ChainProof(new BlockChain(prefix, chains), new HeaderChain(suffix));
}
// Prune unnecessary blocks if the chain is good.
// Try to extend proof if the chain is bad.
const deletedBlockHeights = new Set();
for (let i = depth; i >= 0; i--) {
const superchain = chains[i];
if (superchain.length < Policy.M) {
continue;
}
// XXX Hack: Convert BlockChain to array of pseudo-ChainData for the super quality check.
const _superchain = superchain.blocks.map(block => ({ head: block }));
if (!BaseChain._hasSuperQuality(_superchain, i, Policy.M, Policy.DELTA)) {
Log.w(BaseChain, `Chain quality badness detected at depth ${i}`);
// TODO extend superchains at lower levels
if (failOnBadness) {
return null;
}
continue;
}
// Remove all blocks in lower chains up to (including) superchain[-m].
const referenceBlock = superchain.blocks[superchain.length - Policy.M];
for (let j = i - 1; j >= 0; j--) {
let numBlocksToDelete = 0;
let candidateBlock = chains[j].blocks[numBlocksToDelete];
while (candidateBlock.height <= referenceBlock.height) {
// eslint-disable-next-line no-await-in-loop
const candidateDepth = BlockUtils.getHashDepth(await candidateBlock.pow());
if (candidateDepth === j && candidateBlock.height > 1) {
deletedBlockHeights.add(candidateBlock.height);
}
numBlocksToDelete++;
candidateBlock = chains[j].blocks[numBlocksToDelete];
}
if (numBlocksToDelete > 0) {
// Don't modify the chain, create a copy.
chains[j] = new BlockChain(chains[j].blocks.slice(numBlocksToDelete));
}
}
}
// Remove all deleted blocks from prefix.
const newPrefix = new BlockChain(prefix.filter(block => !deletedBlockHeights.has(block.height)), chains);
// Return the extended proof.
return new ChainProof(newPrefix, new HeaderChain(suffix));
}
/**
* MUST be synchronized with .pushBlock() and variants!
* @param {Block} blockToProve
* @param {Block} knownBlock
* @returns {Promise.<?BlockChain>}
* @protected
*/
async _getBlockProof(blockToProve, knownBlock) {
/**
* @param {Block} block
* @param {number} depth
* @returns {Hash}
*/
const getInterlinkReference = (block, depth) => {
const index = Math.min(depth - BlockUtils.getTargetDepth(block.target), block.interlink.length - 1);
return index < 0 ? block.prevHash : block.interlink.hashes[index];
};
const blocks = [];
const hashToProve = blockToProve.hash();
const proveTarget = BlockUtils.hashToTarget(await blockToProve.pow());
const proveDepth = BlockUtils.getTargetDepth(proveTarget);
let depth = BlockUtils.getTargetDepth(knownBlock.target) + knownBlock.interlink.length - 1;
let block = knownBlock;
let reference = getInterlinkReference(block, depth);
while (!hashToProve.equals(reference)) {
const nextBlock = await this.getBlock(reference); // eslint-disable-line no-await-in-loop
if (!nextBlock) {
// This can happen in the light/nano client if the blockToProve is known but blocks between tailBlock
// and blockToProve are missing.
Log.w(BaseChain, `Failed to find block ${reference} while constructing inclusion proof`);
return null;
}
if (nextBlock.height < blockToProve.height) {
// We have gone past the blockToProve, but are already at proveDepth, fail.
if (depth <= proveDepth) {
return null;
}
// Decrease depth and thereby step size.
depth--;
reference = getInterlinkReference(block, depth);
} else if (nextBlock.height > blockToProve.height) {
// We are still in front of blockToProve, add block to result and advance.
blocks.push(nextBlock.toLight());
block = nextBlock;
reference = getInterlinkReference(block, depth);
} else {
// We found a reference to a different block than blockToProve at its height.
Log.w(BaseChain, `Failed to prove block ${hashToProve} - different block ${reference} at its height ${block.height}`);
return null;
}
}
// Include the blockToProve in the result.
blocks.push(blockToProve.toLight());
return new BlockChain(blocks.reverse());
}
/**
* @param {Array.<BlockHeader>} headers
* @return {Promise.<void>}
*/
static async manyPow(headers) {
const worker = await CryptoWorker.getInstanceAsync();
const size = worker.poolSize || 1;
const partitions = [];
let j = 0;
for (let i = 0; i < size; ++i) {
partitions.push([]);
for (; j < ((i + 1) / size) * headers.length; ++j) {
partitions[i].push(headers[j].serialize());
}
}
const promises = [];
for (const part of partitions) {
promises.push(worker.computeArgon2dBatch(part));
}
const pows = (await Promise.all(promises)).reduce((a, b) => [...a, ...b], []);
for(let i = 0; i < headers.length; ++i) {
headers[i]._pow = new Hash(pows[i]);
}
}
/* NiPoPoW Verifier functions */
/**
* @param {ChainProof} proof1
* @param {ChainProof} proof2
* @param {number} m
* @returns {boolean}
*/
static async isBetterProof(proof1, proof2, m) {
const lca = BlockChain.lowestCommonAncestor(proof1.prefix, proof2.prefix);
const score1 = await NanoChain._getProofScore(proof1.prefix, lca, m);
const score2 = await NanoChain._getProofScore(proof2.prefix, lca, m);
return score1 === score2
? proof1.suffix.totalDifficulty() > proof2.suffix.totalDifficulty()
: score1 > score2;
}
/**
*
* @param {BlockChain} chain
* @param {Block} lca
* @param {number} m
* @returns {Promise.<number>}
* @protected
*/
static async _getProofScore(chain, lca, m) {
const counts = [];
for (const block of chain.blocks) {
if (block.height < lca.height) {
continue;
}
const depth = BlockUtils.getHashDepth(await block.pow()); // eslint-disable-line no-await-in-loop
counts[depth] = counts[depth] ? counts[depth] + 1 : 1;
}
let sum = 0;
let depth;
for (depth = counts.length - 1; sum < m && depth >= 0; depth--) {
sum += counts[depth] ? counts[depth] : 0;
}
let maxScore = Math.pow(2, depth + 1) * sum;
let length = sum;
for (let i = depth; i >= 0; i--) {
length += counts[i] ? counts[i] : 0;
const score = Math.pow(2, i) * length;
maxScore = Math.max(maxScore, score);
}
return maxScore;
}
}
BaseChain.MultilevelStrategy = {
STRICT: 1,
MODERATE: 2,
RELAXED: 3
};
BaseChain.MULTILEVEL_STRATEGY = BaseChain.MultilevelStrategy.MODERATE;
Class.register(BaseChain);