Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
18 changes: 13 additions & 5 deletions l1-contracts/src/core/Rollup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {HeaderLib} from "./libraries/HeaderLib.sol";
import {Hash} from "./libraries/Hash.sol";
import {Errors} from "./libraries/Errors.sol";
import {Constants} from "./libraries/ConstantsGen.sol";
import {MerkleLib} from "./libraries/MerkleLib.sol";
import {EnumerableSet} from "@oz/utils/structs/EnumerableSet.sol";

// Contracts
Expand Down Expand Up @@ -149,12 +150,19 @@ contract Rollup is IRollup {
revert Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash);
}

// Currently trying out storing each tx's L2 to L1 messages in variable height trees (smallest tree required)
// => path lengths will differ and we cannot provide one here
// We can provide a minimum which is the height of the rollup layers (txTreeHeight) and the smallest 'tree' (1 layer)
uint256 l2ToL1TreeMinHeight = header.contentCommitment.txTreeHeight + 1;
// Both rollup tx trees and each tx's L2 to L1 messages are variable height.
// TODO: Is the below necessary? Is it fine to not supply any height?
// Min size = smallest path of the rollup tree + 1
// Max size = largest path of the rollup tree + log2(max_messages)
(uint256 min, uint256 max) = MerkleLib.computeMinMaxPathLength(header.contentCommitment.numTxs);
Comment thread
MirandaWood marked this conversation as resolved.
Outdated
uint256 l2ToL1TreeMinHeight = min + 1;
uint256 l2ToL1TreeMaxHeight =
max + MerkleLib.calculateTreeHeightFromSize(Constants.MAX_NEW_L2_TO_L1_MSGS_PER_TX);
OUTBOX.insert(
header.globalVariables.blockNumber, header.contentCommitment.outHash, l2ToL1TreeMinHeight
header.globalVariables.blockNumber,
header.contentCommitment.outHash,
l2ToL1TreeMinHeight,
l2ToL1TreeMaxHeight
);

