Skip to content

Commit

Permalink
Merge pull request #584 from EYBlockchain/ilyas/move-siblingPath
Browse files Browse the repository at this point in the history
Timber updates sibling paths using incoming leaves.
  • Loading branch information
Westlad authored Apr 4, 2022
2 parents 24dcc84 + 935c963 commit 3e4ef59
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 0 deletions.
79 changes: 79 additions & 0 deletions common-files/classes/timber.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,36 @@ const _insertLeaf = (leafVal, tree, path) => {
}
};

/**
This function is like _insertLeaf but it doesnt prune children on insertion
@function _safeInsertLeaf
@param {string} leafVal - The commitment hash to be inserted into the tree
@param {object} tree - The tree where leafVal will be inserted
@param {string} path - The path down tree that leafVal will be inserted into
@returns {object} An updated tree post-insertion but deepest branches are preserved.
*/
const _safeInsertLeaf = (leafVal, tree, path) => {
if (path.length === 0) {
if (tree.value === '0') return Leaf(leafVal);
return tree;
}
switch (tree.tag) {
// If we are at a branch, we use the next element in path to decide if we go down the left or right subtree.
case 'branch':
return path[0] === '0'
? Branch(_safeInsertLeaf(leafVal, tree.left, path.slice(1)), tree.right)
: Branch(tree.left, _safeInsertLeaf(leafVal, tree.right, path.slice(1)));
// If we are at a leaf AND path.length > 0, we need to expand the undeveloped subtree
// We then use the next element in path to decided which subtree to traverse
case 'leaf':
return path[0] === '0'
? Branch(_safeInsertLeaf(leafVal, Leaf('0'), path.slice(1)), Leaf('0'))
: Branch(Leaf('0'), _safeInsertLeaf(leafVal, Leaf('0'), path.slice(1)));
default:
return tree;
}
};

/**
This function is called recursively to traverse down the tree to find check set membership
@function _checkMembership
Expand Down Expand Up @@ -143,6 +173,28 @@ const frontierToTree = timber => {
}, timber.tree);
};

/**
This function converts a siblingPath into a tree-like structure.
@function
* @param {object} tree - The tree this sibling path will be inserted into.
* @param {object} siblingPath - The sibling path to be inserted.
* @param {number} index - The leafIndex corresponding to this valid sibling path.
* @param {string} value - The leafValue corresponding to this valid sibling path.
* @returns A tree object updated with this sibling path.
*/
const insertSiblingPath = (tree, siblingPath, index, value) => {
const pathToIndex = Number(index).toString(2).padStart(TIMBER_HEIGHT, '0');
const siblingIndexPath = siblingPath.path.map((s, idx) =>
s.dir === 'left'
? { value: s.value, path: `${pathToIndex.slice(0, TIMBER_HEIGHT - idx - 1)}0` }
: { value: s.value, path: `${pathToIndex.slice(0, TIMBER_HEIGHT - idx - 1)}1` },
);
const allPaths = [{ value, path: pathToIndex }].concat(siblingIndexPath);
return allPaths.reduce((acc, curr) => {
return _safeInsertLeaf(curr.value, acc, curr.path);
}, tree);
};

