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: enforce limits for each side effect type in AVM #8889

Merged
merged 2 commits into from
Oct 2, 2024
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
4 changes: 2 additions & 2 deletions yarn-project/simulator/src/avm/journal/journal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ describe('journal', () => {

it('Should maintain l1 messages', () => {
const recipient = new Fr(1);
persistableState.writeL2ToL1Message(recipient, utxo);
persistableState.writeL2ToL1Message(address, recipient, utxo);
expect(trace.traceNewL2ToL1Message).toHaveBeenCalledTimes(1);
expect(trace.traceNewL2ToL1Message).toHaveBeenCalledWith(recipient, utxo);
expect(trace.traceNewL2ToL1Message).toHaveBeenCalledWith(address, recipient, utxo);
});
});

Expand Down
7 changes: 4 additions & 3 deletions yarn-project/simulator/src/avm/journal/journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,13 @@ export class AvmPersistableStateManager {

/**
* Write an L2 to L1 message.
* @param contractAddress - L2 contract address that created this message
* @param recipient - L1 contract address to send the message to.
* @param content - Message content.
*/
public writeL2ToL1Message(recipient: Fr, content: Fr) {
this.log.debug(`L1Messages(${recipient}) += ${content}.`);
this.trace.traceNewL2ToL1Message(recipient, content);
public writeL2ToL1Message(contractAddress: Fr, recipient: Fr, content: Fr) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not against this but seems unrelated? ok to keep it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had it in a separate PR originally, but merged it because our CI makes small PRs hard.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

will add to pr description

this.log.debug(`L2ToL1Messages(${contractAddress}) += (recipient: ${recipient}, content: ${content}).`);
this.trace.traceNewL2ToL1Message(contractAddress, recipient, content);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ describe('Accrued Substate', () => {
/*contentOffset=*/ value1Offset,
).execute(context);
expect(trace.traceNewL2ToL1Message).toHaveBeenCalledTimes(1);
expect(trace.traceNewL2ToL1Message).toHaveBeenCalledWith(/*recipient=*/ value0, /*content=*/ value1);
expect(trace.traceNewL2ToL1Message).toHaveBeenCalledWith(address, /*recipient=*/ value0, /*content=*/ value1);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export class SendL2ToL1Message extends Instruction {

const recipient = memory.get(recipientOffset).toFr();
const content = memory.get(contentOffset).toFr();
context.persistableState.writeL2ToL1Message(recipient, content);
context.persistableState.writeL2ToL1Message(context.environment.address, recipient, content);

memory.assert({ reads: 2, addressing });
context.machineState.incrementPc();
Expand Down
2 changes: 0 additions & 2 deletions yarn-project/simulator/src/public/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ export interface PublicExecutionResult {
*/
allUnencryptedLogs: UnencryptedFunctionL2Logs;

// TODO(dbanks12): add contract instance read requests

Comment on lines -89 to -90
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

GETCONTRACTINSTANCE is already in the hints, and doesn't need a dedicated entry here. It will ultimately just emit a nullifier.

/** The requests to call public functions made by this call. */
publicCallRequests: PublicInnerCallRequest[];
/** The results of nested calls. */
Expand Down
6 changes: 6 additions & 0 deletions yarn-project/simulator/src/public/side_effect_errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class SideEffectLimitReachedError extends Error {
constructor(sideEffectType: string, limit: number) {
super(`Reached the limit on number of '${sideEffectType}' side effects: ${limit}`);
this.name = 'SideEffectLimitReachedError';
}
}
153 changes: 148 additions & 5 deletions yarn-project/simulator/src/public/side_effect_trace.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import { UnencryptedL2Log } from '@aztec/circuit-types';
import { AztecAddress, EthAddress, Gas, L2ToL1Message } from '@aztec/circuits.js';
import {
AztecAddress,
EthAddress,
Gas,
L2ToL1Message,
MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX,
MAX_L2_TO_L1_MSGS_PER_TX,
MAX_NOTE_HASHES_PER_TX,
MAX_NOTE_HASH_READ_REQUESTS_PER_TX,
MAX_NULLIFIERS_PER_TX,
MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX,
MAX_NULLIFIER_READ_REQUESTS_PER_TX,
MAX_PUBLIC_DATA_READS_PER_TX,
MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX,
MAX_UNENCRYPTED_LOGS_PER_TX,
} from '@aztec/circuits.js';
import { Fr } from '@aztec/foundation/fields';
import { SerializableContractInstance } from '@aztec/types/contracts';

import { randomBytes, randomInt } from 'crypto';

import { AvmContractCallResult } from '../avm/avm_contract_call_result.js';
import { initExecutionEnvironment } from '../avm/fixtures/index.js';
import { SideEffectLimitReachedError } from './side_effect_errors.js';
import { PublicSideEffectTrace, type TracedContractInstance } from './side_effect_trace.js';

function randomTracedContractInstance(): TracedContractInstance {
Expand All @@ -24,6 +40,7 @@ describe('Side Effect Trace', () => {
const recipient = Fr.random();
const content = Fr.random();
const log = [Fr.random(), Fr.random(), Fr.random()];
const contractInstance = SerializableContractInstance.empty().withAddress(new Fr(42));

const startGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]);
const endGasLeft = Gas.fromFields([new Fr(randomInt(10000)), new Fr(randomInt(10000))]);
Expand Down Expand Up @@ -190,7 +207,7 @@ describe('Side Effect Trace', () => {
});

it('Should trace new L2ToL1 messages', () => {
trace.traceNewL2ToL1Message(recipient, content);
trace.traceNewL2ToL1Message(address, recipient, content);
expect(trace.getCounter()).toBe(startCounterPlus1);

const pxResult = toPxResult(trace);
Expand Down Expand Up @@ -221,8 +238,7 @@ describe('Side Effect Trace', () => {
expect(trace.getCounter()).toBe(startCounterPlus1);

const pxResult = toPxResult(trace);
// TODO(dbanks12): process contract instance read requests in public kernel
//expect(pxResult.gotContractInstances).toEqual([instance]);
// TODO(dbanks12): once this emits nullifier read, check here
expect(pxResult.avmCircuitHints.contractInstances.items).toEqual([
{
// hint omits "version" and has "exists" as an Fr
Expand All @@ -231,6 +247,127 @@ describe('Side Effect Trace', () => {
},
]);
});
describe('Maximum accesses', () => {
it('Should enforce maximum number of public storage reads', () => {
for (let i = 0; i < MAX_PUBLIC_DATA_READS_PER_TX; i++) {
trace.tracePublicStorageRead(new Fr(i), new Fr(i), new Fr(i), true, true);
}
expect(() => trace.tracePublicStorageRead(new Fr(42), new Fr(42), new Fr(42), true, true)).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of public storage writes', () => {
for (let i = 0; i < MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX; i++) {
trace.tracePublicStorageWrite(new Fr(i), new Fr(i), new Fr(i));
}
expect(() => trace.tracePublicStorageWrite(new Fr(42), new Fr(42), new Fr(42))).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of note hash checks', () => {
for (let i = 0; i < MAX_NOTE_HASH_READ_REQUESTS_PER_TX; i++) {
trace.traceNoteHashCheck(new Fr(i), new Fr(i), new Fr(i), true);
}
expect(() => trace.traceNoteHashCheck(new Fr(42), new Fr(42), new Fr(42), true)).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of new note hashes', () => {
for (let i = 0; i < MAX_NOTE_HASHES_PER_TX; i++) {
trace.traceNewNoteHash(new Fr(i), new Fr(i));
}
expect(() => trace.traceNewNoteHash(new Fr(42), new Fr(42))).toThrow(SideEffectLimitReachedError);
});

it('Should enforce maximum number of nullifier checks', () => {
for (let i = 0; i < MAX_NULLIFIER_READ_REQUESTS_PER_TX; i++) {
trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), true, true);
}
expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), true, true)).toThrow(
SideEffectLimitReachedError,
);
// NOTE: also cannot do a non-existent check once existent checks have filled up
expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), false, true)).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of nullifier non-existent checks', () => {
for (let i = 0; i < MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX; i++) {
trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), false, true);
}
expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), false, true)).toThrow(
SideEffectLimitReachedError,
);
// NOTE: also cannot do a existent check once non-existent checks have filled up
expect(() => trace.traceNullifierCheck(new Fr(42), new Fr(42), new Fr(42), true, true)).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of new nullifiers', () => {
for (let i = 0; i < MAX_NULLIFIERS_PER_TX; i++) {
trace.traceNewNullifier(new Fr(i), new Fr(i));
}
expect(() => trace.traceNewNullifier(new Fr(42), new Fr(42))).toThrow(SideEffectLimitReachedError);
});

