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

IMT optimization #154

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c2eae4f
remove length check to avoid storage read
NouDaimon Oct 23, 2022
4a35971
add natspec
NouDaimon Oct 23, 2022
5dcf49a
store IMT hashes via assembly
ItsNickBarry Oct 23, 2022
4c5850b
rename rowSize to rowLength, modify lt comparison for clarity
ItsNickBarry Oct 23, 2022
af581c5
remove rootIndex variable
ItsNickBarry Oct 23, 2022
045c652
optimize rowLength calculation
ItsNickBarry Oct 23, 2022
dfebf46
optimize IMT size function
ItsNickBarry Oct 23, 2022
cc90a1d
wrap contents of IMT pop function in unchecked block
ItsNickBarry Oct 24, 2022
ba217cd
remove unused unchecked block
ItsNickBarry Oct 24, 2022
3fdae95
do not recalculate hashes if layer is removed on pop
ItsNickBarry Oct 24, 2022
bff831e
emphasize prallel structure of push and pop functions
ItsNickBarry Oct 24, 2022
761ce87
optimize recursive row length calculation
ItsNickBarry Oct 24, 2022
24b4834
replace lt with not eq comparison
ItsNickBarry Oct 24, 2022
886ba4b
add additional at function revert test
ItsNickBarry Oct 24, 2022
e3116c3
use assembly for height and root functions
ItsNickBarry Oct 24, 2022
511153c
circumvent all Solididity safety features for underlying IMT arrays
ItsNickBarry Oct 24, 2022
ad3f47d
wrap root functionality in assembly block
NouDaimon Oct 24, 2022
d455bb9
remove unrequired mstore operation
NouDaimon Oct 24, 2022
d9c9175
Merge branch 'master' into IMT-optimization
ItsNickBarry Nov 3, 2022
e8d8ebc
Merge branch 'IMT-optimization' into IMT-optimization-extreme
ItsNickBarry Nov 3, 2022
ea628a8
Merge branch 'master' into IMT-optimization
ItsNickBarry Dec 1, 2022
ba3c5ba
Merge branch 'IMT-optimization' into IMT-optimization-extreme
ItsNickBarry Dec 1, 2022
5340f02
Merge branch 'master' into IMT-optimization
ItsNickBarry Dec 14, 2022
a594007
Merge branch 'IMT-optimization' into IMT-optimization-extreme
ItsNickBarry Dec 14, 2022
4f204a3
Merge branch 'master' into IMT-optimization
ItsNickBarry Apr 17, 2023
610d22b
Merge branch 'IMT-optimization' into IMT-optimization-extreme
ItsNickBarry Apr 17, 2023
9ddd1fd
Merge branch 'master' into IMT-optimization
ItsNickBarry Feb 22, 2025
0e2d727
Merge branch 'IMT-optimization' into IMT-optimization-extreme
ItsNickBarry Feb 22, 2025
b0adf4d
Merge branch 'master' into IMT-optimization
ItsNickBarry Feb 24, 2025
6535209
Merge branch 'IMT-optimization' into IMT-optimization-extreme
ItsNickBarry Feb 24, 2025
ece54c2
Merge branch 'master' into IMT-optimization
ItsNickBarry Mar 22, 2025
9131b4f
Merge branch 'IMT-optimization' into IMT-optimization-extreme
ItsNickBarry Mar 22, 2025
1dedd3b
add comments explaining some IMT assembly blocks
ItsNickBarry Mar 22, 2025
fbc5115
panic via assembly rather than inline invalid array indexing
ItsNickBarry Mar 22, 2025
4f1e391
Merge pull request #155 from solidstate-network/IMT-optimization-extreme
ItsNickBarry Mar 22, 2025
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
187 changes: 125 additions & 62 deletions contracts/data/IncrementalMerkleTree.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ library IncrementalMerkleTree {
using IncrementalMerkleTree for Tree;

struct Tree {
bytes32[][] nodes;
bytes32[][] __nodes;
}

/**
Expand All @@ -15,19 +15,30 @@ library IncrementalMerkleTree {
* @return treeSize size of tree
*/
function size(Tree storage t) internal view returns (uint256 treeSize) {
if (t.height() > 0) {
treeSize = t.nodes[0].length;
assembly {
// assembly block equivalent to:
//
// if (t.height() > 0) treeSize = t.__nodes[0].length;

mstore(0x00, t.slot)
treeSize := sload(keccak256(0x00, 0x20))
}
}

/**
* @notice query one-indexed height of tree
* @dev conventional zero-indexed height would require the use of signed integers, so height is one-indexed instead
* @param t Tree struct storage reference
* @return one-indexed height of tree
* @return treeHeight one-indexed height of tree
*/
function height(Tree storage t) internal view returns (uint256) {
return t.nodes.length;
function height(Tree storage t) internal view returns (uint256 treeHeight) {
assembly {
// assembly block equivalent to:
//
// treeHeight = t.__nodes.length;

treeHeight := sload(t.slot)
}
}

/**
Expand All @@ -36,11 +47,16 @@ library IncrementalMerkleTree {
* @return hash root hash
*/
function root(Tree storage t) internal view returns (bytes32 hash) {
uint256 treeHeight = t.height();

if (treeHeight > 0) {
unchecked {
hash = t.nodes[treeHeight - 1][0];
assembly {
// assembly block equivalent to:
//
// if (t.height() > 0) hash = t.__nodes[t.height() - 1][0];

let treeHeight := sload(t.slot)
if gt(treeHeight, 0) {
mstore(0x00, t.slot)
mstore(0x00, add(keccak256(0x00, 0x20), sub(treeHeight, 1)))
hash := sload(keccak256(0x00, 0x20))
}
}
}
Expand All @@ -49,7 +65,23 @@ library IncrementalMerkleTree {
Tree storage t,
uint256 index
) internal view returns (bytes32 hash) {
hash = t.nodes[0][index];
if (index >= t.size()) {
assembly {
mstore(0x00, 0x4e487b71)
mstore(0x20, 0x32)
revert(0x1c, 0x24)
}
}

assembly {
// assembly block equivalent to:
//
// hash = t.__nodes[0][index];

mstore(0x00, t.slot)
mstore(0x00, keccak256(0x00, 0x20))
hash := sload(add(keccak256(0x00, 0x20), index))
}
}

/**
Expand All @@ -58,60 +90,67 @@ library IncrementalMerkleTree {
* @param hash to add
*/
function push(Tree storage t, bytes32 hash) internal {
unchecked {
uint256 treeHeight = t.height();
uint256 treeSize = t.size();
// index to add to tree
uint256 updateIndex = t.size();

// add new layer if tree is at capacity
// update stored tree size

if (treeSize == (1 << treeHeight) >> 1) {
t.nodes.push();
treeHeight++;
}
assembly {
mstore(0x00, t.slot)
sstore(keccak256(0x00, 0x20), add(updateIndex, 1))
}

// add new columns if rows are full
// add new layer if tree is at capacity

uint256 row;
uint256 col = treeSize;
uint256 treeHeight = t.height();

while (row < treeHeight && t.nodes[row].length <= col) {
t.nodes[row].push();
row++;
col >>= 1;
if (updateIndex == (1 << treeHeight) >> 1) {
// increment tree height in storage
assembly {
sstore(t.slot, add(treeHeight, 1))
}
}

// add hash to tree
// add hash to tree

t.set(treeSize, hash);
}
t.set(updateIndex, hash);
}

function pop(Tree storage t) internal {
uint256 treeHeight = t.height();
uint256 treeSize = t.size() - 1;

// remove layer if tree has excess capacity
uint256 treeSize = t.size();

if (treeSize == (1 << treeHeight) >> 2) {
treeHeight--;
t.nodes.pop();
if (treeSize == 0) {
assembly {
mstore(0x00, 0x4e487b71)
mstore(0x20, 0x32)
revert(0x1c, 0x24)
}
}

// remove columns if rows are too long
unchecked {
// index to remove from tree
uint256 updateIndex = treeSize - 1;

// update stored tree size

uint256 row;
uint256 col = treeSize;
assembly {
mstore(0x00, t.slot)
sstore(keccak256(0x00, 0x20), updateIndex)
}

while (row < treeHeight && t.nodes[row].length > col) {
t.nodes[row].pop();
row++;
col = (col + 1) >> 1;
}
// if new tree is full, remove excess layer
// if no layer is removed, recalculate hashes

// recalculate hashes
uint256 treeHeight = t.height();

if (treeSize > 0) {
t.set(treeSize - 1, t.at(treeSize - 1));
if (updateIndex == (1 << treeHeight) >> 2) {
// decrement tree height in storage
assembly {
sstore(t.slot, sub(treeHeight, 1))
}
} else {
t.set(updateIndex - 1, t.at(updateIndex - 1));
}
}
}

Expand All @@ -122,48 +161,72 @@ library IncrementalMerkleTree {
* @param hash new hash to add
*/
function set(Tree storage t, uint256 index, bytes32 hash) internal {
unchecked {
_set(t.nodes, 0, index, t.height() - 1, hash);
uint256 treeSize = t.size();

if (index >= treeSize) {
assembly {
mstore(0x00, 0x4e487b71)
mstore(0x20, 0x32)
revert(0x1c, 0x24)
}
}

_set(t, 0, index, treeSize, hash);
}

/**
* @notice update element in tree and recursively recalculate hashes
* @param nodes internal tree structure storage reference
* @param t Tree struct storage reference
* @param rowIndex index of current row to update
* @param colIndex index of current column to update
* @param rootIndex index of root row
* @param rowLength length of row at rowIndex
* @param hash hash to store at current position
*/
function _set(
bytes32[][] storage nodes,
Tree storage t,
uint256 rowIndex,
uint256 colIndex,
uint256 rootIndex,
uint256 rowLength,
bytes32 hash
) private {
bytes32[] storage row = nodes[rowIndex];
bytes32[] storage row;

assembly {
// assembly block equivalent to:
//
// row = nodes[rowIndex];

mstore(0x00, t.slot)
row.slot := add(keccak256(0x00, 0x20), rowIndex)
}

row[colIndex] = hash;
// store hash in array via assembly to avoid array length sload

if (rowIndex == rootIndex) return;
assembly {
// assembly block equivalent to:
//
// row[colIndex] = hash;

mstore(0x00, row.slot)
sstore(add(keccak256(0x00, 0x20), colIndex), hash)
}

if (rowLength == 1) return;

unchecked {
if (colIndex & 1 == 1) {
// sibling is on the left
assembly {
mstore(0x00, row.slot)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

questionable

let sibling := sload(
add(keccak256(0x00, 0x20), sub(colIndex, 1))
)
mstore(0x00, sibling)
mstore(0x20, hash)
hash := keccak256(0x00, 0x40)
}
} else if (colIndex + 1 < row.length) {
} else if (colIndex < rowLength - 1) {
// sibling is on the right (and sibling exists)
assembly {
mstore(0x00, row.slot)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

questionable

let sibling := sload(
add(keccak256(0x00, 0x20), add(colIndex, 1))
)
Expand All @@ -172,8 +235,8 @@ library IncrementalMerkleTree {
hash := keccak256(0x00, 0x40)
}
}

_set(nodes, rowIndex + 1, colIndex >> 1, rootIndex, hash);
}

_set(t, rowIndex + 1, colIndex >> 1, (rowLength + 1) >> 1, hash);
}
}
12 changes: 10 additions & 2 deletions test/data/IncrementalMerkleTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,19 @@ describe('IncrementalMerkleTree', () => {
});

describe('reverts if', () => {
it('index is out of bounds', async () => {
it('tree is size zero', async () => {
await expect(
instance.$at.staticCall(STORAGE_SLOT, 0),
).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});

it('index is out of bounds', async () => {
await instance.$push(STORAGE_SLOT, randomHash());

await expect(
instance.$at.staticCall(STORAGE_SLOT, 1),
).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});
});
});

Expand Down Expand Up @@ -196,7 +204,7 @@ describe('IncrementalMerkleTree', () => {
describe('reverts if', () => {
it('tree is size zero', async () => {
await expect(instance.$pop(STORAGE_SLOT)).to.be.revertedWithPanic(
PANIC_CODES.ARITHMETIC_OVERFLOW,
PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS,
);
});
});
Expand Down