Skip to content
Merged
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
145 changes: 89 additions & 56 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { SerialQueue } from '@aztec/foundation/queue';
import { count } from '@aztec/foundation/string';
import { DateProvider, Timer } from '@aztec/foundation/timer';
import { SiblingPath } from '@aztec/foundation/trees';
import type { AztecKVStore } from '@aztec/kv-store';
import { openTmpStore } from '@aztec/kv-store/lmdb';
import { RollupAbi } from '@aztec/l1-artifacts';
import { SHA256Trunc, StandardTree, UnbalancedTree } from '@aztec/merkle-tree';
Expand Down Expand Up @@ -699,14 +698,20 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
}

/**
* Returns the index of a l2ToL1Message in a ephemeral l2 to l1 data tree as well as its sibling path.
* @remarks This tree is considered ephemeral because it is created on-demand by: taking all the l2ToL1 messages
* in a single block, and then using them to make a variable depth append-only tree with these messages as leaves.
* The tree is discarded immediately after calculating what we need from it.
* Returns the index and sibling path for a L2->L1 message in a block's message tree.
* @remarks The message tree is constructed on-demand by taking all L2->L1 messages in a block
* and creating a variable depth append-only tree with the messages as leaves.
* The tree is constructed in two layers:
* 1. Subtree - For each transaction in the block, a subtree is created containing all L2->L1 messages from that
* transaction.
* 2. Top tree - A tree containing the roots of all the subtrees as leaves
* The final path is constructed by concatenating the path in the subtree with the path in the top tree.
* When there is only one transaction in the block, the subtree itself becomes the block's L2->L1 message tree,
* and no top tree is needed. The out hash is the root of the the block's L2->L1 message tree.
* TODO: Handle the case where two messages in the same tx have the same hash.
* @param blockNumber - The block number at which to get the data.
* @param l2ToL1Message - The l2ToL1Message get the index / sibling path for.
* @returns A tuple of the index and the sibling path of the L2ToL1Message.
* @param blockNumber - Block number to get data from
* @param l2ToL1Message - Message to get index/path for
* @returns [index, siblingPath] for the message
*/
public async getL2ToL1MessageMembershipWitness(
blockNumber: L2BlockNumber,
Expand All @@ -715,70 +720,98 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
const block = await this.blockSource.getBlock(blockNumber === 'latest' ? await this.getBlockNumber() : blockNumber);

if (block === undefined) {
throw new Error('Block is not defined');
throw new Error('Block not found in getL2ToL1MessageMembershipWitness');
}

const l2ToL1Messages = block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs);
const messagesPerTx = block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs);

// Find index of message
let indexOfMsgInSubtree = -1;
const indexOfMsgTx = l2ToL1Messages.findIndex(msgs => {
const idx = msgs.findIndex(msg => msg.equals(l2ToL1Message));
indexOfMsgInSubtree = Math.max(indexOfMsgInSubtree, idx);
return idx !== -1;
});
// Find index of message in subtree and index of tx in a block
let messageIndexInTx = -1,
txIndex = -1;
{
txIndex = messagesPerTx.findIndex(messages => {
const idx = messages.findIndex(msg => msg.equals(l2ToL1Message));
messageIndexInTx = Math.max(messageIndexInTx, idx);
return idx !== -1;
});
}

