Skip to content

Commit

Permalink
feat: priority tree on sync layer (#562)
Browse files Browse the repository at this point in the history
Co-authored-by: Lyova Potyomkin <[email protected]>
Co-authored-by: Raid Ateir <[email protected]>
  • Loading branch information
3 people authored Jun 29, 2024
1 parent ad522d1 commit 8c0b80c
Show file tree
Hide file tree
Showing 44 changed files with 2,209 additions and 881 deletions.
2 changes: 1 addition & 1 deletion l1-contracts/contracts/bridgehub/MessageRoot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity 0.8.24;

// solhint-disable reason-string, gas-custom-errors

import {DynamicIncrementalMerkle} from "../common/libraries/openzeppelin/IncrementalMerkle.sol"; // todo figure out how to import from OZ
import {DynamicIncrementalMerkle} from "../common/libraries/DynamicIncrementalMerkle.sol";

import {IBridgehub} from "./IBridgehub.sol";
import {IMessageRoot} from "./IMessageRoot.sol";
Expand Down
15 changes: 9 additions & 6 deletions l1-contracts/contracts/common/Config.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

pragma solidity 0.8.24;

import {PriorityOperation} from "../state-transition/libraries/PriorityQueue.sol";

/// @dev `keccak256("")`
bytes32 constant EMPTY_STRING_KECCAK = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470;

Expand Down Expand Up @@ -120,6 +118,13 @@ struct StoredBatchHashInfo {
bytes32 hash;
}

struct PriorityTreeCommitment {
uint256 nextLeafIndex;
uint256 startIndex;
uint256 unprocessedIndex;
bytes32[] sides;
}

// Info that allows to restore a chain.
struct HyperchainCommitment {
/// @notice Total number of executed batches i.e. batches[totalBatchesExecuted] points at the latest executed batch
Expand All @@ -130,13 +135,11 @@ struct HyperchainCommitment {
/// @notice Total number of committed batches i.e. batches[totalBatchesCommitted] points at the latest committed
/// batch
uint256 totalBatchesCommitted;
/// @notice total number of priority txs ever executed
uint256 priorityQueueHead;
PriorityOperation[] priorityQueueTxs;
/// @notice
bytes32 l2SystemContractsUpgradeTxHash;
/// @notice
uint256 l2SystemContractsUpgradeBatchNumber;
bytes32[] batchHashes;
/// TODO: add priority queue here.
/// @notice Commitment to the priority merkle tree
PriorityTreeCommitment priorityTree;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

pragma solidity ^0.8.0;

import {Hashes} from "./Hashes.sol";
import {Arrays} from "./Arrays.sol";
import {Merkle} from "./Merkle.sol";
import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol";

/**
* @dev Library for managing https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures.
Expand All @@ -18,17 +18,15 @@ import {Arrays} from "./Arrays.sol";
* * Zero value: The value that represents an empty leaf. Used to avoid regular zero values to be part of the tree.
* * Hashing function: A cryptographic hash function used to produce internal nodes.
*
* _Available since v5.1._
* This is a fork of OpenZeppelin's [`MerkleTree`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/9af280dc4b45ee5bda96ba47ff829b407eaab67e/contracts/utils/structs/MerkleTree.sol)
* library, with the changes to support dynamic tree growth (doubling the size when full).
*/
library DynamicIncrementalMerkle {
/**
* @dev A complete `bytes32` Merkle tree.
*
* The `sides` and `zero` arrays are set to have a length equal to the depth of the tree during setup.
*
* The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth
* for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions.
*
* Struct members have an underscore prefix indicating that they are "private" and should not be read or written to
* directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and
* lead to unexpected behavior.
Expand All @@ -41,10 +39,8 @@ library DynamicIncrementalMerkle {
*/
struct Bytes32PushTree {
uint256 _nextLeafIndex;
uint256 _height;
bytes32[] _sides;
bytes32[] _zeros;
function(bytes32, bytes32) view returns (bytes32) _fnHash;
}

/**
Expand All @@ -57,30 +53,10 @@ library DynamicIncrementalMerkle {
* empty leaves. It should be a value that is not expected to be part of the tree.
*/
function setup(Bytes32PushTree storage self, bytes32 zero) internal returns (bytes32 initialRoot) {
return setup(self, zero, Hashes.Keccak256);
}

/**
* @dev Same as {setup}, but allows to specify a custom hashing function.
*
* IMPORTANT: Providing a custom hashing function is a security-sensitive operation since it may
* compromise the soundness of the tree. Consider using functions from {Hashes}.
*/
function setup(
Bytes32PushTree storage self,
bytes32 zero,
function(bytes32, bytes32) view returns (bytes32) fnHash
) internal returns (bytes32 initialRoot) {
// Store depth in the dynamic array

Arrays.unsafeAccess(self._zeros, 0).value = zero;

// Set the first root
self._nextLeafIndex = 0;
self._fnHash = fnHash;
self._height = 0;

return zero;
self._zeros.push(zero);
self._sides.push(bytes32(0));
return bytes32(0);
}

/**
Expand All @@ -92,54 +68,61 @@ library DynamicIncrementalMerkle {
*/
function push(Bytes32PushTree storage self, bytes32 leaf) internal returns (uint256 index, bytes32 newRoot) {
// Cache read
uint256 levels = self._zeros.length;
function(bytes32, bytes32) view returns (bytes32) fnHash = self._fnHash;
uint256 levels = self._zeros.length - 1;

// Get leaf index
// solhint-disable-next-line gas-increment-by-one
index = self._nextLeafIndex++;

// Check if tree is full.
if (index == 1 << levels) {
// Tree is full, reset index add new level
bytes32 currentMaxZero = Arrays.unsafeAccess(self._zeros, levels).value;
++self._height;
bytes32 zero = self._zeros[levels];
bytes32 newZero = Merkle.efficientHash(zero, zero);
self._zeros.push(newZero);
self._sides.push(bytes32(0));
++levels;
Arrays.unsafeAccess(self._zeros, levels).value = fnHash(currentMaxZero, currentMaxZero);
Arrays.unsafeSetLength(self._sides, levels);
Arrays.unsafeSetLength(self._zeros, levels);
}

// Rebuild branch from leaf to root
uint256 currentIndex = index;
bytes32 currentLevelHash = leaf;
bool updatedSides = false;
for (uint32 i = 0; i < levels; ++i) {
// Reaching the parent node, is currentLevelHash the left child?
bool isLeft = currentIndex % 2 == 0;

// If so, next time we will come from the right, so we need to save it
if (isLeft) {
if (isLeft && !updatedSides) {
Arrays.unsafeAccess(self._sides, i).value = currentLevelHash;
updatedSides = true;
}

// Compute the current node hash by using the hash function
// with either the its sibling (side) or the zero value for that level.
currentLevelHash = fnHash(
// with either its sibling (side) or the zero value for that level.
currentLevelHash = Merkle.efficientHash(
isLeft ? currentLevelHash : Arrays.unsafeAccess(self._sides, i).value,
isLeft ? Arrays.unsafeAccess(self._zeros, i).value : currentLevelHash
);

// Update node index
currentIndex >>= 1;
}

Arrays.unsafeAccess(self._sides, levels).value = currentLevelHash;
return (index, currentLevelHash);
}

/**
* @dev Tree's root.
*/

function root(Bytes32PushTree storage self) internal view returns (bytes32) {
return Arrays.unsafeAccess(self._sides, self._height).value;
return Arrays.unsafeAccess(self._sides, self._sides.length - 1).value;
}

/**
* @dev Tree's height (does not include the root node).
*/
function height(Bytes32PushTree storage self) internal view returns (uint256) {
return self._sides.length - 1;
}
}
17 changes: 7 additions & 10 deletions l1-contracts/contracts/common/libraries/FullMerkle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ pragma solidity 0.8.24;
// solhint-disable reason-string, gas-custom-errors

import {UncheckedMath} from "../../common/libraries/UncheckedMath.sol";
// This importing issue should not be pushed to main, we only need this while we did not import OZ-v5 and v4 in parallel. Once we merge that it will be removed
import {Arrays} from "./openzeppelin/Arrays.sol";
import {Hashes} from "./openzeppelin/Hashes.sol";
import {Merkle} from "./Merkle.sol";

/// @author Matter Labs
/// @custom:security-contact [email protected]
Expand All @@ -22,7 +20,7 @@ library FullMerkle {
}

/**
* @dev Initialize a {Bytes32PushTree} using {Hashes-Keccak256} to hash internal nodes.
* @dev Initialize a {Bytes32PushTree} using {Merkle.efficientHash} to hash internal nodes.
* The capacity of the tree (i.e. number of leaves) is set to `2**levels`.
*
* Calling this function on MerkleTree that was already setup and used will reset it to a blank state.
Expand All @@ -32,8 +30,7 @@ library FullMerkle {
*/
function setup(FullTree storage self, bytes32 zero) internal returns (bytes32 initialRoot) {
// Store depth in the dynamic array
Arrays.unsafeSetLength(self._zeros, 1);
Arrays.unsafeAccess(self._zeros, 0).value = zero;
self._zeros.push(zero);
self._nodes.push([zero]);

return zero;
Expand All @@ -47,7 +44,7 @@ library FullMerkle {
uint256 newHeight = self._height.uncheckedInc();
self._height = newHeight;
bytes32 topZero = self._zeros[newHeight - 1];
bytes32 newZero = Hashes.Keccak256(topZero, topZero);
bytes32 newZero = Merkle.efficientHash(topZero, topZero);
self._zeros.push(newZero);
self._nodes.push([newZero]);
}
Expand All @@ -74,12 +71,12 @@ library FullMerkle {
bytes32 currentHash = _itemHash;
for (uint256 i; i < self._height; i = i.uncheckedInc()) {
if (_index % 2 == 0) {
currentHash = Hashes.Keccak256(
currentHash = Merkle.efficientHash(
currentHash,
maxNodeNumber == _index ? self._zeros[i] : self._nodes[i][_index + 1]
);
} else {
currentHash = Hashes.Keccak256(self._nodes[i][_index - 1], currentHash);
currentHash = Merkle.efficientHash(self._nodes[i][_index - 1], currentHash);
}
_index /= 2;
maxNodeNumber /= 2;
Expand Down Expand Up @@ -108,7 +105,7 @@ library FullMerkle {
for (uint256 i; i < length; i = i.uncheckedAdd(2)) {
self._nodes[_height][i] = _newNodes[i];
self._nodes[_height][i + 1] = _newNodes[i + 1];
_newRow[i / 2] = Hashes.Keccak256(_newNodes[i], _newNodes[i + 1]);
_newRow[i / 2] = Merkle.efficientHash(_newNodes[i], _newNodes[i + 1]);
}
return updateAllNodesAtHeight(self, _height + 1, _newRow);
}
Expand Down
48 changes: 45 additions & 3 deletions l1-contracts/contracts/common/libraries/Merkle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,58 @@ library Merkle {
bytes32 currentHash = _itemHash;
for (uint256 i; i < pathLength; i = i.uncheckedInc()) {
currentHash = (_index % 2 == 0)
? _efficientHash(currentHash, _path[i])
: _efficientHash(_path[i], currentHash);
? efficientHash(currentHash, _path[i])
: efficientHash(_path[i], currentHash);
_index /= 2;
}

return currentHash;
}

/// @dev Calculate Merkle root by the provided Merkle proof for a range of elements
/// NOTE: When using this function, check that the _startPath and _endPath lengths are equal to the tree height to prevent shorter/longer paths attack
/// @param _startPath Merkle path from the first element of the range to the root
/// @param _endPath Merkle path from the last element of the range to the root
/// @param _startIndex Index of the first element of the range in the tree
/// @param _itemHashes Hashes of the elements in the range
/// @return The Merkle root
function calculateRoot(
bytes32[] calldata _startPath,
bytes32[] calldata _endPath,
uint256 _startIndex,
bytes32[] calldata _itemHashes
) internal pure returns (bytes32) {
uint256 pathLength = _startPath.length;
require(pathLength == _endPath.length, "Merkle: path length mismatch");
require(pathLength < 256, "Merkle: path too long");
uint256 levelLen = _itemHashes.length;
// Edge case: we want to be able to prove an element in a single-node tree.
require(pathLength > 0 || (_startIndex == 0 && levelLen == 1), "Merkle: empty paths");
require(levelLen > 0, "Merkle: nothing to prove");
require(_startIndex + levelLen <= (1 << pathLength), "Merkle: index/height mismatch");
bytes32[] memory itemHashes = _itemHashes;

for (uint256 level; level < pathLength; level = level.uncheckedInc()) {
uint256 parity = _startIndex % 2;
// We get an extra element on the next level if on the current level elements either
// start on an odd index (`parity == 1`) or end on an even index (`levelLen % 2 == 1`)
uint256 nextLevelLen = levelLen / 2 + (parity | (levelLen % 2));
for (uint256 i; i < nextLevelLen; i = i.uncheckedInc()) {
bytes32 lhs = (i == 0 && parity == 1) ? _startPath[level] : itemHashes[2 * i - parity];
bytes32 rhs = (i == nextLevelLen - 1 && (levelLen - parity) % 2 == 1)
? _endPath[level]
: itemHashes[2 * i + 1 - parity];
itemHashes[i] = efficientHash(lhs, rhs);
}
levelLen = nextLevelLen;
_startIndex /= 2;
}

return itemHashes[0];
}

/// @dev Keccak hash of the concatenation of two 32-byte words
function _efficientHash(bytes32 _lhs, bytes32 _rhs) private pure returns (bytes32 result) {
function efficientHash(bytes32 _lhs, bytes32 _rhs) internal pure returns (bytes32 result) {
assembly {
mstore(0x00, _lhs)
mstore(0x20, _rhs)
Expand Down
Loading

0 comments on commit 8c0b80c

Please sign in to comment.