Skip to content

Commit 7b8396d

Browse files
committed
fix: Wait until rollup catches up to inbox for msg sync
Do not acknowledge an L1 to L2 message as synced until the rollup pending block number has caught up with the message block. The inbox block number may drift way ahead of the rollup block number in the event of a reorg or if there are too many l1 to l2 messages being inserted. Note that the existing approach used throughout the codebase of waiting for two blocks if flawed, since if there was an earlier reorg on the chain, then the inbox will have drifted and the message will require more blocks to become available. This PR does NOT remove the existing isL1ToL2MessageSynced call, since it's used all over the place, but rather flags it as deprecated. Instead, the node and pxe now expose a function that returns the block in which the message is to be available, and aztecjs provides a helper to wait until the block is reached. The bot factory is updated to use this new approach.
1 parent b4460df commit 7b8396d

File tree

20 files changed

+523
-83
lines changed

20 files changed

+523
-83
lines changed

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ import {
8282
tryStop,
8383
} from '@aztec/stdlib/interfaces/server';
8484
import type { LogFilter, PrivateLog, TxScopedL2Log } from '@aztec/stdlib/logs';
85-
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
85+
import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
8686
import { P2PClientType } from '@aztec/stdlib/p2p';
8787
import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing';
8888
import type { NullifierLeafPreimage, PublicDataTreeLeaf, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees';
@@ -860,13 +860,19 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
860860
return [witness.index, witness.path];
861861
}
862862

863+
public async getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise<number | undefined> {
864+
const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
865+
return messageIndex ? InboxLeaf.l2BlockFromIndex(messageIndex) : undefined;
866+
}
867+
863868
/**
864869
* Returns whether an L1 to L2 message is synced by archiver and if it's ready to be included in a block.
865870
* @param l1ToL2Message - The L1 to L2 message to check.
866871
* @returns Whether the message is synced and ready to be included in a block.
867872
*/
868873
public async isL1ToL2MessageSynced(l1ToL2Message: Fr): Promise<boolean> {
869-
return (await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message)) !== undefined;
874+
const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
875+
return messageIndex !== undefined;
870876
}
871877

872878
/**

yarn-project/aztec.js/src/api/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export { waitForPXE } from '../utils/pxe.js';
1919
export { waitForNode, createAztecNodeClient, type AztecNode } from '../utils/node.js';
2020
export { getFeeJuiceBalance } from '../utils/fee_juice.js';
2121
export { readFieldCompressedString } from '../utils/field_compressed_string.js';
22+
export { isL1ToL2MessageReady, waitForL1ToL2MessageReady } from '../utils/cross_chain.js';

yarn-project/aztec.js/src/contract/batch_call.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export class BatchCall extends BaseContractInteraction {
1414
super(wallet);
1515
}
1616

17+
/**
18+
* Creates a new instance with no actual calls. Useful for triggering a no-op.
19+
* @param wallet - The wallet to use for sending the batch call.
20+
*/
21+
public static empty(wallet: Wallet) {
22+
return new BatchCall(wallet, []);
23+
}
24+
1725
/**
1826
* Returns an execution request that represents this operation.
1927
* @param options - An optional object containing additional configuration for the request generation.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Fr } from '@aztec/foundation/fields';
2+
import { retryUntil } from '@aztec/foundation/retry';
3+
4+
import type { PXE } from '../api/interfaces.js';
5+
6+
/**
7+
* Waits for the L1 to L2 message to be ready to be consumed.
8+
* @param pxe - PXE instance
9+
* @param l1ToL2MessageHash - Hash of the L1 to L2 message
10+
* @param opts - Options
11+
*/
12+
export async function waitForL1ToL2MessageReady(
13+
pxe: Pick<PXE, 'getBlockNumber' | 'getL1ToL2MessageBlock'>,
14+
l1ToL2MessageHash: Fr,
15+
opts: {
16+
/** Timeout for the operation in seconds */ timeoutSeconds: number;
17+
/** True if the message is meant to be consumed from a public function */ forPublicConsumption: boolean;
18+
},
19+
) {
20+
const messageBlockNumber = await pxe.getL1ToL2MessageBlock(l1ToL2MessageHash);
21+
return retryUntil(
22+
() => isL1ToL2MessageReady(pxe, l1ToL2MessageHash, { ...opts, messageBlockNumber }),
23+
`L1 to L2 message ${l1ToL2MessageHash.toString()} ready`,
24+
opts.timeoutSeconds,
25+
1,
26+
);
27+
}
28+
29+
/**
30+
* Returns whether the L1 to L2 message is ready to be consumed.
31+
* @param pxe - PXE instance
32+
* @param l1ToL2MessageHash - Hash of the L1 to L2 message
33+
* @param opts - Options
34+
* @returns True if the message is ready to be consumed, false otherwise
35+
*/
36+
export async function isL1ToL2MessageReady(
37+
pxe: Pick<PXE, 'getBlockNumber' | 'getL1ToL2MessageBlock'>,
38+
l1ToL2MessageHash: Fr,
39+
opts: {
40+
/** True if the message is meant to be consumed from a public function */ forPublicConsumption: boolean;
41+
/** Cached synced block number for the message (will be fetched from PXE otherwise) */ messageBlockNumber?: number;
42+
},
43+
): Promise<boolean> {
44+
const blockNumber = await pxe.getBlockNumber();
45+
const messageBlockNumber = opts.messageBlockNumber ?? (await pxe.getL1ToL2MessageBlock(l1ToL2MessageHash));
46+
if (messageBlockNumber === undefined) {
47+
return false;
48+
}
49+
50+
// Note that public messages can be consumed 1 block earlier, since the sequencer will include the messages
51+
// in the L1 to L2 message tree before executing the txs for the block. In private, however, we need to wait
52+
// until the message is included so we can make use of the membership witness.
53+
return opts.forPublicConsumption ? blockNumber + 1 >= messageBlockNumber : blockNumber >= messageBlockNumber;
54+
}

