Skip to content

Commit

Permalink
feat: enabling public and private bridging w/ cli (#8011)
Browse files Browse the repository at this point in the history
Enabling public and private bridging
  • Loading branch information
sklppy88 authored Aug 20, 2024
1 parent 618e251 commit f0f9c73
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 30 deletions.
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);
});

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

0 comments on commit f0f9c73

Please sign in to comment.