Skip to content
Merged
Show file tree
Hide file tree
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
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