diff --git a/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts b/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts index 7a0b896660f..fd8f5a08bdb 100644 --- a/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts +++ b/yarn-project/aztec.js/src/rpc_clients/pxe_client.ts @@ -8,6 +8,7 @@ import { Note, NullifierMembershipWitness, type PXE, + SiblingPath, SimulatedTx, Tx, TxEffect, @@ -58,6 +59,7 @@ export const createPXEClient = (url: string, fetch = makeFetch([1, 2, 3], false) TxExecutionRequest, TxHash, Buffer32, + SiblingPath, }, { EncryptedNoteL2BlockL2Logs, diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index f170202c719..df7fed113eb 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -10,6 +10,7 @@ import { type OutgoingNotesFilter, type PXE, type PXEInfo, + type SiblingPath, type SimulatedTx, type SyncStatus, type Tx, @@ -25,6 +26,7 @@ import { type CompleteAddress, type Fq, type Fr, + type L1_TO_L2_MSG_TREE_HEIGHT, type PartialAddress, type Point, } from '@aztec/circuits.js'; @@ -206,4 +208,11 @@ export abstract class BaseWallet implements Wallet { ) { return this.pxe.getEvents(type, eventMetadata, from, limit, vpks); } + public getL1ToL2MembershipWitness( + contractAddress: AztecAddress, + messageHash: Fr, + secret: Fr, + ): Promise<[bigint, SiblingPath]> { + return this.pxe.getL1ToL2MembershipWitness(contractAddress, messageHash, secret); + } } diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index bcff339a02f..37f6e0cbfa5 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -3,6 +3,7 @@ import { type CompleteAddress, type Fq, type Fr, + type L1_TO_L2_MSG_TREE_HEIGHT, type PartialAddress, type Point, } from '@aztec/circuits.js'; @@ -19,6 +20,7 @@ import { type L2Block } from '../l2_block.js'; import { type GetUnencryptedLogsResponse, type L1EventPayload, type LogFilter } from '../logs/index.js'; import { type IncomingNotesFilter } from '../notes/incoming_notes_filter.js'; import { type ExtendedNote, type OutgoingNotesFilter, type UniqueNote } from '../notes/index.js'; +import { type SiblingPath } from '../sibling_path/sibling_path.js'; import { type NoteProcessorStats } from '../stats/stats.js'; import { type SimulatedTx, type Tx, type TxHash, type TxReceipt } from '../tx/index.js'; import { type TxEffect } from '../tx_effect.js'; @@ -239,6 +241,20 @@ export interface PXE { */ getIncomingNotes(filter: IncomingNotesFilter): Promise; + /** + * Fetches an L1 to L2 message from the node. + * @param contractAddress - Address of a contract by which the message was emitted. + * @param messageHash - Hash of the message. + * @param secret - Secret used to compute a nullifier. + * @dev Contract address and secret are only used to compute the nullifier to get non-nullified messages + * @returns The l1 to l2 membership witness (index of message in the tree and sibling path). + */ + getL1ToL2MembershipWitness( + contractAddress: AztecAddress, + messageHash: Fr, + secret: Fr, + ): Promise<[bigint, SiblingPath]>; + /** * Gets outgoing notes of accounts registered in this PXE based on the provided filter. * @param filter - The filter to apply to the notes. diff --git a/yarn-project/circuit-types/src/messaging/l1_to_l2_message.ts b/yarn-project/circuit-types/src/messaging/l1_to_l2_message.ts index 79980b07da4..56e457f62f1 100644 --- a/yarn-project/circuit-types/src/messaging/l1_to_l2_message.ts +++ b/yarn-project/circuit-types/src/messaging/l1_to_l2_message.ts @@ -1,7 +1,13 @@ +import { type L1_TO_L2_MSG_TREE_HEIGHT } from '@aztec/circuits.js'; +import { computeL1ToL2MessageNullifier } from '@aztec/circuits.js/hash'; +import { type AztecAddress } from '@aztec/foundation/aztec-address'; import { sha256ToField } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; +import { type AztecNode } from '../interfaces/aztec-node.js'; +import { MerkleTreeId } from '../merkle_tree_id.js'; +import { type SiblingPath } from '../sibling_path/index.js'; import { L1Actor } from './l1_actor.js'; import { L2Actor } from './l2_actor.js'; @@ -70,3 +76,34 @@ export class L1ToL2Message { return new L1ToL2Message(L1Actor.random(), L2Actor.random(), Fr.random(), Fr.random()); } } + +// This functionality is not on the node because we do not want to pass the node the secret, and give the node the ability to derive a valid nullifer for an L1 to L2 message. +export async function getNonNullifiedL1ToL2MessageWitness( + node: AztecNode, + contractAddress: AztecAddress, + messageHash: Fr, + secret: Fr, +): Promise<[bigint, SiblingPath]> { + let nullifierIndex: bigint | undefined; + let messageIndex = 0n; + let startIndex = 0n; + let siblingPath: SiblingPath; + + // We iterate over messages until we find one whose nullifier is not in the nullifier tree --> we need to check + // for nullifiers because messages can have duplicates. + do { + const response = await node.getL1ToL2MessageMembershipWitness('latest', messageHash, startIndex); + if (!response) { + throw new Error(`No non-nullified L1 to L2 message found for message hash ${messageHash.toString()}`); + } + [messageIndex, siblingPath] = response; + + const messageNullifier = computeL1ToL2MessageNullifier(contractAddress, messageHash, secret, messageIndex); + + nullifierIndex = await node.findLeafIndex('latest', MerkleTreeId.NULLIFIER_TREE, messageNullifier); + + startIndex = messageIndex + 1n; + } while (nullifierIndex !== undefined); + + return [messageIndex, siblingPath]; +} diff --git a/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts b/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts index 436b4f6a218..f47f145095d 100644 --- a/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts +++ b/yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts @@ -1,7 +1,8 @@ import { createCompatibleClient } from '@aztec/aztec.js'; -import { type AztecAddress } from '@aztec/circuits.js'; import { FeeJuicePortalManager, prettyPrintJSON } from '@aztec/cli/utils'; import { createEthereumChain, createL1Clients } from '@aztec/ethereum'; +import { type AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr } from '@aztec/foundation/fields'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; export async function bridgeL1FeeJuice( @@ -24,9 +25,13 @@ export async function bridgeL1FeeJuice( // Prepare L2 client const client = await createCompatibleClient(rpcUrl, debugLogger); + const { + protocolContractAddresses: { feeJuice: feeJuiceAddress }, + } = await client.getPXEInfo(); + // Setup portal manager const portal = await FeeJuicePortalManager.new(client, publicClient, walletClient, debugLogger); - const { claimAmount, claimSecret } = await portal.bridgeTokensPublic(recipient, amount, mint); + const { claimAmount, claimSecret, messageHash } = await portal.bridgeTokensPublic(recipient, amount, mint); if (json) { const out = { @@ -40,8 +45,20 @@ export async function bridgeL1FeeJuice( } else { log(`Bridged ${claimAmount} fee juice to L2 portal`); } - log(`claimAmount=${claimAmount},claimSecret=${claimSecret}\n`); + log(`claimAmount=${claimAmount},claimSecret=${claimSecret},messageHash=${messageHash}\n`); log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); + log(`This command will now continually poll every minute for the inclusion of the newly created L1 to L2 message`); } + + const interval = setInterval(async () => { + const witness = await client.getL1ToL2MembershipWitness(feeJuiceAddress, Fr.fromString(messageHash), claimSecret); + if (witness) { + log(`Your bridged fee juice is now available. You can claim it like this: + aztec send claim_public --args ${recipient} ${amount} ${claimSecret} ${witness[0]} -ca ${feeJuiceAddress} -c FeeJuice -sk $SECRET_KEY + `); + clearInterval(interval); + } + }, 60_000); + return claimSecret; } diff --git a/yarn-project/cli-wallet/src/cmds/index.ts b/yarn-project/cli-wallet/src/cmds/index.ts index fc1ff2c6608..eec2f823075 100644 --- a/yarn-project/cli-wallet/src/cmds/index.ts +++ b/yarn-project/cli-wallet/src/cmds/index.ts @@ -379,7 +379,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL address, secretKey, rpcUrl, - fields, + body: noteFields, hash, } = options; const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db); @@ -387,7 +387,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL const account = await createOrRetrieveAccount(client, address, db, undefined, secretKey); const wallet = await account.getWallet(); - await addNote(wallet, address, contractAddress, noteName, storageFieldName, artifactPath, hash, fields, log); + await addNote(wallet, address, contractAddress, noteName, storageFieldName, artifactPath, hash, noteFields, log); }); return program; diff --git a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts index d93b705068d..877535faad1 100644 --- a/yarn-project/cli/src/cmds/l1/bridge_erc20.ts +++ b/yarn-project/cli/src/cmds/l1/bridge_erc20.ts @@ -27,10 +27,11 @@ export async function bridgeERC20( // Setup portal manager const manager = new L1PortalManager(portalAddress, tokenAddress, publicClient, walletClient, debugLogger); let claimSecret: Fr; + let messageHash: `0x${string}`; if (privateTransfer) { - ({ claimSecret } = await manager.bridgeTokensPrivate(recipient, amount, mint)); + ({ claimSecret, messageHash } = await manager.bridgeTokensPrivate(recipient, amount, mint)); } else { - ({ claimSecret } = await manager.bridgeTokensPublic(recipient, amount, mint)); + ({ claimSecret, messageHash } = await manager.bridgeTokensPublic(recipient, amount, mint)); } if (json) { @@ -46,7 +47,7 @@ export async function bridgeERC20( } else { log(`Bridged ${amount} tokens to L2 portal`); } - log(`claimAmount=${amount},claimSecret=${claimSecret}\n`); + log(`claimAmount=${amount},claimSecret=${claimSecret}\n,messageHash=${messageHash}`); log(`Note: You need to wait for two L2 blocks before pulling them from the L2 side`); } } diff --git a/yarn-project/cli/src/cmds/pxe/get_l1_to_l2_message_witness.ts b/yarn-project/cli/src/cmds/pxe/get_l1_to_l2_message_witness.ts new file mode 100644 index 00000000000..aff3b463507 --- /dev/null +++ b/yarn-project/cli/src/cmds/pxe/get_l1_to_l2_message_witness.ts @@ -0,0 +1,25 @@ +import { type AztecAddress, type Fr, createCompatibleClient } from '@aztec/aztec.js'; +import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; + +export async function getL1ToL2MessageWitness( + rpcUrl: string, + contractAddress: AztecAddress, + messageHash: Fr, + secret: Fr, + debugLogger: DebugLogger, + log: LogFn, +) { + const client = await createCompatibleClient(rpcUrl, debugLogger); + const messageWitness = await client.getL1ToL2MembershipWitness(contractAddress, messageHash, secret); + + log( + messageWitness === undefined + ? ` + L1 to L2 Message not found. + ` + : ` + L1 to L2 message index: ${messageWitness[0]} + L1 to L2 message sibling path: ${messageWitness[1]} + `, + ); +} diff --git a/yarn-project/cli/src/cmds/pxe/index.ts b/yarn-project/cli/src/cmds/pxe/index.ts index b712ff6f223..e13eb150021 100644 --- a/yarn-project/cli/src/cmds/pxe/index.ts +++ b/yarn-project/cli/src/cmds/pxe/index.ts @@ -7,6 +7,7 @@ import { logJson, parseAztecAddress, parseEthereumAddress, + parseField, parseFieldFromHexString, parseOptionalAztecAddress, parseOptionalInteger, @@ -165,6 +166,18 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL await blockNumber(options.rpcUrl, debugLogger, log); }); + program + .command('get-l1-to-l2-message-witness') + .description('Gets a L1 to L2 message witness.') + .requiredOption('-ca, --contract-address
', 'Aztec address of the contract.', parseAztecAddress) + .requiredOption('--message-hash ', 'The L1 to L2 message hash.', parseField) + .requiredOption('--secret ', 'The secret used to claim the L1 to L2 message', parseField) + .addOption(pxeOption) + .action(async ({ contractAddress, messageHash, secret, rpcUrl }) => { + const { getL1ToL2MessageWitness } = await import('./get_l1_to_l2_message_witness.js'); + await getL1ToL2MessageWitness(rpcUrl, contractAddress, messageHash, secret, debugLogger, log); + }); + program .command('get-node-info') .description('Gets the information of an aztec node at a URL.') diff --git a/yarn-project/cli/src/utils/portal_manager.ts b/yarn-project/cli/src/utils/portal_manager.ts index 4e25af870e2..a30040f42c9 100644 --- a/yarn-project/cli/src/utils/portal_manager.ts +++ b/yarn-project/cli/src/utils/portal_manager.ts @@ -17,6 +17,7 @@ import { export interface L2Claim { claimSecret: Fr; claimAmount: Fr; + messageHash: `0x${string}`; } function stringifyEthAddress(address: EthAddress | Hex, name?: string) { @@ -94,6 +95,9 @@ export class FeeJuicePortalManager { this.logger.info('Sending L1 Fee Juice to L2 to be claimed publicly'); const args = [to.toString(), amount, claimSecretHash.toString()] as const; + + const { result: messageHash } = await this.contract.simulate.depositToAztecPublic(args); + await this.publicClient.waitForTransactionReceipt({ hash: await this.contract.write.depositToAztecPublic(args), }); @@ -101,6 +105,7 @@ export class FeeJuicePortalManager { return { claimAmount: new Fr(amount), claimSecret, + messageHash, }; } @@ -163,13 +168,34 @@ export class L1PortalManager { await this.tokenManager.approve(amount, this.contract.address, 'TokenPortal'); + let messageHash: `0x${string}`; + if (privateTransfer) { + const secret = Fr.random(); + const secretHash = computeSecretHash(secret); this.logger.info('Sending L1 tokens to L2 to be claimed privately'); + ({ result: messageHash } = await this.contract.simulate.depositToAztecPrivate([ + secretHash.toString(), + amount, + claimSecretHash.toString(), + ])); + await this.publicClient.waitForTransactionReceipt({ - hash: await this.contract.write.depositToAztecPrivate([Fr.ZERO.toString(), amount, claimSecretHash.toString()]), + hash: await this.contract.write.depositToAztecPrivate([ + secretHash.toString(), + amount, + claimSecretHash.toString(), + ]), }); + this.logger.info(`Redeem shield secret: ${secret.toString()}, secret hash: ${secretHash.toString()}`); } else { this.logger.info('Sending L1 tokens to L2 to be claimed publicly'); + ({ result: messageHash } = await this.contract.simulate.depositToAztecPublic([ + to.toString(), + amount, + claimSecretHash.toString(), + ])); + await this.publicClient.waitForTransactionReceipt({ hash: await this.contract.write.depositToAztecPublic([to.toString(), amount, claimSecretHash.toString()]), }); @@ -178,6 +204,7 @@ export class L1PortalManager { return { claimAmount: new Fr(amount), claimSecret, + messageHash, }; } } diff --git a/yarn-project/pxe/src/pxe_http/pxe_http_server.ts b/yarn-project/pxe/src/pxe_http/pxe_http_server.ts index 13239d9ba30..9ea8bfb057a 100644 --- a/yarn-project/pxe/src/pxe_http/pxe_http_server.ts +++ b/yarn-project/pxe/src/pxe_http/pxe_http_server.ts @@ -9,6 +9,7 @@ import { Note, NullifierMembershipWitness, type PXE, + SiblingPath, SimulatedTx, Tx, TxEffect, @@ -50,6 +51,7 @@ export function createPXERpcServer(pxeService: PXE): JsonRpcServer { Note, ExtendedNote, UniqueNote, + SiblingPath, AuthWitness, L2Block, TxEffect, diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index 15a1fc143f7..0d44f9f1135 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -17,6 +17,7 @@ import { type PXE, type PXEInfo, type PrivateKernelProver, + type SiblingPath, SimulatedTx, SimulationError, TaggedLog, @@ -27,11 +28,13 @@ import { type TxReceipt, UnencryptedTxL2Logs, UniqueNote, + getNonNullifiedL1ToL2MessageWitness, isNoirCallStackUnresolved, } from '@aztec/circuit-types'; import { AztecAddress, type CompleteAddress, + type L1_TO_L2_MSG_TREE_HEIGHT, type PartialAddress, computeContractClassId, getContractClassFromArtifact, @@ -355,6 +358,14 @@ export class PXEService implements PXE { return Promise.all(extendedNotes); } + public async getL1ToL2MembershipWitness( + contractAddress: AztecAddress, + messageHash: Fr, + secret: Fr, + ): Promise<[bigint, SiblingPath]> { + return await getNonNullifiedL1ToL2MessageWitness(this.node, contractAddress, messageHash, secret); + } + public async addNote(note: ExtendedNote, scope?: AztecAddress) { const owner = await this.db.getCompleteAddress(note.owner); if (!owner) { diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 5798a955888..7e7dc09ab63 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -5,7 +5,7 @@ import { type NoteStatus, type NullifierMembershipWitness, type PublicDataWitness, - type SiblingPath, + getNonNullifiedL1ToL2MessageWitness, } from '@aztec/circuit-types'; import { type AztecAddress, @@ -16,7 +16,6 @@ import { type KeyValidationRequest, type L1_TO_L2_MSG_TREE_HEIGHT, } from '@aztec/circuits.js'; -import { computeL1ToL2MessageNullifier } from '@aztec/circuits.js/hash'; import { type FunctionArtifact, getFunctionArtifact } from '@aztec/foundation/abi'; import { createDebugLogger } from '@aztec/foundation/log'; import { type KeyStore } from '@aztec/key-store'; @@ -127,25 +126,12 @@ export class SimulatorOracle implements DBOracle { messageHash: Fr, secret: Fr, ): Promise> { - let nullifierIndex: bigint | undefined; - let messageIndex = 0n; - let startIndex = 0n; - let siblingPath: SiblingPath; - - // We iterate over messages until we find one whose nullifier is not in the nullifier tree --> we need to check - // for nullifiers because messages can have duplicates. - do { - const response = await this.aztecNode.getL1ToL2MessageMembershipWitness('latest', messageHash, startIndex); - if (!response) { - throw new Error(`No non-nullified L1 to L2 message found for message hash ${messageHash.toString()}`); - } - [messageIndex, siblingPath] = response; - - const messageNullifier = computeL1ToL2MessageNullifier(contractAddress, messageHash, secret, messageIndex); - nullifierIndex = await this.getNullifierIndex(messageNullifier); - - startIndex = messageIndex + 1n; - } while (nullifierIndex !== undefined); + const [messageIndex, siblingPath] = await getNonNullifiedL1ToL2MessageWitness( + this.aztecNode, + contractAddress, + messageHash, + secret, + ); // Assuming messageIndex is what you intended to use for the index in MessageLoadOracleInputs return new MessageLoadOracleInputs(messageIndex, siblingPath);