src/main/generic/consensus/base/account/tree/AccountsProof.js
class AccountsProof {
/**
* @param {Array.<AccountsTreeNode>} nodes
*/
constructor(nodes) {
if (!nodes || !Array.isArray(nodes) || !NumberUtils.isUint16(nodes.length)
|| nodes.some(it => !(it instanceof AccountsTreeNode))) throw 'Malformed nodes';
/** @type {Array.<AccountsTreeNode>} */
this._nodes = nodes;
/** @type {HashMap.<Hash,AccountsTreeNode>} */
this._index = null;
}
/**
* @param {SerialBuffer} buf
* @returns {AccountsProof}
*/
static unserialize(buf) {
const count = buf.readUint16();
const nodes = [];
for (let i = 0; i < count; i++) {
nodes.push(AccountsTreeNode.unserialize(buf));
}
return new AccountsProof(nodes);
}
/**
* @param {SerialBuffer} [buf]
* @returns {SerialBuffer}
*/
serialize(buf) {
buf = buf || new SerialBuffer(this.serializedSize);
buf.writeUint16(this._nodes.length);
for (const node of this._nodes) {
node.serialize(buf);
}
return buf;
}
/** @type {number} */
get serializedSize() {
let size = /*count*/ 2;
for (const node of this._nodes) {
size += node.serializedSize;
}
return size;
}
/**
* Assumes nodes to be in post order and hashes nodes to check internal consistency of proof.
* XXX Abuse this method to index the nodes contained in the proof. This forces callers to explicitly verify()
* the proof before retrieving accounts.
* @returns {boolean}
*/
verify() {
/** @type {Array.<AccountsTreeNode>} */
const children = [];
this._index = new HashMap();
for (const node of this._nodes) {
// If node is a branch node, validate its children.
if (node.isBranch()) {
let child;
while (child = children.pop()) { // eslint-disable-line no-cond-assign
if (child.isChildOf(node)) {
const hash = child.hash();
// If the child is not valid, return false.
if (!node.getChildHash(child.prefix).equals(hash) || node.getChild(child.prefix) !== child.prefix) {
return false;
}
this._index.put(hash, child);
} else {
children.push(child);
break;
}
}
}
// Append child.
children.push(node);
}
// The last element must be the root node.
return children.length === 1 && children[0].prefix === '' && children[0].isBranch();
}
/**
* @param {Address} address
* @returns {?Account}
*/
getAccount(address) {
if (!this._index) {
throw new Error('AccountsProof must be verified before retrieving accounts. Call verify() first.');
}
const rootNode = this._nodes[this._nodes.length - 1];
const prefix = address.toHex();
return this._getAccount(rootNode, prefix);
}
/**
* @param {AccountsTreeNode} node
* @param {string} prefix
* @returns {?Account}
* @private
*/
_getAccount(node, prefix) {
// Find common prefix between node and requested address.
const commonPrefix = StringUtils.commonPrefix(node.prefix, prefix);
// If the prefix does not fully match, the requested account does not exist.
if (commonPrefix.length !== node.prefix.length) return null;
// If the remaining address is empty, we have found the requested node.
if (commonPrefix === prefix) return node.account;
// Descend into the matching child node if one exists.
const childKey = node.getChildHash(prefix);
if (childKey) {
const childNode = this._index.get(childKey);
// If the child exists but is not part of the proof, fail.
if (!childNode) {
throw new Error('Requested address not part of AccountsProof');
}
return this._getAccount(childNode, prefix);
}
// No matching child exists, the requested account does not exist.
return null;
}
/**
* @returns {string}
*/
toString() {
return `AccountsProof{length=${this.length}}`;
}
/**
* @returns {Hash}
*/
root() {
return this._nodes[this._nodes.length - 1].hash();
}
/** @type {number} */
get length() {
return this._nodes.length;
}
/** @type {Array.<AccountsTreeNode>} */
get nodes() {
return this._nodes;
}
}
Class.register(AccountsProof);