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
13 changes: 13 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,17 @@ jobs:
name: "Test"
command: cond_run_script end-to-end ./scripts/run_tests_local e2e_token_contract.test.ts

e2e-private-airdrop:
machine:
image: ubuntu-2004:202010-01
resource_class: large
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_run_script end-to-end ./scripts/run_tests_local e2e_private_airdrop.test.ts

e2e-private-token-contract:
machine:
image: ubuntu-2004:202010-01
Expand Down Expand Up @@ -1459,6 +1470,7 @@ workflows:
- e2e-deploy-contract: *e2e_test
- e2e-lending-contract: *e2e_test
- e2e-token-contract: *e2e_test
- e2e-private-airdrop: *e2e_test
- e2e-private-token-contract: *e2e_test
- e2e-sandbox-example: *e2e_test
- e2e-multi-transfer-contract: *e2e_test
Expand Down Expand Up @@ -1494,6 +1506,7 @@ workflows:
- e2e-deploy-contract
- e2e-lending-contract
- e2e-token-contract
- e2e-private-airdrop
- e2e-private-token-contract
- e2e-sandbox-example
- e2e-multi-transfer-contract
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/acir-simulator/src/client/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export class AcirSimulator {
let abi: FunctionAbiWithDebugMetadata | undefined = undefined;

// Brute force
for (let i = 0; i < MAX_NOTE_FIELDS_LENGTH; i++) {
for (let i = notePreimage.length; i < MAX_NOTE_FIELDS_LENGTH; i++) {
const signature = `compute_note_hash_and_nullifier(Field,Field,Field,[Field;${i}])`;
const selector = FunctionSelector.fromSignature(signature);
try {
Expand Down
95 changes: 60 additions & 35 deletions yarn-project/aztec-rpc/src/aztec_rpc_server/aztec_rpc_server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AcirSimulator,
ExecutionResult,
collectEncryptedLogs,
collectEnqueuedPublicFunctionCalls,
Expand All @@ -7,6 +8,7 @@ import {
} from '@aztec/acir-simulator';
import {
AztecAddress,
CircuitsWasm,
CompleteAddress,
EthAddress,
FunctionData,
Expand All @@ -16,6 +18,7 @@ import {
PartialAddress,
PublicCallRequest,
} from '@aztec/circuits.js';
import { computeCommitmentNonce } from '@aztec/circuits.js/abis';
import { encodeArguments } from '@aztec/foundation/abi';
import { padArrayEnd } from '@aztec/foundation/collection';
import { Fr, Point } from '@aztec/foundation/fields';
Expand All @@ -36,6 +39,7 @@ import {
L2BlockL2Logs,
LogType,
NodeInfo,
NotePreimage,
SimulationError,
Tx,
TxExecutionRequest,
Expand All @@ -61,6 +65,8 @@ import { Synchroniser } from '../synchroniser/index.js';
*/
export class AztecRPCServer implements AztecRPC {
private synchroniser: Synchroniser;
private contractDataOracle: ContractDataOracle;
private simulator: AcirSimulator;
private log: DebugLogger;
private clientInfo: string;

Expand All @@ -73,6 +79,8 @@ export class AztecRPCServer implements AztecRPC {
) {
this.log = createDebugLogger(logSuffix ? `aztec:rpc_server_${logSuffix}` : `aztec:rpc_server`);
this.synchroniser = new Synchroniser(node, db, logSuffix);
this.contractDataOracle = new ContractDataOracle(db, node);
this.simulator = getAcirSimulator(db, node, node, node, keyStore, this.contractDataOracle);

const { version, name } = getPackageInfo();
this.clientInfo = `${name.split('/')[name.split('/').length - 1]}@${version}`;
Expand Down Expand Up @@ -190,6 +198,40 @@ export class AztecRPCServer implements AztecRPC {
return ownerNotes.map(n => n.notePreimage);
}

public async getNoteNonces(
contractAddress: AztecAddress,
storageSlot: Fr,
preimage: NotePreimage,
txHash: TxHash,
): Promise<Fr[]> {
const tx = await this.node.getTx(txHash);
if (!tx) {
throw new Error(`Unknown tx: ${txHash}`);
}

const wasm = await CircuitsWasm.get();

const nonces: Fr[] = [];
const firstNullifier = tx.newNullifiers[0];
const commitments = tx.newCommitments;
for (let i = 0; i < commitments.length; ++i) {
const commitment = commitments[i];
if (commitment.equals(Fr.ZERO)) break;

const nonce = computeCommitmentNonce(wasm, firstNullifier, i);
const { uniqueSiloedNoteHash } = await this.simulator.computeNoteHashAndNullifier(
contractAddress,
nonce,
storageSlot,
preimage.items,
);
if (commitment.equals(uniqueSiloedNoteHash)) {
nonces.push(nonce);
}
}
return nonces;
}

public async getBlock(blockNumber: number): Promise<L2Block | undefined> {
// If a negative block number is provided the current block number is fetched.
if (blockNumber < 0) {
Expand Down Expand Up @@ -314,20 +356,21 @@ export class AztecRPCServer implements AztecRPC {
/**
* Retrieves the simulation parameters required to run an ACIR simulation.
* This includes the contract address, function ABI, portal contract address, and historic tree roots.
* The function uses the given 'contractDataOracle' to fetch the necessary data from the node and user's database.
*
* @param execRequest - The transaction request object containing details of the contract call.
* @param contractDataOracle - An instance of ContractDataOracle used to fetch the necessary data.
* @returns An object containing the contract address, function ABI, portal contract address, and historic tree roots.
*/
async #getSimulationParameters(
execRequest: FunctionCall | TxExecutionRequest,
contractDataOracle: ContractDataOracle,
) {
async #getSimulationParameters(execRequest: FunctionCall | TxExecutionRequest) {
const contractAddress = (execRequest as FunctionCall).to ?? (execRequest as TxExecutionRequest).origin;
const functionAbi = await contractDataOracle.getFunctionAbi(contractAddress, execRequest.functionData.selector);
const debug = await contractDataOracle.getFunctionDebugMetadata(contractAddress, execRequest.functionData.selector);
const portalContract = await contractDataOracle.getPortalContractAddress(contractAddress);
const functionAbi = await this.contractDataOracle.getFunctionAbi(
contractAddress,
execRequest.functionData.selector,
);
const debug = await this.contractDataOracle.getFunctionDebugMetadata(
contractAddress,
execRequest.functionData.selector,
);
const portalContract = await this.contractDataOracle.getPortalContractAddress(contractAddress);

return {
contractAddress,
Expand All @@ -339,22 +382,14 @@ export class AztecRPCServer implements AztecRPC {
};
}

async #simulate(txRequest: TxExecutionRequest, contractDataOracle?: ContractDataOracle): Promise<ExecutionResult> {
async #simulate(txRequest: TxExecutionRequest): Promise<ExecutionResult> {
// TODO - Pause syncing while simulating.
if (!contractDataOracle) {
contractDataOracle = new ContractDataOracle(this.db, this.node);
}

const { contractAddress, functionAbi, portalContract } = await this.#getSimulationParameters(
txRequest,
contractDataOracle,
);

const simulator = getAcirSimulator(this.db, this.node, this.node, this.node, this.keyStore, contractDataOracle);
const { contractAddress, functionAbi, portalContract } = await this.#getSimulationParameters(txRequest);

this.log('Executing simulator...');
try {
const result = await simulator.run(txRequest, functionAbi, contractAddress, portalContract);
const result = await this.simulator.run(txRequest, functionAbi, contractAddress, portalContract);
this.log('Simulation completed!');
return result;
} catch (err) {
Expand All @@ -375,18 +410,11 @@ export class AztecRPCServer implements AztecRPC {
* @returns The simulation result containing the outputs of the unconstrained function.
*/
async #simulateUnconstrained(execRequest: FunctionCall, from?: AztecAddress) {
const contractDataOracle = new ContractDataOracle(this.db, this.node);

const { contractAddress, functionAbi, portalContract } = await this.#getSimulationParameters(
execRequest,
contractDataOracle,
);

const simulator = getAcirSimulator(this.db, this.node, this.node, this.node, this.keyStore, contractDataOracle);
const { contractAddress, functionAbi, portalContract } = await this.#getSimulationParameters(execRequest);

this.log('Executing unconstrained simulator...');
try {
const result = await simulator.runUnconstrained(
const result = await this.simulator.runUnconstrained(
execRequest,
from ?? AztecAddress.ZERO,
functionAbi,
Expand Down Expand Up @@ -419,8 +447,7 @@ export class AztecRPCServer implements AztecRPC {
if (err instanceof SimulationError) {
const callStack = err.getCallStack();
const originalFailingFunction = callStack[callStack.length - 1];
const contractDataOracle = new ContractDataOracle(this.db, this.node);
const debugInfo = await contractDataOracle.getFunctionDebugMetadata(
const debugInfo = await this.contractDataOracle.getFunctionDebugMetadata(
originalFailingFunction.contractAddress,
originalFailingFunction.functionSelector,
);
Expand Down Expand Up @@ -451,12 +478,10 @@ export class AztecRPCServer implements AztecRPC {
async #simulateAndProve(txExecutionRequest: TxExecutionRequest, newContract: ContractDao | undefined) {
// TODO - Pause syncing while simulating.

const contractDataOracle = new ContractDataOracle(this.db, this.node);
const kernelOracle = new KernelOracle(contractDataOracle, this.node);

// Get values that allow us to reconstruct the block hash
const executionResult = await this.#simulate(txExecutionRequest, contractDataOracle);
const executionResult = await this.#simulate(txExecutionRequest);

const kernelOracle = new KernelOracle(this.contractDataOracle, this.node);
const kernelProver = new KernelProver(kernelOracle);
this.log(`Executing kernel prover...`);
const { proof, publicInputs } = await kernelProver.prove(txExecutionRequest.toTxRequest(), executionResult);
Expand Down
71 changes: 53 additions & 18 deletions yarn-project/aztec-rpc/src/note_processor/note_processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { jest } from '@jest/globals';
import { MockProxy, mock } from 'jest-mock-extended';

import { Database, MemoryDB } from '../database/index.js';
import { Database, MemoryDB, NoteSpendingInfoDao } from '../database/index.js';
import { NoteProcessor } from './note_processor.js';

const TXS_PER_BLOCK = 4;
Expand Down Expand Up @@ -46,35 +46,44 @@ describe('Note Processor', () => {
);

// ownedData: [tx1, tx2, ...], the numbers in each tx represents the indices of the note hashes the account owns.
const createEncryptedLogsAndOwnedNoteSpendingInfo = (newNotes: NoteSpendingInfo[], ownedData: number[][]) => {
const txLogs: TxL2Logs[] = [];
const createEncryptedLogsAndOwnedNoteSpendingInfo = (ownedData: number[][], ownedNotes: NoteSpendingInfo[]) => {
const newNotes: NoteSpendingInfo[] = [];
const ownedNoteSpendingInfo: NoteSpendingInfo[] = [];
const txLogs: TxL2Logs[] = [];
let usedOwnedNote = 0;
for (let i = 0; i < TXS_PER_BLOCK; ++i) {
const ownedDataIndices = ownedData[i] || [];
if (ownedDataIndices.some(index => index >= MAX_NEW_COMMITMENTS_PER_TX)) {
throw new Error(`Data index should be less than ${MAX_NEW_COMMITMENTS_PER_TX}.`);
}

const logs: FunctionL2Logs[] = [];
const notesForTx = newNotes.slice(i * MAX_NEW_COMMITMENTS_PER_TX, (i + 1) * MAX_NEW_COMMITMENTS_PER_TX);
notesForTx.forEach((note, noteIndex) => {
for (let noteIndex = 0; noteIndex < MAX_NEW_COMMITMENTS_PER_TX; ++noteIndex) {
const isOwner = ownedDataIndices.includes(noteIndex);
const publicKey = isOwner ? owner.getPublicKey() : Point.random();
const log = note.toEncryptedBuffer(publicKey, grumpkin);
// 1 tx containing 1 function invocation containing 1 log
logs.push(new FunctionL2Logs([log]));
const note = (isOwner && ownedNotes[usedOwnedNote]) || NoteSpendingInfo.random();
usedOwnedNote += note === ownedNotes[usedOwnedNote] ? 1 : 0;
newNotes.push(note);
if (isOwner) {
ownedNoteSpendingInfo.push(note);
}
});
const log = note.toEncryptedBuffer(publicKey, grumpkin);
// 1 tx containing 1 function invocation containing 1 log
logs.push(new FunctionL2Logs([log]));
}
txLogs.push(new TxL2Logs(logs));
}

const encryptedLogs = new L2BlockL2Logs(txLogs);
return { encryptedLogs, ownedNoteSpendingInfo };
return { newNotes, ownedNoteSpendingInfo, encryptedLogs };
};

const mockData = (ownedData: number[][], prependedBlocks = 0, appendedBlocks = 0) => {
const mockData = (
ownedData: number[][],
prependedBlocks = 0,
appendedBlocks = 0,
ownedNotes: NoteSpendingInfo[] = [],
) => {
if (ownedData.length > TXS_PER_BLOCK) {
throw new Error(`Tx size should be less than ${TXS_PER_BLOCK}.`);
}
Expand All @@ -87,17 +96,14 @@ describe('Note Processor', () => {
const block = L2Block.random(firstBlockNum + i, TXS_PER_BLOCK);
block.startPrivateDataTreeSnapshot.nextAvailableLeafIndex = firstBlockDataStartIndex + i * numCommitmentsPerBlock;

const newNotes = Array(numCommitmentsPerBlock).fill(0).map(NoteSpendingInfo.random);

block.newCommitments = newNotes.map(n => computeMockNoteHash(n.notePreimage.items));

const isTargetBlock = i === prependedBlocks;
const { encryptedLogs, ownedNoteSpendingInfo } = createEncryptedLogsAndOwnedNoteSpendingInfo(
newNotes,
const { newNotes, encryptedLogs, ownedNoteSpendingInfo } = createEncryptedLogsAndOwnedNoteSpendingInfo(
isTargetBlock ? ownedData : [],
isTargetBlock ? ownedNotes : [],
);
encryptedLogsArr.push(encryptedLogs);
ownedNoteSpendingInfos.push(...ownedNoteSpendingInfo);
block.newCommitments = newNotes.map(n => computeMockNoteHash(n.notePreimage.items));

const randomBlockContext = new L2BlockContext(block);
blockContexts.push(randomBlockContext);
Expand Down Expand Up @@ -149,12 +155,14 @@ describe('Note Processor', () => {
});

it('should store multiple notes that belong to us', async () => {
const prependedBlocks = 3;
const prependedBlocks = 2;
const appendedBlocks = 1;
const thisBlockDataStartIndex = firstBlockDataStartIndex + prependedBlocks * numCommitmentsPerBlock;

const { blockContexts, encryptedLogsArr, ownedNoteSpendingInfos } = mockData(
[[], [1], [], [0, 2]],
prependedBlocks,
appendedBlocks,
);
await noteProcessor.process(blockContexts, encryptedLogsArr);

Expand Down Expand Up @@ -182,4 +190,31 @@ describe('Note Processor', () => {
const { blockContexts, encryptedLogsArr } = mockData([]);
await noteProcessor.process(blockContexts, encryptedLogsArr);
});

it('should be able to recover two notes with the same preimage', async () => {
const note = NoteSpendingInfo.random();
const note2 = NoteSpendingInfo.random();
// All notes expect one have the same contract address, storage slot, and preimage.
const notes = [note, note, note, note2, note];
const { blockContexts, encryptedLogsArr, ownedNoteSpendingInfos } = mockData([[0, 2], [], [0, 1, 3]], 0, 0, notes);
await noteProcessor.process(blockContexts, encryptedLogsArr);

const addedInfos: NoteSpendingInfoDao[] = addNoteSpendingInfoBatchSpy.mock.calls[0][0];
expect(addedInfos).toEqual([
expect.objectContaining({ ...ownedNoteSpendingInfos[0] }),
expect.objectContaining({ ...ownedNoteSpendingInfos[1] }),
expect.objectContaining({ ...ownedNoteSpendingInfos[2] }),
expect.objectContaining({ ...ownedNoteSpendingInfos[3] }),
expect.objectContaining({ ...ownedNoteSpendingInfos[4] }),
]);
expect(ownedNoteSpendingInfos[0]).toEqual(ownedNoteSpendingInfos[1]);
expect(ownedNoteSpendingInfos[1]).toEqual(ownedNoteSpendingInfos[2]);
expect(ownedNoteSpendingInfos[2]).toEqual(ownedNoteSpendingInfos[4]);
expect(ownedNoteSpendingInfos[3]).not.toEqual(ownedNoteSpendingInfos[4]);

// Check that every note has a different nonce.
const nonceSet = new Set<bigint>();
addedInfos.forEach(info => nonceSet.add(info.nonce.value));
expect(nonceSet.size).toBe(notes.length);
});
});
Loading