it('Should enforce maximum number of L1 to L2 message checks', () => {
for (let i = 0; i < MAX_L1_TO_L2_MSG_READ_REQUESTS_PER_TX; i++) {
trace.traceL1ToL2MessageCheck(new Fr(i), new Fr(i), new Fr(i), true);
}
expect(() => trace.traceL1ToL2MessageCheck(new Fr(42), new Fr(42), new Fr(42), true)).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of new l2 to l1 messages', () => {
for (let i = 0; i < MAX_L2_TO_L1_MSGS_PER_TX; i++) {
trace.traceNewL2ToL1Message(new Fr(i), new Fr(i), new Fr(i));
}
expect(() => trace.traceNewL2ToL1Message(new Fr(42), new Fr(42), new Fr(42))).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of new logs hashes', () => {
for (let i = 0; i < MAX_UNENCRYPTED_LOGS_PER_TX; i++) {
trace.traceUnencryptedLog(new Fr(i), [new Fr(i), new Fr(i)]);
}
expect(() => trace.traceUnencryptedLog(new Fr(42), [new Fr(42), new Fr(42)])).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of nullifier checks for GETCONTRACTINSTANCE', () => {
for (let i = 0; i < MAX_NULLIFIER_READ_REQUESTS_PER_TX; i++) {
trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), true, true);
}
expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow(
SideEffectLimitReachedError,
);
// NOTE: also cannot do a existent check once non-existent checks have filled up
expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow(
SideEffectLimitReachedError,
);
});

