Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timber updates sibling paths using incoming leaves. #584

Merged
merged 5 commits into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;