/**
We do batch insertions when doing stateless operations, the size of each batch is dependent on the tree structure.
Each batch insert has to less than or equal to the next closest power of two - otherwise we may unbalance the tree.
Expand Down Expand Up @@ -631,6 +683,33 @@ class Timber {
if (this.leafCount === 0) return [];
return Timber.reduceTree((a, b) => [].concat([a, b]).flat(), this.tree);
}

/**
@method
Updates a sibling path statelessly, given the previous path and a set of leaves to update the tree.
* @param {object} timber - The timber instance that contains the frontier and leafCount.
* @param {Array<string>} leaves - The elements that will be inserted.
* @param {number} leafIndex - The index in leaves that the sibling path will be calculated for.
* @param {string} leafValue - The value of the leaf sibling path will be calculated for.
* @param {object} siblingPath - The latest sibling path that for leafIndex that will be updated.
* @returns {object} - Updated siblingPath for leafIndex with leafValue.
*/
static statelessIncrementSiblingPath(timber, leaves, leafIndex, leafValue, siblingPath) {
if (leaves.length === 0 || leafIndex < 0 || leafIndex >= timber.leafCount) return siblingPath;

const leavesInsertOrder = batchLeaves(timber.leafCount, leaves, []);
// Turn the frontier into a tree-like structure
const newTree = frontierToTree(timber);
// Add the sibling path to the frontier tree-like structure
const siblingTree = insertSiblingPath(newTree, siblingPath, leafIndex, leafValue);
// Add all the new incoming leaves to this tree-like structure
const finalTree = leavesInsertOrder.reduce(
(acc, curr) => acc.insertLeaves(curr),
new Timber(timber.root, timber.frontier, timber.leafCount, siblingTree),
);
// Get the sibling path for leafIndex from this tree.
return finalTree.getSiblingPath(leafValue, leafIndex);
}
}

export default Timber;
44 changes: 44 additions & 0 deletions test/timber.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,49 @@ describe('Local Timber Tests', () => {
{ numRuns: 20 },
);
});
it('Check Sibling Path Increment', () => {
fc.assert(
fc.property(
fc.array(randomLeaf, { minLength: 0, maxLength: MAX_ARRAY }), // Remove Duplicates within both arrays
fc.array(randomLeaf, { minLength: 1, maxLength: 32 }), // Remove Duplicates within both arrays
fc.array(randomLeaf, { minLength: 1, maxLength: 32 }), // Remove Duplicates within both arrays
(leaves, addedLeaves, yetMoreLeaves) => {
const leafIndex = Math.max(0, Math.floor(Math.random() * (addedLeaves.length - 1)));
const leafValue = addedLeaves[leafIndex];
const initialTimber = new Timber().insertLeaves(leaves);
// Get a sibling path after new leaves are added
const statelessMerklePath = Timber.statelessSiblingPath(
initialTimber,
addedLeaves,
leafIndex,
);
// Update Timber statelessly (no tree)
const statelessUpdate = Timber.statelessUpdate(initialTimber, addedLeaves);
// Given the new state (sans tree), try to increment the sibling path
const statelessIncrementPath = Timber.statelessIncrementSiblingPath(
statelessUpdate,
yetMoreLeaves,
leaves.length + leafIndex,
leafValue,
statelessMerklePath,
);
const timber = new Timber().insertLeaves(
leaves.concat(addedLeaves).concat(yetMoreLeaves),
);
const timberMerklePath = timber.getSiblingPath(leafValue);
if (statelessIncrementPath.isMember) {
expect(
Timber.verifySiblingPath(leafValue, timber.root, statelessIncrementPath),
).to.eql(true);
expect(Timber.verifySiblingPath(leafValue, timber.root, timberMerklePath)).to.eql(
true,
);
}
expect(statelessIncrementPath.path).to.have.deep.members(timberMerklePath.path);
},
),
{ numRuns: 20 },
);
});
});
});
80 changes: 80 additions & 0 deletions wallet/src/common-files/classes/timber.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint import/no-extraneous-dependencies: "off" */
/* ignore unused exports */

/**
Expand Down Expand Up @@ -53,6 +54,36 @@ const _insertLeaf = (leafVal, tree, path) => {
}
};

/**
This function is like _insertLeaf but it doesnt prune children on insertion
@function _safeInsertLeaf
@param {string} leafVal - The commitment hash to be inserted into the tree
@param {object} tree - The tree where leafVal will be inserted
@param {string} path - The path down tree that leafVal will be inserted into
@returns {object} An updated tree post-insertion but deepest branches are preserved.
*/
const _safeInsertLeaf = (leafVal, tree, path) => {
if (path.length === 0) {
if (tree.value === '0') return Leaf(leafVal);
return tree;
}
switch (tree.tag) {
// If we are at a branch, we use the next element in path to decide if we go down the left or right subtree.
case 'branch':
return path[0] === '0'
? Branch(_safeInsertLeaf(leafVal, tree.left, path.slice(1)), tree.right)
: Branch(tree.left, _safeInsertLeaf(leafVal, tree.right, path.slice(1)));
// If we are at a leaf AND path.length > 0, we need to expand the undeveloped subtree
// We then use the next element in path to decided which subtree to traverse
case 'leaf':
return path[0] === '0'
? Branch(_safeInsertLeaf(leafVal, Leaf('0'), path.slice(1)), Leaf('0'))
: Branch(Leaf('0'), _safeInsertLeaf(leafVal, Leaf('0'), path.slice(1)));
default:
return tree;
}
};