yarn-project/aztec/src/testing/anvil_test_watcher.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class AnvilTestWatcher {
5050
}
5151

5252
setIsMarkingAsProven(isMarkingAsProven: boolean) {
53+
this.logger.warn(`Watcher is now ${isMarkingAsProven ? 'marking' : 'not marking'} blocks as proven`);
5354
this.isMarkingAsProven = isMarkingAsProven;
5455
}
5556

yarn-project/bot/src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export const botConfigMappings: ConfigMappingsType<BotConfig> = {
154154
l1ToL2MessageTimeoutSeconds: {
155155
env: 'BOT_L1_TO_L2_TIMEOUT_SECONDS',
156156
description: 'How long to wait for L1 to L2 messages to become available on L2',
157-
...numberConfigHelper(60),
157+
...numberConfigHelper(3600),
158158
},
159159
senderPrivateKey: {
160160
env: 'BOT_PRIVATE_KEY',

yarn-project/bot/src/factory.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
type PXE,
1313
createLogger,
1414
createPXEClient,
15-
retryUntil,
15+
waitForL1ToL2MessageReady,
1616
} from '@aztec/aztec.js';
1717
import { createEthereumChain, createExtendedL1Client } from '@aztec/ethereum';
1818
import { Fr } from '@aztec/foundation/fields';
@@ -112,8 +112,10 @@ export class BotFactory {
112112
private async setupAccount() {
113113
const privateKey = this.config.senderPrivateKey?.getValue();
114114
if (privateKey) {
115+
this.log.info(`Setting up account with provided private key`);
115116
return await this.setupAccountWithPrivateKey(privateKey);
116117
} else {
118+
this.log.info(`Setting up test account`);
117119
return await this.setupTestAccount();
118120
}
119121
}
@@ -395,35 +397,31 @@ export class BotFactory {
395397
const mintAmount = await portal.getTokenManager().getMintAmount();
396398
const claim = await portal.bridgeTokensPublic(recipient, mintAmount, true /* mint */);
397399

398-
const isSynced = async () => await this.pxe.isL1ToL2MessageSynced(Fr.fromHexString(claim.messageHash));
399-
await retryUntil(isSynced, `message ${claim.messageHash} sync`, this.config.l1ToL2MessageTimeoutSeconds, 1);
400+
await this.withNoMinTxsPerBlock(() =>
401+
waitForL1ToL2MessageReady(this.pxe, Fr.fromHexString(claim.messageHash), {
402+
timeoutSeconds: this.config.l1ToL2MessageTimeoutSeconds,
403+
forPublicConsumption: false,
404+
}),
405+
);
400406

401407
this.log.info(`Created a claim for ${mintAmount} L1 fee juice to ${recipient}.`, claim);
402408

403-
// Progress by 2 L2 blocks so that the l1ToL2Message added above will be available to use on L2.
404-
await this.advanceL2Block();
405-
await this.advanceL2Block();
406-
407409
return claim;
408410
}
409411

410412
private async withNoMinTxsPerBlock<T>(fn: () => Promise<T>): Promise<T> {
411413
if (!this.nodeAdmin || !this.config.flushSetupTransactions) {
414+
this.log.verbose(`No node admin client or flushing not requested (not setting minTxsPerBlock to 0)`);
412415
return fn();
413416
}
414417
const { minTxsPerBlock } = await this.nodeAdmin.getConfig();
418+
this.log.warn(`Setting sequencer minTxsPerBlock to 0 from ${minTxsPerBlock} to flush setup transactions`);
415419
await this.nodeAdmin.setConfig({ minTxsPerBlock: 0 });
416420
try {
417421
return await fn();
418422
} finally {
423+
this.log.warn(`Restoring sequencer minTxsPerBlock to ${minTxsPerBlock}`);
419424
await this.nodeAdmin.setConfig({ minTxsPerBlock });
420425
}
421426
}
422-
423-
private async advanceL2Block() {
424-
await this.withNoMinTxsPerBlock(async () => {
425-
const initialBlockNumber = await this.node!.getBlockNumber();
426-
await retryUntil(async () => (await this.node!.getBlockNumber()) >= initialBlockNumber + 1);
427-
});
428-
}
429427
}

yarn-project/end-to-end/src/e2e_bot.test.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { getInitialTestAccountsData } from '@aztec/accounts/testing';
2-
import type { PXE } from '@aztec/aztec.js';
2+
import { Fr, type PXE } from '@aztec/aztec.js';
33
import { AmmBot, Bot, type BotConfig, SupportedTokenContracts, getBotDefaultConfig } from '@aztec/bot';
44
import { AVM_MAX_PROCESSABLE_L2_GAS, MAX_PROCESSABLE_DA_GAS_PER_BLOCK } from '@aztec/constants';
5+
import { SecretValue } from '@aztec/foundation/config';
6+
import { bufferToHex } from '@aztec/foundation/string';
57

6-
import { setup } from './fixtures/utils.js';
8+
import { type EndToEndContext, getPrivateKeyFromIndex, setup } from './fixtures/utils.js';
79

810
describe('e2e_bot', () => {
911
let pxe: PXE;
1012
let teardown: () => Promise<void>;
1113

1214
let config: BotConfig;
15+
let ctx: EndToEndContext;
1316

1417
beforeAll(async () => {
1518
const initialFundedAccounts = await getInitialTestAccountsData();
16-
({ teardown, pxe } = await setup(1, {
17-
initialFundedAccounts,
18-
}));
19+
ctx = await setup(1, { initialFundedAccounts });
20+
({ teardown, pxe } = ctx);
1921
});
2022

2123
afterAll(() => teardown());
@@ -59,13 +61,7 @@ describe('e2e_bot', () => {
5961
});
6062

6163
it('sends token from the bot using PrivateToken', async () => {
62-
const easyBot = await Bot.create(
63-
{
64-
...config,
65-
contract: SupportedTokenContracts.PrivateTokenContract,
66-
},
67-
{ pxe },
68-
);
64+
const easyBot = await Bot.create({ ...config, contract: SupportedTokenContracts.PrivateTokenContract }, { pxe });
6965
const { recipient: recipientBefore } = await easyBot.getBalances();
7066

7167
await easyBot.run();
@@ -105,4 +101,25 @@ describe('e2e_bot', () => {
105101
).toBeTrue();
106102
});
107103
});
104+
105+
describe('setup via bridging funds cross-chain', () => {
106+
beforeAll(() => {
107+
config = {
108+
...getBotDefaultConfig(),
109+
followChain: 'PENDING',
110+
ammTxs: false,
111+
senderPrivateKey: new SecretValue(Fr.random()),
112+
l1PrivateKey: new SecretValue(bufferToHex(getPrivateKeyFromIndex(8)!)),
113+
l1RpcUrls: ctx.config.l1RpcUrls,
114+
flushSetupTransactions: true,
115+
};
116+
});
117+
118+
// See 'can consume L1 to L2 message in %s after inbox drifts away from the rollup'
119+
// in end-to-end/src/e2e_cross_chain_messaging/l1_to_l2.test.ts for context on this test.
120+
it('creates bot after inbox drift', async () => {
121+
await ctx.cheatCodes.rollup.advanceInboxInProgress(10);
122+
await Bot.create(config, { pxe, node: ctx.aztecNode, nodeAdmin: ctx.aztecNodeAdmin });
123+
}, 300_000);
124+
});
108125
});

