Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 1 addition & 1 deletion yarn-project/archiver/src/store/message_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class MessageStore {
);
}

// Check the first message in a block has the correct index.
// Check the first message in a checkpoint has the correct index.
if (
(!lastMessage || message.checkpointNumber > lastMessage.checkpointNumber) &&
message.index !== expectedStart
Expand Down
6 changes: 6 additions & 0 deletions yarn-project/archiver/src/test/mock_l2_block_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource {
await this.createCheckpoints(numBlocks, 1);
}

public getCheckpointNumber(): Promise<CheckpointNumber> {
return Promise.resolve(
this.checkpointList.length === 0 ? CheckpointNumber.ZERO : CheckpointNumber(this.checkpointList.length),
);
}

/** Creates checkpoints, each containing `blocksPerCheckpoint` blocks. */
public async createCheckpoints(numCheckpoints: number, blocksPerCheckpoint: number = 1) {
for (let c = 0; c < numCheckpoints; c++) {
Expand Down
10 changes: 6 additions & 4 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
return await this.blockSource.getCheckpointedL2BlockNumber();
}

public getCheckpointNumber(): Promise<CheckpointNumber> {
return this.blockSource.getCheckpointNumber();
}

/**
* Method to fetch the version of the package.
* @returns The node package version
Expand Down Expand Up @@ -1041,11 +1045,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
return [witness.index, witness.path];
}

public async getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise<BlockNumber | undefined> {
public async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise<CheckpointNumber | undefined> {
const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
return messageIndex
? BlockNumber.fromCheckpointNumber(InboxLeaf.checkpointNumberFromIndex(messageIndex))
: undefined;
return messageIndex ? InboxLeaf.checkpointNumberFromIndex(messageIndex) : undefined;
}

/**
Expand Down
27 changes: 9 additions & 18 deletions yarn-project/aztec.js/src/utils/cross_chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client';
* @param l1ToL2MessageHash - Hash of the L1 to L2 message
* @param opts - Options
*/
export async function waitForL1ToL2MessageReady(
node: Pick<AztecNode, 'getBlockNumber' | 'getL1ToL2MessageBlock'>,
export function waitForL1ToL2MessageReady(
node: Pick<AztecNode, 'getBlock' | 'getL1ToL2MessageCheckpoint'>,
l1ToL2MessageHash: Fr,
opts: {
/** Timeout for the operation in seconds */ timeoutSeconds: number;
/** True if the message is meant to be consumed from a public function */ forPublicConsumption: boolean;
},
) {
const messageBlockNumber = await node.getL1ToL2MessageBlock(l1ToL2MessageHash);
return retryUntil(
() => isL1ToL2MessageReady(node, l1ToL2MessageHash, { ...opts, messageBlockNumber }),
() => isL1ToL2MessageReady(node, l1ToL2MessageHash),
`L1 to L2 message ${l1ToL2MessageHash.toString()} ready`,
opts.timeoutSeconds,
1,
Expand All @@ -29,25 +27,18 @@ export async function waitForL1ToL2MessageReady(
* Returns whether the L1 to L2 message is ready to be consumed.
* @param node - Aztec node instance used to obtain the information about the message
* @param l1ToL2MessageHash - Hash of the L1 to L2 message
* @param opts - Options
* @returns True if the message is ready to be consumed, false otherwise
*/
export async function isL1ToL2MessageReady(
node: Pick<AztecNode, 'getBlockNumber' | 'getL1ToL2MessageBlock'>,
node: Pick<AztecNode, 'getBlock' | 'getL1ToL2MessageCheckpoint'>,
l1ToL2MessageHash: Fr,
opts: {
/** True if the message is meant to be consumed from a public function */ forPublicConsumption: boolean;
/** Cached synced block number for the message (will be fetched from PXE otherwise) */ messageBlockNumber?: number;
},
): Promise<boolean> {
const blockNumber = await node.getBlockNumber();
const messageBlockNumber = opts.messageBlockNumber ?? (await node.getL1ToL2MessageBlock(l1ToL2MessageHash));
if (messageBlockNumber === undefined) {
const messageCheckpointNumber = await node.getL1ToL2MessageCheckpoint(l1ToL2MessageHash);
if (messageCheckpointNumber === undefined) {
return false;
}

// Note that public messages can be consumed 1 block earlier, since the sequencer will include the messages
// in the L1 to L2 message tree before executing the txs for the block. In private, however, we need to wait
// until the message is included so we can make use of the membership witness.
return opts.forPublicConsumption ? blockNumber + 1 >= messageBlockNumber : blockNumber >= messageBlockNumber;
// L1 to L2 messages are included in the first block of a checkpoint
const latestBlock = await node.getBlock('latest');
return latestBlock !== undefined && latestBlock.checkpointNumber >= messageCheckpointNumber;
}
9 changes: 1 addition & 8 deletions yarn-project/bot/src/cross_chain_bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,7 @@ export class CrossChainBot extends BaseBot {
): Promise<PendingL1ToL2Message | undefined> {
const now = Date.now();
for (const msg of pendingMessages) {
const ready = await isL1ToL2MessageReady(this.node, Fr.fromHexString(msg.msgHash), {
// Use forPublicConsumption: false so we wait until blockNumber >= messageBlockNumber.
// With forPublicConsumption: true, the check returns true one block early (the sequencer
// includes L1→L2 messages before executing the block's txs), but gas estimation simulates
// against the current world state which doesn't yet have the message.
// See https://linear.app/aztec-labs/issue/A-548 for details.
forPublicConsumption: false,
});
const ready = await isL1ToL2MessageReady(this.node, Fr.fromHexString(msg.msgHash));
if (ready) {
return msg;
}
Expand Down
7 changes: 0 additions & 7 deletions yarn-project/bot/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,6 @@ export class BotFactory {
const firstMsg = allMessages[0];
await waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(firstMsg.msgHash), {
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
// Use forPublicConsumption: false so we wait until the message is in the current world
// state. With true, it returns one block early which causes gas estimation simulation to
// fail since it runs against the current state.
// See https://linear.app/aztec-labs/issue/A-548 for details.
forPublicConsumption: false,
});
this.log.info(`First L1→L2 message is ready`);
}
Expand Down Expand Up @@ -507,7 +502,6 @@ export class BotFactory {
await this.withNoMinTxsPerBlock(() =>
waitForL1ToL2MessageReady(this.aztecNode, messageHash, {
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
forPublicConsumption: false,
}),
);
return existingClaim.claim;
Expand Down Expand Up @@ -546,7 +540,6 @@ export class BotFactory {
await this.withNoMinTxsPerBlock(() =>
waitForL1ToL2MessageReady(this.aztecNode, Fr.fromHexString(claim.messageHash), {
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
forPublicConsumption: false,
}),
);

Expand Down
14 changes: 11 additions & 3 deletions yarn-project/end-to-end/src/bench/node_rpc_perf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ describe('e2e_node_rpc_perf', () => {
expect(stats.avg).toBeLessThan(1000);
});

it('benchmarks getCheckpointNumber', async () => {
const { stats } = await benchmark('getCheckpointNumber', () => aztecNode.getCheckpointNumber());
addResult('getCheckpointNumber', stats);
expect(stats.avg).toBeLessThan(1000);
});

it('benchmarks getProvenBlockNumber', async () => {
const { stats } = await benchmark('getProvenBlockNumber', () => aztecNode.getProvenBlockNumber());
addResult('getProvenBlockNumber', stats);
Expand Down Expand Up @@ -414,10 +420,12 @@ describe('e2e_node_rpc_perf', () => {
});

describe('message APIs', () => {
it('benchmarks getL1ToL2MessageBlock', async () => {
it('benchmarks getL1ToL2MessageCheckpoint', async () => {
const l1ToL2Message = Fr.random();
const { stats } = await benchmark('getL1ToL2MessageBlock', () => aztecNode.getL1ToL2MessageBlock(l1ToL2Message));
addResult('getL1ToL2MessageBlock', stats);
const { stats } = await benchmark('getL1ToL2MessageCheckpoint', () =>
aztecNode.getL1ToL2MessageCheckpoint(l1ToL2Message),
);
addResult('getL1ToL2MessageCheckpoint', stats);
expect(stats.avg).toBeLessThan(2000);
});

Expand Down
126 changes: 85 additions & 41 deletions yarn-project/end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { isL1ToL2MessageReady } from '@aztec/aztec.js/messaging';
import type { AztecNode } from '@aztec/aztec.js/node';
import { TxExecutionResult } from '@aztec/aztec.js/tx';
import type { Wallet } from '@aztec/aztec.js/wallet';
import { BlockNumber } from '@aztec/foundation/branded-types';
import { BlockNumber, IndexWithinCheckpoint } from '@aztec/foundation/branded-types';
import { timesAsync } from '@aztec/foundation/collection';
import { retryUntil } from '@aztec/foundation/retry';
import { TestContract } from '@aztec/noir-test-contracts.js/Test';
Expand Down Expand Up @@ -56,7 +56,34 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
if (newBlock === block) {
throw new Error(`Failed to advance block ${block}`);
}
return undefined;
return newBlock;
};

const waitForBlockToCheckpoint = async (blockNumber: BlockNumber) => {
return await retryUntil(
async () => {
const checkpointedBlockNumber = await aztecNode.getCheckpointedBlockNumber();
const isCheckpointed = checkpointedBlockNumber >= blockNumber;
if (!isCheckpointed) {
return undefined;
}
const [checkpointedBlock] = await aztecNode.getCheckpointedBlocks(blockNumber, 1);
return checkpointedBlock.checkpointNumber;
},
'wait for block to checkpoint',
60,
);
};

const advanceCheckpoint = async () => {
let checkpoint = await aztecNode.getCheckpointNumber();
const originalCheckpoint = checkpoint;
log.warn(`Original checkpoint ${originalCheckpoint}`);
do {
const newBlock = await advanceBlock();
checkpoint = await waitForBlockToCheckpoint(newBlock);
} while (checkpoint <= originalCheckpoint);
log.warn(`At checkpoint ${checkpoint}`);
};

// Same as above but ignores errors. Useful if we expect a prune.
Expand All @@ -68,12 +95,19 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
}
};

// Waits until the message is fetched by the archiver of the node and returns the msg target block
// Waits until the message is fetched by the archiver of the node and returns the msg target checkpoint
const waitForMessageFetched = async (msgHash: Fr) => {
log.warn(`Waiting until the message is fetched by the node`);
return await retryUntil(
async () => (await aztecNode.getL1ToL2MessageBlock(msgHash)) ?? (await advanceBlock()),
'get msg block',
async () => {
const checkpoint = await aztecNode.getL1ToL2MessageCheckpoint(msgHash);
if (checkpoint !== undefined) {
return checkpoint;
}
await advanceBlock();
return undefined;
},
'get msg checkpoint',
60,
);
};
Expand All @@ -84,20 +118,27 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
scope: 'private' | 'public',
onNotReady?: (blockNumber: BlockNumber) => Promise<void>,
) => {
const msgBlock = await waitForMessageFetched(msgHash);
log.warn(`Waiting until L2 reaches msg block ${msgBlock} (current is ${await aztecNode.getBlockNumber()})`);
const msgCheckpoint = await waitForMessageFetched(msgHash);
log.warn(
`Waiting until L2 reaches the first block of msg checkpoint ${msgCheckpoint} (current is ${await aztecNode.getCheckpointNumber()})`,
);
await retryUntil(
async () => {
const blockNumber = await aztecNode.getBlockNumber();
const [blockNumber, checkpointNumber] = await Promise.all([
aztecNode.getBlockNumber(),
aztecNode.getCheckpointNumber(),
]);
const witness = await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash);
const isReady = await isL1ToL2MessageReady(aztecNode, msgHash, { forPublicConsumption: scope === 'public' });
log.info(`Block is ${blockNumber}. Message block is ${msgBlock}. Witness ${!!witness}. Ready ${isReady}.`);
const isReady = await isL1ToL2MessageReady(aztecNode, msgHash);
log.info(
`Block is ${blockNumber}, checkpoint is ${checkpointNumber}. Message checkpoint is ${msgCheckpoint}. Witness ${!!witness}. Ready ${isReady}.`,
);
if (!isReady) {
await (onNotReady ? onNotReady(blockNumber) : advanceBlock());
}
return isReady;
},
`wait for rollup to reach msg block ${msgBlock}`,
`wait for rollup to reach msg checkpoint ${msgCheckpoint}`,
120,
);
};
Expand All @@ -118,12 +159,8 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {

await waitForMessageReady(message1Hash, scope);

// The waitForMessageReady returns true earlier for public-land, so we can only check the membership
// witness for private-land here.
if (scope === 'private') {
const [message1Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message1Hash))!;
expect(actualMessage1Index.toBigInt()).toBe(message1Index);
}
const [message1Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message1Hash))!;
expect(actualMessage1Index.toBigInt()).toBe(message1Index);

// We consume the L1 to L2 message using the test contract either from private or public
await getConsumeMethod(scope)(
Expand All @@ -143,12 +180,10 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
// We check that the duplicate message was correctly inserted by checking that its message index is defined
await waitForMessageReady(message2Hash, scope);

if (scope === 'private') {
const [message2Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message2Hash))!;
expect(message2Index).toBeDefined();
expect(message2Index).toBeGreaterThan(actualMessage1Index.toBigInt());
expect(actualMessage2Index.toBigInt()).toBe(message2Index);
}
const [message2Index] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', message2Hash))!;
expect(message2Index).toBeDefined();
expect(message2Index).toBeGreaterThan(actualMessage1Index.toBigInt());
expect(actualMessage2Index.toBigInt()).toBe(message2Index);

// Now we consume the message again. Everything should pass because oracle should return the duplicate message
// which is not nullified
Expand All @@ -162,21 +197,22 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
120_000,
);

// Inbox block number can drift on two scenarios: if the rollup reorgs and rolls back its own
// block number, or if the inbox receives too many messages and they are inserted faster than
// they are consumed. In this test, we mine several blocks without marking them as proven until
// Inbox checkpoint number can drift on two scenarios: if the rollup reorgs and rolls back its own
// checkpoint number, or if the inbox receives too many messages and they are inserted faster than
// they are consumed. In this test, we mine several checkpoints without marking them as proven until
// we can trigger a reorg, and then wait until the message can be processed to consume it.
it.each(['private', 'public'] as const)(
'can consume L1 to L2 message in %s after inbox drifts away from the rollup',
async (scope: 'private' | 'public') => {
// Stop proving
const lastProven = await aztecNode.getBlockNumber();
log.warn(`Stopping proof submission at block ${lastProven} to allow drift`);
const [checkpointedProvenBlock] = await aztecNode.getCheckpointedBlocks(lastProven, 1);
log.warn(`Stopping proof submission at checkpoint ${checkpointedProvenBlock.checkpointNumber} to allow drift`);
t.context.watcher.setIsMarkingAsProven(false);

// Mine several blocks to ensure drift
// Mine several checkpoints to ensure drift
log.warn(`Mining blocks to allow drift`);
await timesAsync(4, advanceBlock);
await timesAsync(4, advanceCheckpoint);

// Generate and send the message to the L1 contract
log.warn(`Sending L1 to L2 message`);
Expand All @@ -185,9 +221,9 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
const { msgHash, globalLeafIndex } = await sendL1ToL2Message(message, crossChainTestHarness);

// Wait until the Aztec node has synced it
const msgBlockNumber = await waitForMessageFetched(msgHash);
log.warn(`Message synced for block ${msgBlockNumber}`);
expect(lastProven + 4).toBeLessThan(msgBlockNumber);
const msgCheckpointNumber = await waitForMessageFetched(msgHash);
log.warn(`Message synced for checkpoint ${msgCheckpointNumber}`);
expect(checkpointedProvenBlock.checkpointNumber + 4).toBeLessThan(msgCheckpointNumber);

// And keep mining until we prune back to the original block number. Now the "waiting for two blocks"
// strategy for the message to be ready to use shouldn't work, since the lastProven block is more than
Expand All @@ -214,25 +250,33 @@ describe('e2e_cross_chain_messaging l1_to_l2', () => {
// On private, we simulate the tx locally and check that we get a missing message error, then we advance to the next block
await expect(() => consume().simulate({ from: user1Address })).rejects.toThrow(/No L1 to L2 message found/);
await tryAdvanceBlock();
await t.context.watcher.markAsProven();
} else {
// On public, we actually send the tx and check that it reverts due to the missing message.
// This advances the block too as a side-effect. Note that we do not rely on a simulation since the cross chain messages
// do not get added at the beginning of the block during node_simulatePublicCalls (maybe they should?).
// In public it is harder to determine when a message becomes consumable.
// We send a transaction, this advances the chain and the message MIGHT be consumed in the new block.
// If it does get consumed then we check that the block contains the message.
// If it fails we check that the block doesn't contain the message
const receipt = await consume().send({ from: user1Address, wait: { dontThrowOnRevert: true } });
expect(receipt.executionResult).toEqual(TxExecutionResult.APP_LOGIC_REVERTED);
await t.context.watcher.markAsProven();
if (receipt.executionResult === TxExecutionResult.SUCCESS) {
// The block the transaction included should be for the message checkpoint number
// and be the first block in the checkpoint
const block = await aztecNode.getBlock(receipt.blockNumber!);
expect(block).toBeDefined();
expect(block!.checkpointNumber).toEqual(msgCheckpointNumber);
expect(block!.indexWithinCheckpoint).toEqual(IndexWithinCheckpoint.ZERO);
} else {
expect(receipt.executionResult).toEqual(TxExecutionResult.APP_LOGIC_REVERTED);
}
}
await t.context.watcher.markAsProven();
});

// Verify the membership witness is available for creating the tx (private-land only)
if (scope === 'private') {
const [messageIndex] = (await aztecNode.getL1ToL2MessageMembershipWitness('latest', msgHash))!;
expect(messageIndex).toEqual(globalLeafIndex.toBigInt());
// And consume the message for private, public was already consumed.
await consume().send({ from: user1Address });
}

// And consume the message
await consume().send({ from: user1Address });
},
);
});
Loading
Loading