if (indexOfMsgTx === -1) {
if (txIndex === -1) {
throw new Error('The L2ToL1Message you are trying to prove inclusion of does not exist');
}

const tempStores: AztecKVStore[] = [];
// Get the message path in subtree and message subtree height
let messagePathInSubtree: SiblingPath<number>;
let messageSubtreeHeight: number;
{
const subtreeStore = openTmpStore(true);
const txMessages = messagesPerTx[txIndex];
messageSubtreeHeight = txMessages.length <= 1 ? 1 : Math.ceil(Math.log2(txMessages.length));
const subtree = new StandardTree(
subtreeStore,
new SHA256Trunc(),
`subtree_${txIndex}`,
messageSubtreeHeight,
0n,
Fr,
);
subtree.appendLeaves(txMessages);
messagePathInSubtree = await subtree.getSiblingPath(BigInt(messageIndexInTx), true);
await subtreeStore.delete();
}

// Construct message subtrees
const l2toL1Subtrees = l2ToL1Messages.map((msgs, i) => {
const store = openTmpStore(true);
tempStores.push(store);
const treeHeight = msgs.length <= 1 ? 1 : Math.ceil(Math.log2(msgs.length));
const tree = new StandardTree(store, new SHA256Trunc(), `temp_msgs_subtrees_${i}`, treeHeight, 0n, Fr);
tree.appendLeaves(msgs);
return tree;
});
// If the number of txs is 1 we are dealing with a special case where the tx subtree itself is the whole block's
// l2 to l1 message tree.
const numTransactions = block.body.txEffects.length;
if (numTransactions === 1) {
return [BigInt(messageIndexInTx), messagePathInSubtree];
}

// path of the input msg from leaf -> first out hash calculated in base rolllup
const subtreePathOfL2ToL1Message = await l2toL1Subtrees[indexOfMsgTx].getSiblingPath(
BigInt(indexOfMsgInSubtree),
true,
// Calculate roots for all tx subtrees
const txSubtreeRoots = await Promise.all(
messagesPerTx.map(async (messages, txIdx) => {
// For a tx with no messages, we have to set an out hash of 0 to match what the circuit does.
if (messages.length === 0) {
return Fr.ZERO;
}

const txStore = openTmpStore(true);
const txTreeHeight = messages.length <= 1 ? 1 : Math.ceil(Math.log2(messages.length));
const txTree = new StandardTree(
txStore,
new SHA256Trunc(),
`tx_messages_subtree_${txIdx}`,
txTreeHeight,
0n,
Fr,
);
txTree.appendLeaves(messages);
const root = Fr.fromBuffer(txTree.getRoot(true));
await txStore.delete();
return root;
}),
);

const numTxs = block.body.txEffects.length;
if (numTxs === 1) {
return [BigInt(indexOfMsgInSubtree), subtreePathOfL2ToL1Message];
// Construct the top tree and compute the combined path
let combinedPath: Buffer[];
{
const topTreeHeight = Math.ceil(Math.log2(txSubtreeRoots.length));
// The root of this tree is the out_hash calculated in Noir => we truncate to match Noir's SHA
const topTree = new UnbalancedTree(new SHA256Trunc(), 'top_tree', topTreeHeight, Fr);
await topTree.appendLeaves(txSubtreeRoots);

const txPathInTopTree = await topTree.getSiblingPath(txSubtreeRoots[txIndex].toBigInt());
// Append subtree path to top tree path
combinedPath = messagePathInSubtree.toBufferArray().concat(txPathInTopTree.toBufferArray());
}

// For a tx with no messages, we have to set an out hash of 0 to match the circuit/contract behavior
const l2toL1SubtreeRoots = l2toL1Subtrees.map(t =>
t.getNumLeaves(true) == 0n ? Fr.ZERO : Fr.fromBuffer(t.getRoot(true)),
);
const maxTreeHeight = Math.ceil(Math.log2(l2toL1SubtreeRoots.length));
// The root of this tree is the out_hash calculated in Noir => we truncate to match Noir's SHA
const outHashTree = new UnbalancedTree(new SHA256Trunc(), 'temp_outhash_sibling_path', maxTreeHeight, Fr);
await outHashTree.appendLeaves(l2toL1SubtreeRoots);

const pathOfTxInOutHashTree = await outHashTree.getSiblingPath(l2toL1SubtreeRoots[indexOfMsgTx].toBigInt());
// Append subtree path to out hash tree path
const mergedPath = subtreePathOfL2ToL1Message.toBufferArray().concat(pathOfTxInOutHashTree.toBufferArray());
// Append binary index of subtree path to binary index of out hash tree path
const mergedIndex = parseInt(
indexOfMsgTx
.toString(2)
.concat(indexOfMsgInSubtree.toString(2).padStart(l2toL1Subtrees[indexOfMsgTx].getDepth(), '0')),
// Append binary index of subtree path to binary index of top tree path
const combinedIndex = parseInt(
txIndex.toString(2).concat(messageIndexInTx.toString(2).padStart(messageSubtreeHeight, '0')),
2,
);

// clear the tmp stores
await Promise.all(tempStores.map(store => store.delete()));

return [BigInt(mergedIndex), new SiblingPath(mergedPath.length, mergedPath)];
return [BigInt(combinedIndex), new SiblingPath(combinedPath.length, combinedPath)];
}

/**
Expand Down