Repository navigation
feat(avm-simulator): create cache for pending nullifiers and existence checks #4743
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { Fr } from '@aztec/foundation/fields'; | ||
|
|
||
| import { HostStorage } from './host_storage.js'; | ||
| import { Nullifiers } from './nullifiers.js'; | ||
| import { PublicStorage } from './public_storage.js'; | ||
| import { WorldStateAccessTrace } from './trace.js'; | ||
|
|
||
|
|
@@ -10,6 +11,7 @@ import { WorldStateAccessTrace } from './trace.js'; | |
| export type JournalData = { | ||
| newNoteHashes: Fr[]; | ||
| newNullifiers: Fr[]; | ||
|
|
||
| newL1Messages: Fr[][]; | ||
| newLogs: Fr[][]; | ||
|
|
||
|
|
@@ -38,8 +40,8 @@ export class AvmPersistableStateManager { | |
| /** World State */ | ||
| /** Public storage, including cached writes */ | ||
| private publicStorage: PublicStorage; | ||
| ///** Nullifier set, including cached/recently-emitted nullifiers */ | ||
| //private nullifiers: NullifiersDB; | ||
| /** Nullifier set, including cached/recently-emitted nullifiers */ | ||
| private nullifiers: Nullifiers; | ||
|
|
||
|
dbanks12 marked this conversation as resolved.
Outdated
|
||
| /** World State Access Trace */ | ||
| private trace: WorldStateAccessTrace; | ||
|
|
@@ -51,6 +53,7 @@ export class AvmPersistableStateManager { | |
| constructor(hostStorage: HostStorage, parent?: AvmPersistableStateManager) { | ||
| this.hostStorage = hostStorage; | ||
| this.publicStorage = new PublicStorage(hostStorage.publicStateDb, parent?.publicStorage); | ||
| this.nullifiers = new Nullifiers(hostStorage.commitmentsDb, parent?.nullifiers); | ||
| this.trace = new WorldStateAccessTrace(parent?.trace); | ||
| } | ||
|
|
||
|
|
@@ -69,8 +72,9 @@ export class AvmPersistableStateManager { | |
| * @param value - the value being written to the slot | ||
| */ | ||
| public writeStorage(storageAddress: Fr, slot: Fr, value: Fr) { | ||
| // Cache storage writes for later reference/reads | ||
| this.publicStorage.write(storageAddress, slot, value); | ||
| // We want to keep track of all performed writes in the journal | ||
| // Trace all storage writes (even reverted ones) | ||
| this.trace.tracePublicStorageWrite(storageAddress, slot, value); | ||
| } | ||
|
|
||
|
|
@@ -83,7 +87,7 @@ export class AvmPersistableStateManager { | |
| */ | ||
| public async readStorage(storageAddress: Fr, slot: Fr): Promise<Fr> { | ||
| const [_exists, value] = await this.publicStorage.read(storageAddress, slot); | ||
| // We want to keep track of all performed reads | ||
| // We want to keep track of all performed reads (even reverted ones) | ||
| this.trace.tracePublicStorageRead(storageAddress, slot, value); | ||
| return Promise.resolve(value); | ||
| } | ||
|
|
@@ -92,9 +96,17 @@ export class AvmPersistableStateManager { | |
| this.trace.traceNewNoteHash(/*storageAddress*/ Fr.ZERO, noteHash); | ||
| } | ||
|
|
||
| public writeNullifier(nullifier: Fr) { | ||
| // TODO track pending nullifiers in set per-contract | ||
| this.trace.traceNewNullifier(/*storageAddress*/ Fr.ZERO, nullifier); | ||
| public async checkNullifierExists(storageAddress: Fr, nullifier: Fr) { | ||
| const [exists, _isPending, _leafIndex] = await this.nullifiers.checkExists(storageAddress, nullifier); | ||
| //this.trace.traceNullifierCheck(storageAddress, nullifier, exists, isPending, leafIndex); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this coming up?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep next pr
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or the one after that |
||
| return Promise.resolve(exists); | ||
| } | ||
|
|
||
| public async writeNullifier(storageAddress: Fr, nullifier: Fr) { | ||
| // Cache pending nullifiers for later access | ||
| await this.nullifiers.append(storageAddress, nullifier); | ||
| // Trace all nullifier creations (even reverted ones) | ||
| this.trace.traceNewNullifier(storageAddress, nullifier); | ||
| } | ||
|
|
||
| public writeL1Message(message: Fr[]) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| import { Fr } from '@aztec/foundation/fields'; | ||
|
|
||
| import { MockProxy, mock } from 'jest-mock-extended'; | ||
|
|
||
| import { CommitmentsDB } from '../../index.js'; | ||
| import { Nullifiers } from './nullifiers.js'; | ||
|
|
||
| describe('avm nullifier caching', () => { | ||
| let commitmentsDb: MockProxy<CommitmentsDB>; | ||
| let nullifiers: Nullifiers; | ||
|
|
||
| beforeEach(() => { | ||
| commitmentsDb = mock<CommitmentsDB>(); | ||
| nullifiers = new Nullifiers(commitmentsDb); | ||
| }); | ||
|
|
||
| describe('Nullifier caching and existence checks', () => { | ||
| it('Reading a non-existent nullifier works (gets zero & DNE)', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier = new Fr(2); | ||
| // never written! | ||
| const [exists, isPending, gotIndex] = await nullifiers.checkExists(contractAddress, nullifier); | ||
| // doesn't exist, not pending, index is zero (non-existent) | ||
| expect(exists).toEqual(false); | ||
| expect(isPending).toEqual(false); | ||
| expect(gotIndex).toEqual(Fr.ZERO); | ||
| }); | ||
| it('Should cache nullifier, existence check works after creation', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier = new Fr(2); | ||
|
|
||
| // Write to cache | ||
| await nullifiers.append(contractAddress, nullifier); | ||
| const [exists, isPending, gotIndex] = await nullifiers.checkExists(contractAddress, nullifier); | ||
| // exists (in cache), isPending, index is zero (not in tree) | ||
| expect(exists).toEqual(true); | ||
| expect(isPending).toEqual(true); | ||
| expect(gotIndex).toEqual(Fr.ZERO); | ||
| }); | ||
| it('Existence check works on fallback to host (gets index, exists, not-pending)', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier = new Fr(2); | ||
| const storedLeafIndex = BigInt(420); | ||
|
|
||
| commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); | ||
|
|
||
| const [exists, isPending, gotIndex] = await nullifiers.checkExists(contractAddress, nullifier); | ||
| // exists (in host), not pending, tree index retrieved from host | ||
| expect(exists).toEqual(true); | ||
| expect(isPending).toEqual(false); | ||
| expect(gotIndex).toEqual(gotIndex); | ||
| }); | ||
| it('Existence check works on fallback to parent (gets value, exists, is pending)', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier = new Fr(2); | ||
| const childNullifiers = new Nullifiers(commitmentsDb, nullifiers); | ||
|
|
||
| // Write to parent cache | ||
| await nullifiers.append(contractAddress, nullifier); | ||
| // Get from child cache | ||
| const [exists, isPending, gotIndex] = await childNullifiers.checkExists(contractAddress, nullifier); | ||
| // exists (in parent), isPending, index is zero (not in tree) | ||
| expect(exists).toEqual(true); | ||
| expect(isPending).toEqual(true); | ||
| expect(gotIndex).toEqual(Fr.ZERO); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Nullifier collision failures', () => { | ||
| it('Cant append nullifier that already exists in cache', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier = new Fr(2); // same nullifier for both! | ||
|
|
||
| // Append a nullifier to cache | ||
| await nullifiers.append(contractAddress, nullifier); | ||
| // Can't append again | ||
| await expect(nullifiers.append(contractAddress, nullifier)).rejects.toThrowError( | ||
| `Nullifier ${nullifier} at contract ${contractAddress} already exists in parent cache or host.`, | ||
| ); | ||
| }); | ||
| it('Cant append nullifier that already exists in parent cache', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier = new Fr(2); // same nullifier for both! | ||
|
|
||
| // Append a nullifier to parent | ||
| await nullifiers.append(contractAddress, nullifier); | ||
| const childNullifiers = new Nullifiers(commitmentsDb, nullifiers); | ||
| // Can't append again in child | ||
| await expect(childNullifiers.append(contractAddress, nullifier)).rejects.toThrowError( | ||
| `Nullifier ${nullifier} at contract ${contractAddress} already exists in parent cache or host.`, | ||
| ); | ||
| }); | ||
| it('Cant append nullifier that already exist in host', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier = new Fr(2); // same nullifier for both! | ||
| const storedLeafIndex = BigInt(420); | ||
|
|
||
| // Nullifier exists in host | ||
| commitmentsDb.getNullifierIndex.mockResolvedValue(Promise.resolve(storedLeafIndex)); | ||
| // Can't append to cache | ||
| await expect(nullifiers.append(contractAddress, nullifier)).rejects.toThrowError( | ||
| `Nullifier ${nullifier} at contract ${contractAddress} already exists in parent cache or host.`, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Nullifier cache merging', () => { | ||
| it('Should be able to merge two nullifier caches together', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier0 = new Fr(2); | ||
| const nullifier1 = new Fr(3); | ||
|
|
||
| // Append a nullifier to parent | ||
| await nullifiers.append(contractAddress, nullifier0); | ||
|
|
||
| const childNullifiers = new Nullifiers(commitmentsDb, nullifiers); | ||
| // Append a nullifier to child | ||
| await childNullifiers.append(contractAddress, nullifier1); | ||
|
|
||
| // Parent accepts child's nullifiers | ||
| nullifiers.acceptAndMerge(childNullifiers); | ||
|
|
||
| // After merge, parent has both nullifiers | ||
| const results0 = await nullifiers.checkExists(contractAddress, nullifier0); | ||
| expect(results0).toEqual([/*exists=*/ true, /*isPending=*/ true, /*leafIndex=*/ Fr.ZERO]); | ||
| const results1 = await nullifiers.checkExists(contractAddress, nullifier1); | ||
| expect(results1).toEqual([/*exists=*/ true, /*isPending=*/ true, /*leafIndex=*/ Fr.ZERO]); | ||
| }); | ||
| it('Cant merge two nullifier caches with colliding entries', async () => { | ||
| const contractAddress = new Fr(1); | ||
| const nullifier = new Fr(2); | ||
|
|
||
| // Append a nullifier to parent | ||
| await nullifiers.append(contractAddress, nullifier); | ||
|
|
||
| // Create child cache, don't derive from parent so we can concoct a collision on merge | ||
| const childNullifiers = new Nullifiers(commitmentsDb); | ||
| // Append a nullifier to child | ||
| await childNullifiers.append(contractAddress, nullifier); | ||
|
|
||
| // Parent accepts child's nullifiers | ||
| expect(() => nullifiers.acceptAndMerge(childNullifiers)).toThrowError( | ||
| `Failed to accept child call's nullifiers. Nullifier ${nullifier.toBigInt()} already exists at contract ${contractAddress.toBigInt()}.`, | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.