it('Should enforce maximum number of nullifier non-existent checks for GETCONTRACTINSTANCE', () => {
for (let i = 0; i < MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_TX; i++) {
trace.traceNullifierCheck(new Fr(i), new Fr(i), new Fr(i), false, true);
}
expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: false })).toThrow(
SideEffectLimitReachedError,
);
// NOTE: also cannot do a existent check once non-existent checks have filled up
expect(() => trace.traceGetContractInstance({ ...contractInstance, exists: true })).toThrow(
SideEffectLimitReachedError,
);
});
});

it('Should trace nested calls', () => {
const existsDefault = true;
Expand All @@ -244,6 +381,7 @@ describe('Side Effect Trace', () => {
nestedTrace.tracePublicStorageWrite(address, slot, value);
testCounter++;
nestedTrace.traceNoteHashCheck(address, utxo, leafIndex, existsDefault);
// counter does not increment for note hash checks
nestedTrace.traceNewNoteHash(address, utxo);
testCounter++;
nestedTrace.traceNullifierCheck(address, utxo, leafIndex, /*exists=*/ true, isPending);
Expand All @@ -253,10 +391,15 @@ describe('Side Effect Trace', () => {
nestedTrace.traceNewNullifier(address, utxo);
testCounter++;
nestedTrace.traceL1ToL2MessageCheck(address, utxo, leafIndex, existsDefault);
nestedTrace.traceNewL2ToL1Message(recipient, content);
// counter does not increment for l1tol2 message checks
nestedTrace.traceNewL2ToL1Message(address, recipient, content);
testCounter++;
nestedTrace.traceUnencryptedLog(address, log);
testCounter++;
nestedTrace.traceGetContractInstance({ ...contractInstance, exists: true });
testCounter++;
nestedTrace.traceGetContractInstance({ ...contractInstance, exists: false });
testCounter++;

trace.traceNestedCall(nestedTrace, avmEnvironment, startGasLeft, endGasLeft, bytecode, avmCallResults);
// parent trace adopts nested call's counter
Expand Down
Loading
Loading