838 lines
26 KiB
TypeScript
838 lines
26 KiB
TypeScript
// Copyright 2015-2018 authors & contributors of https://github.com/ethereumjs/merkle-patricia-tree
|
|
// Copyright 2017-2018 @polkadot/trie-db authors & contributors
|
|
// This software may be modified and distributed under the terms
|
|
// of the MPL-2.0 license. See the LICENSE file for details.
|
|
|
|
import { HashFn, NodeFactory, SemaphorePromise } from './types';
|
|
|
|
import async from 'async';
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
import levelup from 'levelup';
|
|
import memdown from 'memdown';
|
|
// import logger from '@polkadot/util/logger';
|
|
import assert from '@polkadot/util/assert';
|
|
import isU8a from '@polkadot/util/is/u8a';
|
|
import promisify from '@polkadot/util/promisify';
|
|
// import u8aToHex from '@polkadot/util/u8a/toHex';
|
|
import keccakAsU8a from '@polkadot/util-crypto/keccak/asU8a';
|
|
import decodeRlp from '@polkadot/util-rlp/decode';
|
|
|
|
import { EMPTY_ROOT_U8A, EMPTY_ROOT_STR } from './constants';
|
|
import encoder from './encoder';
|
|
import { nibblesMatchingLength, nibblesIsEqual, nibblesFromU8a } from './nibbles';
|
|
import ReadStream from './streams/TrieRead';
|
|
import createFactory from './nodeFactory';
|
|
import isRawNode from './util/isRawNode';
|
|
import prioritizedTaskExecutor from './util/taskExecutor';
|
|
import semaphore from './util/semaphore';
|
|
|
|
type RawPutFn = (key: Uint8Array, value: Uint8Array) => Promise<any>;
|
|
|
|
// const l = logger('trie/base');
|
|
|
|
export default class BaseTrie {
|
|
// @ts-ignore we are assigning via this.root in constructor
|
|
_root: Uint8Array;
|
|
_getDBs: any[]; // FIXME
|
|
_putDBs: any[]; // FIXME
|
|
dbDown: any; // FIXME
|
|
db: any; // FIXME
|
|
hashing: HashFn;
|
|
nodeFactory: NodeFactory;
|
|
putRaw: RawPutFn;
|
|
semaphore: SemaphorePromise;
|
|
|
|
constructor (db: any, root: Uint8Array = EMPTY_ROOT_U8A, hashing: HashFn = keccakAsU8a) {
|
|
this.putRaw = this._putRaw;
|
|
|
|
this.semaphore = semaphore(1);
|
|
this.dbDown = db || memdown();
|
|
this.db = levelup(encoder(this.dbDown));
|
|
this._getDBs = [this.db];
|
|
this._putDBs = [this.db];
|
|
this.hashing = hashing;
|
|
this.nodeFactory = createFactory(hashing);
|
|
this.root = root;
|
|
|
|
// l.debug(() => ['Created BaseTrie', typeof db, u8aToHex(root)]);
|
|
}
|
|
|
|
get root (): Uint8Array {
|
|
return this._root;
|
|
}
|
|
|
|
set root (value: Uint8Array) {
|
|
if (value) {
|
|
assert(isU8a(value) && value.length === 32, 'Invalid root length. Roots are Uint8Array 32 bytes');
|
|
} else {
|
|
value = EMPTY_ROOT_U8A;
|
|
}
|
|
|
|
this._root = value;
|
|
}
|
|
|
|
/**
|
|
* Gets a value given a `key`
|
|
* @method get
|
|
* @param {Buffer|String} key - the key to search for
|
|
* @param {Function} cb A callback `Function` which is given the arguments `err` - for errors that may have occured and `value` - the found value in a `Buffer` or if no value was found `null`
|
|
*/
|
|
async get (key: Uint8Array): Promise<Uint8Array | null> {
|
|
// l.debug(() => ['get', 'key ->', u8aToHex(key)]);
|
|
|
|
const result = await this.findPath(key);
|
|
|
|
let value = null;
|
|
|
|
if (result && result.node && result.remainder.length === 0) {
|
|
value = result.node.value;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Stores a given `value` at the given `key`
|
|
* @method put
|
|
* @param {Buffer|String} key
|
|
* @param {Buffer|String} Value
|
|
* @returns {Function} Promise<void>
|
|
*/
|
|
async put (key: Uint8Array, value: Uint8Array) {
|
|
// l.debug(() => ['put', 'key ->', u8aToHex(key), 'value ->', u8aToHex(value, 256)]);
|
|
|
|
if (!value || value.toString() === '') {
|
|
return this.del(key);
|
|
}
|
|
|
|
return this.semaphore(async () => {
|
|
if (this.root.toString() === EMPTY_ROOT_STR) {
|
|
return this._createInitialNode(key, value); // if no root initialize this trie
|
|
}
|
|
|
|
// first try to find the give key or its nearst node
|
|
const result = await this.findPath(key);
|
|
|
|
return this._updateNode(key, value, result && result.remainder, result && result.stack);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* deletes a value given a `key`
|
|
* @method del
|
|
* @param {Buffer|String} key
|
|
* @param {Function} callback the callback `Function`
|
|
*/
|
|
del (key: Uint8Array) {
|
|
// l.debug(() => ['del', 'key ->', u8aToHex(key)]);
|
|
|
|
return this.semaphore(async () => {
|
|
const result = await this.findPath(key);
|
|
|
|
if (result && result.node) {
|
|
return this._deleteNode(key, result.stack);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieves a raw value in the underlying db
|
|
* @method getRaw
|
|
* @param {Buffer} key
|
|
* @param {Function} callback A callback `Function`, which is given the arguments `err` - for errors that may have occured and `value` - the found value in a `Buffer` or if no value was found `null`.
|
|
*/
|
|
async getRaw (key: Uint8Array): Promise<Uint8Array> {
|
|
let found: Uint8Array;
|
|
|
|
await Promise.all(
|
|
this._getDBs.map(async (db) => {
|
|
if (!found) {
|
|
try {
|
|
found = await db.get(key);
|
|
} catch (error) {
|
|
// l.error('getRaw', 'key ->', u8aToHex(key), error);
|
|
// not doing anything
|
|
}
|
|
}
|
|
|
|
return found;
|
|
})
|
|
);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
return found;
|
|
}
|
|
|
|
// retrieves a node from dbs by hash
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
async _lookupNode (node) {
|
|
if (isRawNode(node)) {
|
|
return this.nodeFactory.fromRaw(node);
|
|
}
|
|
|
|
let value = await this.getRaw(node);
|
|
|
|
if (value) {
|
|
try {
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
value = this.nodeFactory.fromRaw(decodeRlp(value));
|
|
} catch (error) {
|
|
// l.error('_lookupNode decoding', typeof value, value);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
// TODO: remove the proxy method when changing the caching
|
|
async _putRaw (key: Uint8Array, value: Uint8Array) {
|
|
return Promise.all(
|
|
this._putDBs.map((db) => {
|
|
return db.put(key, value);
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Removes a raw value in the underlying db
|
|
* @method delRaw
|
|
* @param {Buffer|String} key
|
|
* @param {Function} callback A callback `Function`, which is given the argument `err` - for errors that may have occured
|
|
*/
|
|
delRaw (key: Uint8Array) {
|
|
return Promise.all(
|
|
this._putDBs.map((db) => {
|
|
return db.del(key);
|
|
})
|
|
);
|
|
}
|
|
|
|
// writes a single node to dbs
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
_putNode (node) {
|
|
const hash = node.hash();
|
|
const serialized = node.serialize();
|
|
|
|
return this._putRaw(hash, serialized);
|
|
}
|
|
|
|
// writes many nodes to db
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
_batchNodes (opStack) {
|
|
return Promise.all(
|
|
this._putDBs.map((db) => {
|
|
return db.batch(opStack);
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Trys to find a path to the node for the given key
|
|
* It returns a `stack` of nodes to the closet node
|
|
* @method findPath
|
|
* @param {String|Buffer} - key - the search key
|
|
* @param {Function} - cb - the callback function. Its is given the following
|
|
* arguments
|
|
* - err - any errors encontered
|
|
* - node - the last node found
|
|
* - remainder - the remaining key nibbles not accounted for
|
|
* - stack - an array of nodes that forms the path to node we are searching for
|
|
*/
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
findPath (_targetKey) {
|
|
const stack: any[] = []; // FIXME
|
|
const targetKey = nibblesFromU8a(_targetKey);
|
|
|
|
// l.debug(() => ['findPath', targetKey.join(', ')]);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const processNode = (nodeRef, node, keyProgress, walkController) => {
|
|
const nodeKey = node.key || [];
|
|
const remainder = targetKey.slice(nibblesMatchingLength(keyProgress, targetKey));
|
|
const matchingLen = nibblesMatchingLength(remainder, nodeKey);
|
|
|
|
stack.push(node);
|
|
|
|
if (node.type === 'branch') {
|
|
if (remainder.length === 0) {
|
|
walkController.return(null, { node, remainder: [], stack });
|
|
// we exhausted the key without finding a node
|
|
} else {
|
|
const branchIndex = remainder[0];
|
|
const branchNode = node.getValue(branchIndex);
|
|
|
|
if (!branchNode) {
|
|
// there are no more nodes to find and we didn't find the key
|
|
walkController.return(null, { node: null, remainder, stack });
|
|
} else {
|
|
// node found, continuing search
|
|
walkController.only(branchIndex);
|
|
}
|
|
}
|
|
} else if (node.type === 'leaf') {
|
|
if (nibblesIsEqual(remainder, nodeKey)) {
|
|
// keys match, return node with empty key
|
|
walkController.return(null, { node, remainder: [], stack });
|
|
} else {
|
|
// reached leaf but keys dont match
|
|
walkController.return(null, { node: null, remainder, stack });
|
|
}
|
|
} else if (node.type === 'extention') {
|
|
if (matchingLen !== nodeKey.length) {
|
|
// keys dont match, fail
|
|
walkController.return(null, { node: null, remainder, stack });
|
|
} else {
|
|
// keys match, continue search
|
|
walkController.next();
|
|
}
|
|
}
|
|
};
|
|
|
|
return promisify(this, this._walkTrie, this.root, processNode);
|
|
}
|
|
|
|
/*
|
|
* Finds all nodes that store k,v values
|
|
*/
|
|
// NOTE seems unused
|
|
// _findNode = function (key, root, stack, cb) {
|
|
// this.findPath(key, (...args) => {
|
|
// cb.apply(null, args);
|
|
// });
|
|
// };
|
|
|
|
/*
|
|
* Finds all nodes that store k,v values
|
|
*/
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
_findValueNodes (onFound, cb) {
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
this._walkTrie(this.root, (nodeRef, node, key, walkController) => { // tslint:disable-line
|
|
let fullKey = key;
|
|
|
|
if (node.key) {
|
|
fullKey = key.concat(node.key);
|
|
}
|
|
|
|
if (node.type === 'leaf') {
|
|
// found leaf node!
|
|
onFound(nodeRef, node, fullKey, walkController.next);
|
|
} else if (node.type === 'branch' && node.value) {
|
|
// found branch with value
|
|
onFound(nodeRef, node, fullKey, walkController.next);
|
|
} else {
|
|
// keep looking for value nodes
|
|
walkController.next();
|
|
}
|
|
}, cb);
|
|
}
|
|
|
|
/*
|
|
* Finds all nodes that are stored directly in the db
|
|
* (some nodes are stored raw inside other nodes)
|
|
*/
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
_findDbNodes (onFound, cb) {
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
this._walkTrie(this.root, (nodeRef, node, key, walkController) => { // tslint:disable-line
|
|
if (isRawNode(nodeRef)) {
|
|
walkController.next();
|
|
} else {
|
|
onFound(nodeRef, node, key, walkController.next);
|
|
}
|
|
}, cb);
|
|
}
|
|
|
|
/**
|
|
* Updates a node
|
|
* @method _updateNode
|
|
* @param {Buffer} key
|
|
* @param {Buffer| String} value
|
|
* @param {Array} keyRemainder
|
|
* @param {Array} stack -
|
|
* @param {Function} cb - the callback
|
|
*/
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
async _updateNode (key, value, keyRemainder, stack) {
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const toSave = [];
|
|
const lastNode = stack.pop();
|
|
|
|
// add the new nodes
|
|
key = nibblesFromU8a(key);
|
|
|
|
// Check if the last node is a leaf and the key matches to this
|
|
let matchLeaf = false;
|
|
|
|
if (lastNode.type === 'leaf') {
|
|
let l = 0;
|
|
|
|
for (let i = 0; i < stack.length; i++) {
|
|
const n = stack[i];
|
|
|
|
if (n.type === 'branch') {
|
|
l++;
|
|
} else {
|
|
l += n.key.length;
|
|
}
|
|
}
|
|
|
|
if ((nibblesMatchingLength(lastNode.key, key.slice(l)) === lastNode.key.length) && (keyRemainder.length === 0)) {
|
|
matchLeaf = true;
|
|
}
|
|
}
|
|
|
|
if (matchLeaf) {
|
|
// just updating a found value
|
|
lastNode.value = value;
|
|
stack.push(lastNode);
|
|
} else if (lastNode.type === 'branch') {
|
|
stack.push(lastNode);
|
|
|
|
if (keyRemainder !== 0) {
|
|
// add an extention to a branch node
|
|
keyRemainder.shift();
|
|
// create a new leaf
|
|
stack.push(
|
|
this.nodeFactory.fromLeaf(keyRemainder, value)
|
|
);
|
|
} else {
|
|
lastNode.value = value;
|
|
}
|
|
} else {
|
|
// create a branch node
|
|
const lastKey = lastNode.key;
|
|
const matchingLength = nibblesMatchingLength(lastKey, keyRemainder);
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const newBranchNode = this.nodeFactory.fromBranch();
|
|
|
|
// create a new extention node
|
|
if (matchingLength !== 0) {
|
|
const newKey = lastNode.key.slice(0, matchingLength);
|
|
const newExtNode = this.nodeFactory.fromExtention(newKey, value);
|
|
|
|
stack.push(newExtNode);
|
|
lastKey.splice(0, matchingLength);
|
|
keyRemainder.splice(0, matchingLength);
|
|
}
|
|
|
|
stack.push(newBranchNode);
|
|
|
|
if (lastKey.length !== 0) {
|
|
const branchKey = lastKey.shift();
|
|
|
|
if (lastKey.length !== 0 || lastNode.type === 'leaf') {
|
|
// shriking extention or leaf
|
|
lastNode.key = lastKey;
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const formatedNode = this._formatNode(lastNode, false, toSave);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
newBranchNode.setValue(branchKey, formatedNode);
|
|
} else {
|
|
// remove extention or attaching
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
this._formatNode(lastNode, false, true, toSave);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
newBranchNode.setValue(branchKey, lastNode.value);
|
|
}
|
|
} else {
|
|
newBranchNode.value = lastNode.value;
|
|
}
|
|
|
|
if (keyRemainder.length !== 0) {
|
|
keyRemainder.shift();
|
|
|
|
// add a leaf node to the new branch node
|
|
stack.push(
|
|
this.nodeFactory.fromLeaf(keyRemainder, value)
|
|
);
|
|
} else {
|
|
newBranchNode.value = value;
|
|
}
|
|
}
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
return this._saveStack(key, stack, toSave);
|
|
}
|
|
|
|
// walk tree
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
async _walkTrie (root, onNode, onDone) {
|
|
root = root || this.root;
|
|
|
|
let isAborted: boolean = false;
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
let returnValues: any[] = [];
|
|
|
|
if (root.toString() === EMPTY_ROOT_STR) {
|
|
onDone();
|
|
return;
|
|
}
|
|
|
|
// the maximum pool size should be high enough to utilise the parallelizability of reading nodes from disk and
|
|
// low enough to utilize the prioritisation of node lookup.
|
|
const maxPoolSize = 500;
|
|
const taskExecutor = prioritizedTaskExecutor(maxPoolSize);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const processNode = (nodeRef, node, key, cb) => {
|
|
if (!node || isAborted) {
|
|
return cb();
|
|
}
|
|
|
|
const self = this;
|
|
let isStopped = false;
|
|
|
|
key = key || [];
|
|
|
|
const walkController = {
|
|
stop: function () {
|
|
isStopped = true;
|
|
cb();
|
|
},
|
|
// end all traversal and return values to the onDone cb
|
|
return: function () {
|
|
isAborted = true;
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
returnValues = arguments;
|
|
cb();
|
|
},
|
|
next: async function () {
|
|
if (isAborted || isStopped) {
|
|
return cb();
|
|
}
|
|
|
|
const children = node.getChildren();
|
|
|
|
// @ts-ignore FIXME callback
|
|
async.forEachOf(children, (childData: any[], index: number, cb) => {
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const keyExtension = childData[0];
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const childRef = childData[1];
|
|
const childKey = key.concat(keyExtension);
|
|
const priority = childKey.length;
|
|
|
|
// tslint:disable-next-line
|
|
taskExecutor(priority, async (taskCallback) => {
|
|
const childNode = await self._lookupNode(childRef);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
taskCallback();
|
|
processNode(childRef, childNode, childKey, cb);
|
|
});
|
|
}, cb);
|
|
},
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
only: function (childIndex) {
|
|
const childRef = node.getValue(childIndex);
|
|
const childKey = key.slice();
|
|
|
|
childKey.push(childIndex);
|
|
|
|
const priority = childKey.length;
|
|
|
|
// tslint:disable-next-line
|
|
taskExecutor(priority, async (taskCallback) => {
|
|
const childNode = await self._lookupNode(childRef);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
taskCallback();
|
|
processNode(childRef, childNode, childKey, cb);
|
|
});
|
|
}
|
|
};
|
|
onNode(nodeRef, node, key, walkController);
|
|
};
|
|
|
|
const node = await this._lookupNode(root);
|
|
|
|
processNode(root, node, null, (error: Error | null) => {
|
|
if (error) {
|
|
onDone(error);
|
|
return;
|
|
}
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
onDone.apply(null, returnValues);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* saves a stack
|
|
* @method _saveStack
|
|
* @param {Array} key - the key. Should follow the stack
|
|
* @param {Array} stack - a stack of nodes to the value given by the key
|
|
* @param {Array} opStack - a stack of levelup operations to commit at the end of this funciton
|
|
* @param {Function} cb
|
|
*/
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
_saveStack (key, stack, opStack: any[]) {
|
|
let lastRoot;
|
|
|
|
// update nodes
|
|
while (stack.length) {
|
|
const node = stack.pop();
|
|
|
|
if (node.type === 'leaf') {
|
|
key.splice(key.length - node.key.length);
|
|
} else if (node.type === 'extention') {
|
|
key.splice(key.length - node.key.length);
|
|
if (lastRoot) {
|
|
node.value = lastRoot;
|
|
}
|
|
} else if (node.type === 'branch') {
|
|
if (lastRoot) {
|
|
const branchKey = key.pop();
|
|
|
|
node.setValue(branchKey, lastRoot);
|
|
}
|
|
}
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
lastRoot = this._formatNode(node, stack.length === 0, opStack);
|
|
}
|
|
|
|
if (lastRoot) {
|
|
this.root = lastRoot;
|
|
}
|
|
|
|
return this._batchNodes(opStack);
|
|
}
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
async _deleteNode (key: Uint8Array, stack) {
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const processBranchNode = (key, branchKey, branchNode, parentNode, stack) => {
|
|
// branchNode is the node ON the branch node not THE branch node
|
|
const branchNodeKey = branchNode.key;
|
|
|
|
if (!parentNode || parentNode.type === 'branch') {
|
|
// branch->?
|
|
if (parentNode) {
|
|
stack.push(parentNode);
|
|
}
|
|
|
|
if (branchNode.type === 'branch') {
|
|
// create an extention node
|
|
// branch->extention->branch
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
stack.push(this.nodeFactory.fromExtention([branchKey], null));
|
|
key.push(branchKey);
|
|
} else {
|
|
// branch key is an extention or a leaf
|
|
// branch->(leaf or extention)
|
|
branchNodeKey.unshift(branchKey);
|
|
branchNode.key = branchNodeKey;
|
|
|
|
// hackery. This is equvilant to array.concat except we need keep the
|
|
// rerfance to the `key` that was passed in.
|
|
branchNodeKey.unshift(0);
|
|
branchNodeKey.unshift(key.length);
|
|
key.splice.apply(key, branchNodeKey);
|
|
}
|
|
stack.push(branchNode);
|
|
} else {
|
|
// parent is a extention
|
|
let parentKey = parentNode.key;
|
|
|
|
if (branchNode.type === 'branch') {
|
|
// ext->branch
|
|
parentKey.push(branchKey);
|
|
key.push(branchKey);
|
|
parentNode.key = parentKey;
|
|
stack.push(parentNode);
|
|
} else {
|
|
// branch node is an leaf or extention and parent node is an exstention
|
|
// add two keys together
|
|
// dont push the parent node
|
|
branchNodeKey.unshift(branchKey);
|
|
key = key.concat(branchNodeKey);
|
|
parentKey = parentKey.concat(branchNodeKey);
|
|
branchNode.key = parentKey;
|
|
}
|
|
stack.push(branchNode);
|
|
}
|
|
|
|
return key;
|
|
};
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const opStack: any[] = [];
|
|
let lastNode = stack.pop();
|
|
let parentNode = stack.pop();
|
|
|
|
if (!Array.isArray(key)) {
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
key = nibblesFromU8a(key);
|
|
}
|
|
|
|
if (!parentNode) {
|
|
// the root here has to be a leaf.
|
|
this.root = EMPTY_ROOT_U8A;
|
|
} else {
|
|
if (lastNode.type === 'branch') {
|
|
lastNode.value = null;
|
|
} else {
|
|
// the lastNode has to be a leaf if its not a branch. And a leaf's parent
|
|
// if it has one must be a branch.
|
|
const lastNodeKey = lastNode.key;
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
key.splice(key.length - lastNodeKey.length);
|
|
// delete the value
|
|
this._formatNode(lastNode, false, true, opStack);
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
parentNode.setValue(key.pop(), null);
|
|
lastNode = parentNode;
|
|
parentNode = stack.pop();
|
|
}
|
|
|
|
// nodes on the branch
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const branchNodes = [];
|
|
|
|
// count the number of nodes on the branch
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
lastNode.raw.forEach(function (node, i) {
|
|
const val = lastNode.getValue(i);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
if (val) branchNodes.push([i, val]);
|
|
});
|
|
|
|
// if there is only one branch node left, collapse the branch node
|
|
if (branchNodes.length === 1) {
|
|
// add the one remaing branch node to node above it
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const branchNode = branchNodes[0][1];
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
const branchNodeKey = branchNodes[0][0];
|
|
|
|
// look up node
|
|
const foundNode = await this._lookupNode(branchNode);
|
|
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
key = processBranchNode(key, branchNodeKey, foundNode, parentNode, stack, opStack);
|
|
|
|
return this._saveStack(key, stack, opStack);
|
|
} else {
|
|
// simple removing a leaf and recaluclation the stack
|
|
if (parentNode) {
|
|
stack.push(parentNode);
|
|
}
|
|
|
|
stack.push(lastNode);
|
|
|
|
return this._saveStack(key, stack, opStack);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Creates the initial node from an empty tree
|
|
_createInitialNode (key: Uint8Array, value: Uint8Array) {
|
|
const newNode = this.nodeFactory.fromLeaf(key, value);
|
|
|
|
this.root = newNode.hash();
|
|
|
|
return this._putNode(newNode);
|
|
}
|
|
|
|
// formats node to be saved by levelup.batch.
|
|
// returns either the hash that will be used key or the rawNode
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
_formatNode (node: any, topLevel: any, remove: any, opStack: any) {
|
|
if (arguments.length === 3) {
|
|
opStack = remove;
|
|
remove = false;
|
|
}
|
|
|
|
const rlpNode = node.serialize();
|
|
|
|
if (rlpNode.length >= 32 || topLevel) {
|
|
const hashRoot = node.hash();
|
|
|
|
// FIXME Actually 2 things wrong here - this is on checkpoint, so one higher-level class...
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
if (remove && this.isCheckpoint) {
|
|
opStack.push({
|
|
type: 'del',
|
|
key: hashRoot
|
|
});
|
|
} else {
|
|
opStack.push({
|
|
type: 'put',
|
|
key: hashRoot,
|
|
value: rlpNode
|
|
});
|
|
}
|
|
|
|
return hashRoot;
|
|
}
|
|
|
|
return node.raw;
|
|
}
|
|
|
|
/**
|
|
* The `data` event is given an `Object` hat has two properties; the `key` and the `value`. Both should be Buffers.
|
|
* @method createReadStream
|
|
* @return {stream.Readable} Returns a [stream](https://nodejs.org/dist/latest-v5.x/docs/api/stream.html#stream_class_stream_readable) of the contents of the `trie`
|
|
*/
|
|
createReadStream () {
|
|
return new ReadStream(this);
|
|
}
|
|
|
|
// creates a new trie backed by the same db
|
|
// and starting at the same root
|
|
copy () {
|
|
// l.debug(() => ['Copying BaseTrie', typeof this.dbDown, u8aToHex(this.root)]);
|
|
|
|
return new BaseTrie(this.dbDown, this.root, this.hashing);
|
|
}
|
|
|
|
/**
|
|
* The given hash of operations (key additions or deletions) are executed on the DB
|
|
* @method batch
|
|
* @example
|
|
* const ops = [
|
|
* { type: 'del', key: 'father' }
|
|
* , { type: 'put', key: 'name', value: 'Yuri Irsenovich Kim' }
|
|
* , { type: 'put', key: 'dob', value: '16 February 1941' }
|
|
* , { type: 'put', key: 'spouse', value: 'Kim Young-sook' }
|
|
* , { type: 'put', key: 'occupation', value: 'Clown' }
|
|
* ]
|
|
* trie.batch(ops)
|
|
* @param {Array} ops
|
|
* @param {Function} cb
|
|
*/
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
batch (ops: any[]) {
|
|
return Promise.all(
|
|
// @ts-ignore FIXME, we need to properly check the full file
|
|
ops.map((op) => {
|
|
if (op.type === 'put') {
|
|
return this.put(op.key, op.value);
|
|
} else if (op.type === 'del') {
|
|
return this.del(op.key);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks if a given root exists
|
|
* @method checkRoot
|
|
* @param {Buffer} root
|
|
* @param {Function} cb
|
|
*/
|
|
async checkRoot (root: Uint8Array): Promise<boolean> {
|
|
const value = await this._lookupNode(root);
|
|
|
|
return !!value;
|
|
}
|
|
}
|