yarn-project/end-to-end/src/e2e_cross_chain_messaging/cross_chain_messaging_test.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AztecNodeConfig } from '@aztec/aztec-node';
22
import { AztecAddress, type AztecNode, EthAddress, type Logger, type PXE, createLogger } from '@aztec/aztec.js';
33
import { CheatCodes } from '@aztec/aztec/testing';
44
import {
5+
type DeployL1ContractsArgs,
56
type DeployL1ContractsReturnType,
67
type ExtendedViemWalletClient,
78
createExtendedL1Client,
@@ -23,6 +24,7 @@ import {
2324
deployAccounts,
2425
publicDeployAccounts,
2526
} from '../fixtures/snapshot_manager.js';
27+
import type { SetupOptions } from '../fixtures/utils.js';
2628
import { CrossChainTestHarness } from '../shared/cross_chain_test_harness.js';
2729

2830
const { E2E_DATA_PATH: dataPath } = process.env;
@@ -34,6 +36,7 @@ export class CrossChainMessagingTest {
3436
pxe!: PXE;
3537
aztecNodeConfig!: AztecNodeConfig;
3638
aztecNodeAdmin!: AztecNodeAdmin;
39+
ctx!: SubsystemsContext;
3740

3841
l1Client!: ExtendedViemWalletClient | undefined;
3942

@@ -52,23 +55,26 @@ export class CrossChainMessagingTest {
5255

5356
deployL1ContractsValues!: DeployL1ContractsReturnType;
5457

55-
constructor(testName: string) {
58+
constructor(testName: string, opts: SetupOptions = {}, deployL1ContractsArgs: Partial<DeployL1ContractsArgs> = {}) {
5659
this.logger = createLogger(`e2e:e2e_cross_chain_messaging:${testName}`);
57-
this.snapshotManager = createSnapshotManager(`e2e_cross_chain_messaging/${testName}`, dataPath);
60+
this.snapshotManager = createSnapshotManager(`e2e_cross_chain_messaging/${testName}`, dataPath, opts, {
61+
initialValidators: [],
62+
...deployL1ContractsArgs,
63+
});
5864
}
5965

6066
async assumeProven() {
6167
await this.cheatCodes.rollup.markAsProven();
6268
}
6369

6470
async setup() {
65-
const { aztecNode, pxe, aztecNodeConfig, deployL1ContractsValues } = await this.snapshotManager.setup();
66-
this.aztecNode = aztecNode;
67-
this.pxe = pxe;
68-
this.aztecNodeConfig = aztecNodeConfig;
71+
this.ctx = await this.snapshotManager.setup();
72+
this.aztecNode = this.ctx.aztecNode;
73+
this.pxe = this.ctx.pxe;
74+
this.aztecNodeConfig = this.ctx.aztecNodeConfig;
6975
this.cheatCodes = await CheatCodes.create(this.aztecNodeConfig.l1RpcUrls, this.pxe);
70-
this.deployL1ContractsValues = deployL1ContractsValues;
71-
this.aztecNodeAdmin = aztecNode;
76+
this.deployL1ContractsValues = this.ctx.deployL1ContractsValues;
77+
this.aztecNodeAdmin = this.ctx.aztecNode;
7278
}
7379

7480
snapshot = <T>(

0 commit comments

Comments
 (0)