Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enabling public and private bridging w/ cli #8011

Merged
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: 2 additions & 0 deletions yarn-project/aztec.js/src/rpc_clients/pxe_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Note,
NullifierMembershipWitness,
type PXE,
SiblingPath,
SimulatedTx,
Tx,
TxEffect,
Expand Down Expand Up @@ -58,6 +59,7 @@ export const createPXEClient = (url: string, fetch = makeFetch([1, 2, 3], false)
TxExecutionRequest,
TxHash,
Buffer32,
SiblingPath,
},
{
EncryptedNoteL2BlockL2Logs,
Expand Down
9 changes: 9 additions & 0 deletions yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type OutgoingNotesFilter,
type PXE,
type PXEInfo,
type SiblingPath,
type SimulatedTx,
type SyncStatus,
type Tx,
Expand All @@ -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';
Expand Down Expand Up @@ -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<typeof L1_TO_L2_MSG_TREE_HEIGHT>]> {
return this.pxe.getL1ToL2MembershipWitness(contractAddress, messageHash, secret);
}
}
16 changes: 16 additions & 0 deletions yarn-project/circuit-types/src/interfaces/pxe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -239,6 +241,20 @@ export interface PXE {
*/
getIncomingNotes(filter: IncomingNotesFilter): Promise<UniqueNote[]>;

/**
* 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<typeof L1_TO_L2_MSG_TREE_HEIGHT>]>;

/**
* Gets outgoing notes of accounts registered in this PXE based on the provided filter.
* @param filter - The filter to apply to the notes.
Expand Down
37 changes: 37 additions & 0 deletions yarn-project/circuit-types/src/messaging/l1_to_l2_message.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<typeof L1_TO_L2_MSG_TREE_HEIGHT>]> {
let nullifierIndex: bigint | undefined;
let messageIndex = 0n;
let startIndex = 0n;
let siblingPath: SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>;

// 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];
}
23 changes: 20 additions & 3 deletions yarn-project/cli-wallet/src/cmds/bridge_fee_juice.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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 = {
Expand All @@ -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;
}
4 changes: 2 additions & 2 deletions yarn-project/cli-wallet/src/cmds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,15 +379,15 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL
address,
secretKey,
rpcUrl,
fields,
body: noteFields,
hash,
} = options;
const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db);
const client = await createCompatibleClient(rpcUrl, debugLogger);
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;
Expand Down
7 changes: 4 additions & 3 deletions yarn-project/cli/src/cmds/l1/bridge_erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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`);
}
}
25 changes: 25 additions & 0 deletions yarn-project/cli/src/cmds/pxe/get_l1_to_l2_message_witness.ts
Original file line number Diff line number Diff line change
@@ -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]}
`,
);
}
13 changes: 13 additions & 0 deletions yarn-project/cli/src/cmds/pxe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
logJson,
parseAztecAddress,
parseEthereumAddress,
parseField,
parseFieldFromHexString,
parseOptionalAztecAddress,
parseOptionalInteger,
Expand Down Expand Up @@ -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 <address>', 'Aztec address of the contract.', parseAztecAddress)
.requiredOption('--message-hash <messageHash>', 'The L1 to L2 message hash.', parseField)
.requiredOption('--secret <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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it exists :D

});

program
.command('get-node-info')
.description('Gets the information of an aztec node at a URL.')
Expand Down
29 changes: 28 additions & 1 deletion yarn-project/cli/src/utils/portal_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
export interface L2Claim {
claimSecret: Fr;
claimAmount: Fr;
messageHash: `0x${string}`;
}

function stringifyEthAddress(address: EthAddress | Hex, name?: string) {
Expand Down Expand Up @@ -94,13 +95,17 @@ 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),
});

return {
claimAmount: new Fr(amount),
claimSecret,
messageHash,
};
}

Expand Down Expand Up @@ -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()]),
});
Expand All @@ -178,6 +204,7 @@ export class L1PortalManager {
return {
claimAmount: new Fr(amount),
claimSecret,
messageHash,
};
}
}
2 changes: 2 additions & 0 deletions yarn-project/pxe/src/pxe_http/pxe_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Note,
NullifierMembershipWitness,
type PXE,
SiblingPath,
SimulatedTx,
Tx,
TxEffect,
Expand Down Expand Up @@ -50,6 +51,7 @@ export function createPXERpcServer(pxeService: PXE): JsonRpcServer {
Note,
ExtendedNote,
UniqueNote,
SiblingPath,
AuthWitness,
L2Block,
TxEffect,
Expand Down
11 changes: 11 additions & 0 deletions yarn-project/pxe/src/pxe_service/pxe_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type PXE,
type PXEInfo,
type PrivateKernelProver,
type SiblingPath,
SimulatedTx,
SimulationError,
TaggedLog,
Expand All @@ -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,
Expand Down Expand Up @@ -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<typeof L1_TO_L2_MSG_TREE_HEIGHT>]> {
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) {
Expand Down
Loading
Loading