/**
This function is called recursively to traverse down the tree to find check set membership
@function _checkMembership
Expand Down Expand Up @@ -140,6 +171,28 @@ const frontierToTree = timber => {
}, timber.tree);
};

/**
This function converts a siblingPath into a tree-like structure.
@function
* @param {object} tree - The tree this sibling path will be inserted into.
* @param {object} siblingPath - The sibling path to be inserted.
* @param {number} index - The leafIndex corresponding to this valid sibling path.
* @param {string} value - The leafValue corresponding to this valid sibling path.
* @returns A tree object updated with this sibling path.
*/
const insertSiblingPath = (tree, siblingPath, index, value) => {
const pathToIndex = Number(index).toString(2).padStart(TIMBER_HEIGHT, '0');
const siblingIndexPath = siblingPath.path.map((s, idx) =>
s.dir === 'left'
? { value: s.value, path: `${pathToIndex.slice(0, TIMBER_HEIGHT - idx - 1)}0` }
: { value: s.value, path: `${pathToIndex.slice(0, TIMBER_HEIGHT - idx - 1)}1` },
);
const allPaths = [{ value, path: pathToIndex }].concat(siblingIndexPath);
return allPaths.reduce((acc, curr) => {
return _safeInsertLeaf(curr.value, acc, curr.path);
}, tree);
};

/**
We do batch insertions when doing stateless operations, the size of each batch is dependent on the tree structure.
Each batch insert has to less than or equal to the next closest power of two - otherwise we may unbalance the tree.
Expand Down Expand Up @@ -628,6 +681,33 @@ class Timber {
if (this.leafCount === 0) return [];
return Timber.reduceTree((a, b) => [].concat([a, b]).flat(), this.tree);
}

/**
@method
Updates a sibling path statelessly, given the previous path and a set of leaves to update the tree.
* @param {object} timber - The timber instance that contains the frontier and leafCount.
* @param {Array<string>} leaves - The elements that will be inserted.
* @param {number} leafIndex - The index in leaves that the sibling path will be calculated for.
* @param {string} leafValue - The value of the leaf sibling path will be calculated for.
* @param {object} siblingPath - The latest sibling path that for leafIndex that will be updated.
* @returns {object} - Updated siblingPath for leafIndex with leafValue.
*/
static statelessIncrementSiblingPath(timber, leaves, leafIndex, leafValue, siblingPath) {
if (leaves.length === 0 || leafIndex < 0 || leafIndex >= timber.leafCount) return siblingPath;

const leavesInsertOrder = batchLeaves(timber.leafCount, leaves, []);
// Turn the frontier into a tree-like structure
const newTree = frontierToTree(timber);
// Add the sibling path to the frontier tree-like structure
const siblingTree = insertSiblingPath(newTree, siblingPath, leafIndex, leafValue);
// Add all the new incoming leaves to this tree-like structure
const finalTree = leavesInsertOrder.reduce(
(acc, curr) => acc.insertLeaves(curr),
new Timber(timber.root, timber.frontier, timber.leafCount, siblingTree),
);
// Get the sibling path for leafIndex from this tree.
return finalTree.getSiblingPath(leafValue, leafIndex);
}
}

export default Timber;

0 comments on commit 3e4ef59

Please sign in to comment.