// pay the coinbase 1 gas token if it is not empty and header.totalFees is not zero
Expand Down
8 changes: 6 additions & 2 deletions l1-contracts/src/core/interfaces/messagebridge/IOutbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {DataStructures} from "../../libraries/DataStructures.sol";
* and will be consumed by the portal contracts.
*/
interface IOutbox {
event RootAdded(uint256 indexed l2BlockNumber, bytes32 indexed root, uint256 height);
event RootAdded(
uint256 indexed l2BlockNumber, bytes32 indexed root, uint256 minHeight, uint256 maxHeight
);
event MessageConsumed(
uint256 indexed l2BlockNumber,
bytes32 indexed root,
Expand All @@ -28,8 +30,10 @@ interface IOutbox {
* @param _l2BlockNumber - The L2 Block Number in which the L2 to L1 messages reside
* @param _root - The merkle root of the tree where all the L2 to L1 messages are leaves
* @param _minHeight - The min height of the merkle tree that the root corresponds to
* @param _maxHeight - The max height of the merkle tree that the root corresponds to
*/
function insert(uint256 _l2BlockNumber, bytes32 _root, uint256 _minHeight) external;
function insert(uint256 _l2BlockNumber, bytes32 _root, uint256 _minHeight, uint256 _maxHeight)
external;
// docs:end:outbox_insert

// docs:start:outbox_consume
Expand Down
3 changes: 2 additions & 1 deletion l1-contracts/src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ library Errors {
error Rollup__TimestampInFuture(); // 0xbc1ce916
error Rollup__TimestampTooOld(); // 0x72ed9c81
error Rollup__UnavailableTxs(bytes32 txsHash); // 0x414906c3
error Rollup__InvalidSequencer(address sequencer);
error Rollup__InvalidSequencer(address sequencer); // 0xa127a106

// Registry
error Registry__RollupNotRegistered(address rollup); // 0xa1fee4cf
error Registry__RollupAlreadyRegistered(address rollup); // 0x3c34eabf

//TxsDecoder
error TxsDecoder__InvalidLogsLength(uint256 expected, uint256 actual); // 0x829ca981
error TxsDecoder__TxsTooLarge(uint256 expected, uint256 actual); // 0xc7d44a62

// HeaderLib
error HeaderLib__InvalidHeaderSize(uint256 expected, uint256 actual); // 0xf3ccb247
Expand Down
8 changes: 4 additions & 4 deletions l1-contracts/src/core/libraries/HeaderLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {Hash} from "./Hash.sol";
* | 0x0000 | 0x20 | lastArchive.root
* | 0x0020 | 0x04 | lastArchive.nextAvailableLeafIndex
* | | | ContentCommitment {
* | 0x0024 | 0x20 | txTreeHeight
* | 0x0024 | 0x20 | numTxs
* | 0x0044 | 0x20 | txsEffectsHash
* | 0x0064 | 0x20 | inHash
* | 0x0084 | 0x20 | outHash
Expand Down Expand Up @@ -90,7 +90,7 @@ library HeaderLib {
}

struct ContentCommitment {
uint256 txTreeHeight;
uint256 numTxs;
bytes32 txsEffectsHash;
bytes32 inHash;
bytes32 outHash;
Expand Down Expand Up @@ -163,7 +163,7 @@ library HeaderLib {
);

// Reading ContentCommitment
header.contentCommitment.txTreeHeight = uint256(bytes32(_header[0x0024:0x0044]));
header.contentCommitment.numTxs = uint256(bytes32(_header[0x0024:0x0044]));
header.contentCommitment.txsEffectsHash = bytes32(_header[0x0044:0x0064]);
header.contentCommitment.inHash = bytes32(_header[0x0064:0x0084]);
header.contentCommitment.outHash = bytes32(_header[0x0084:0x00a4]);
Expand Down Expand Up @@ -204,7 +204,7 @@ library HeaderLib {
// must match the order in the Header.getFields
fields[0] = _header.lastArchive.root;
fields[1] = bytes32(uint256(_header.lastArchive.nextAvailableLeafIndex));
fields[2] = bytes32(_header.contentCommitment.txTreeHeight);
fields[2] = bytes32(_header.contentCommitment.numTxs);
fields[3] = _header.contentCommitment.txsEffectsHash;
fields[4] = _header.contentCommitment.inHash;
fields[5] = _header.contentCommitment.outHash;
Expand Down
100 changes: 100 additions & 0 deletions l1-contracts/src/core/libraries/MerkleLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,104 @@ library MerkleLib {
revert Errors.MerkleLib__InvalidRoot(_expectedRoot, subtreeRoot, _leaf, _index);
}
}

/**
* @notice Decomposes a max 2 byte number into powers of 2 to create subtrees for an unbalanced tree.
* @dev Follows structure of rollup circuits - useful for txsEffectHash and outHash tree.
* @param _numTxs - The number of txs to form into subtrees.
* @return res - The subtree sizes in a byte array.
*/
function computeSubtreeSizes(uint256 _numTxs) internal pure returns (bytes memory) {
// TODO: maximum number of subtrees?
// Using 1 byte per subtree means each can only hold 255 leaves => cannot decompose a total num > 256
// => Currently using 2 bytes per subtree up to 16 subtrees => can store 2**16 - 1 = 65,535
// We could reduce the max num bytes from 32 to 30 => could store 15 trees holding max 65,534 leaves
// However the gas cost is the same as 32 bytes for this fn
if (_numTxs > 65535) {
revert Errors.TxsDecoder__TxsTooLarge(65535, _numTxs);
}

bytes memory res = new bytes(32);
uint256 x = _numTxs;
uint32 i = 0;
// We have already padded _numTxs to at least 2, so no need for 0/1 cases
while (x > 1) {
uint256 v = x;
// the following rounds v up to the next power of 2 (works only for 4 bytes value!)
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
v++;
// We find the prev power
uint256 prevPower = v == 2 ? 2 : v - (v >> 1);
bytes2 prevPowerBytes = bytes2(uint16(prevPower));
res[i++] = prevPowerBytes[0];
res[i++] = prevPowerBytes[1];
x -= prevPower;
}
// If _numTxs is an odd number, we are left with x = 1 and need to append a single base rollup leaf
if (x == 1) {
res[i++] = hex"00";
res[i++] = hex"01";
}
// Trim to length required so we can loop on the subtrees
bytes memory trimmed = new bytes(i);
for (uint256 j = 0; j < i; j++) {
trimmed[j] = res[j];
}

return trimmed;
}

/**
* @notice Computes the minimum and maximum path size of an unbalanced tree.
* @dev Follows structure of rollup circuits - calls above fn to find subtree sizes
* @param _numTxs - The number of txs to form into subtrees.
* @return (min, max) - The min and max path sizes.
*/
function computeMinMaxPathLength(uint256 _numTxs) internal pure returns (uint256, uint256) {
uint256 numTxs = _numTxs < 2 ? 2 : _numTxs;
bytes memory subtreeSizes = computeSubtreeSizes(numTxs);
// Each subtree is encoded into 2 bytes
uint256 numSubtrees = subtreeSizes.length / 2;
uint256 firstSubtreeSize = uint16(bytes2(bytes.concat(subtreeSizes[0], subtreeSizes[1])));
uint256 firstSubtreeHeight = calculateTreeHeightFromSize(firstSubtreeSize);
if (numSubtrees == 1) {
// We have a balanced tree
return (firstSubtreeHeight, firstSubtreeHeight);
}
uint256 finalSubtreeSize = uint16(
bytes2(
bytes.concat(subtreeSizes[subtreeSizes.length - 2], subtreeSizes[subtreeSizes.length - 1])
)
);
uint256 min = calculateTreeHeightFromSize(finalSubtreeSize) + numSubtrees - 1;
uint256 max = firstSubtreeHeight + 1;

return (min, max);
}

/**
* @notice Calculates a tree height from the amount of elements in the tree
* @dev - This mirrors the function in TestUtil, but assumes _size is an exact power of 2 or = 1
* @param _size - The number of elements in the tree
*/
function calculateTreeHeightFromSize(uint256 _size) internal pure returns (uint256) {
/// We need the height of the tree that will contain all of our leaves,
/// hence the next highest power of two from the amount of leaves - Math.ceil(Math.log2(x))
uint256 height = 0;

if (_size == 1) {
return 0;
}

/// While size > 1, we divide by two, and count how many times we do this; producing a rudimentary way of calculating Math.Floor(Math.log2(x))
while (_size > 1) {
_size >>= 1;
height++;
}
return height;
}
}
47 changes: 33 additions & 14 deletions l1-contracts/src/core/libraries/decoders/TxsDecoder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pragma solidity >=0.8.18;
import {Errors} from "../Errors.sol";
import {Constants} from "../ConstantsGen.sol";
import {Hash} from "../Hash.sol";
import {MerkleLib} from "../MerkleLib.sol";

/**
* @title Txs Decoder Library
Expand Down Expand Up @@ -261,7 +262,7 @@ library TxsDecoder {
}
}

return computeRoot(vars.baseLeaves);
return computeUnbalancedRoot(vars.baseLeaves);
}

/**
Expand Down Expand Up @@ -516,6 +517,36 @@ library TxsDecoder {
return _leafs[0];
}

/**
* @notice Computes the root for a binary unbalanced Merkle-tree given the leaves and subtree sizes.
* @dev Filled in greedily with subtrees. Useful for txsEffectHash and outHash tree.
* @param _leaves - The 32 bytes leafs to build the tree of.
* @return The root of the Merkle tree.
*/
function computeUnbalancedRoot(bytes32[] memory _leaves) internal pure returns (bytes32) {
// e.g. an unbalanced tree of 7 txs will contain subtrees of 4, 2, and 1 tx(s)
// Encoded as 000400020001
bytes memory subtreeSizes = MerkleLib.computeSubtreeSizes(_leaves.length);
// We collect the roots of each subtree
bytes32 root;
uint256 currentSubtreeSize;
uint256 processedLeaves = 0;
// We must calculate the smaller rightmost subtrees first, hence working backwards through the leaves/sizes arr
for (uint256 j = subtreeSizes.length; j > 0; j -= 2) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like the subtreeSizes are simply the non-zero bits of the binary form of numTxs.
Consider doing the following so that we don't have to allocate some memory for the sizes:

uint256 currentSubtreeSize = 1;
uint256 numTxs = _leaves.length;
while (currentSubtreeSize <= numTxs) {
  if (!(currentSubtreeSize & numTxs)) {
    continue;
  }
  // Do the thing.
  currentSubtreeSize <<= 1;
}

(☝️ Might contain bug and incorrect syntax)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh of course!! Thanks, great catch. Will also remove the subtree size fn and refactor min/max path length (the latter will disappear once we move to a balanced out_hash tree, so I won't spend too much time on it)

currentSubtreeSize = uint16(bytes2(bytes.concat(subtreeSizes[j - 2], subtreeSizes[j - 1])));
uint256 start = _leaves.length - processedLeaves - currentSubtreeSize;
bytes32[] memory leavesInSubtree = new bytes32[](currentSubtreeSize);
for (uint256 i = start; i < start + currentSubtreeSize; i++) {
leavesInSubtree[i - start] = _leaves[i];
}
bytes32 subtreeRoot = computeRoot(leavesInSubtree);
root =
j == subtreeSizes.length ? subtreeRoot : Hash.sha256ToField(bytes.concat(subtreeRoot, root));
processedLeaves += currentSubtreeSize;
}
return root;
}

/**
* @notice Wrapper around the slicing to avoid some stack too deep
* @param _data - The data to slice
Expand Down Expand Up @@ -592,18 +623,6 @@ library TxsDecoder {
} else if (_numTxEffects == 1) {
return 1;
}

uint32 v = _numTxEffects;

// the following rounds _numTxEffects up to the next power of 2 (works only for 4 bytes value!)
v--;
v |= v >> 1;
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
v++;

return v - _numTxEffects;
return 0;
}
}
40 changes: 12 additions & 28 deletions l1-contracts/src/core/messagebridge/Outbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ contract Outbox is IOutbox {
// This is the outhash specified by header.globalvariables.outHash of any given block.
bytes32 root;
uint256 minHeight;
uint256 maxHeight;
mapping(uint256 => bool) nullified;
}

Expand All @@ -41,8 +42,9 @@ contract Outbox is IOutbox {
* @param _l2BlockNumber - The L2 Block Number in which the L2 to L1 messages reside
* @param _root - The merkle root of the tree where all the L2 to L1 messages are leaves
* @param _minHeight - The min height of the merkle tree that the root corresponds to
* @param _maxHeight - The max height of the merkle tree that the root corresponds to
*/
function insert(uint256 _l2BlockNumber, bytes32 _root, uint256 _minHeight)
function insert(uint256 _l2BlockNumber, bytes32 _root, uint256 _minHeight, uint256 _maxHeight)
external
override(IOutbox)
{
Expand All @@ -60,8 +62,9 @@ contract Outbox is IOutbox {

roots[_l2BlockNumber].root = _root;
roots[_l2BlockNumber].minHeight = _minHeight;
roots[_l2BlockNumber].maxHeight = _maxHeight;

emit RootAdded(_l2BlockNumber, _root, _minHeight);
emit RootAdded(_l2BlockNumber, _root, _minHeight, _maxHeight);
}

/**
Expand Down Expand Up @@ -103,16 +106,15 @@ contract Outbox is IOutbox {

// Min height = height of rollup layers
// The smallest num of messages will require a subtree of height 1
uint256 treeHeight = rootData.minHeight;
if (treeHeight > _path.length) {
revert Errors.Outbox__InvalidPathLength(treeHeight, _path.length);
uint256 minHeight = rootData.minHeight;
if (minHeight > _path.length) {
revert Errors.Outbox__InvalidPathLength(minHeight, _path.length);
}

// Max height = height of rollup layers + max possible subtree height
// The max num of messages N will require a subtree of height log2(N)
uint256 maxSubtreeHeight = calculateTreeHeightFromSize(Constants.MAX_NEW_L2_TO_L1_MSGS_PER_TX);
if (treeHeight + maxSubtreeHeight < _path.length) {
revert Errors.Outbox__InvalidPathLength(treeHeight + maxSubtreeHeight, _path.length);
// Max height = max height of rollup layers + max possible subtree height
uint256 maxHeight = rootData.maxHeight;
if (maxHeight < _path.length) {
revert Errors.Outbox__InvalidPathLength(maxHeight, _path.length);
}

bytes32 messageHash = _message.sha256ToField();
Expand All @@ -138,22 +140,4 @@ contract Outbox is IOutbox {
{
return roots[_l2BlockNumber].nullified[_leafIndex];
}

/**
* @notice Calculates a tree height from the amount of elements in the tree
* @dev - This mirrors the function in TestUtil, but assumes _size is an exact power of 2
* @param _size - The number of elements in the tree
*/
function calculateTreeHeightFromSize(uint256 _size) internal pure returns (uint256) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved to MerkleLib.sol as it's useful for other contracts as well.

/// We need the height of the tree that will contain all of our leaves,
/// hence the next highest power of two from the amount of leaves - Math.ceil(Math.log2(x))
uint256 height = 0;

/// While size > 1, we divide by two, and count how many times we do this; producing a rudimentary way of calculating Math.Floor(Math.log2(x))
while (_size > 1) {
_size >>= 1;
height++;
}
return height;
}
}
Loading