Skip to content

Commit

Permalink
chore(avm-simulator): pull out public storage caching and merging fro…
Browse files Browse the repository at this point in the history
…m the state journal (#4730)
  • Loading branch information
dbanks12 authored Feb 23, 2024
1 parent 8968e6e commit b075401
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 114 deletions.
41 changes: 0 additions & 41 deletions yarn-project/simulator/src/avm/journal/journal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,47 +20,6 @@ describe('journal', () => {
});

describe('Public Storage', () => {
it('Should cache write to storage', () => {
// When writing to storage we should write to the storage writes map
const contractAddress = new Fr(1);
const key = new Fr(2);
const value = new Fr(3);

journal.writeStorage(contractAddress, key, value);

const journalUpdates: JournalData = journal.flush();
expect(journalUpdates.currentStorageValue.get(contractAddress.toBigInt())?.get(key.toBigInt())).toEqual(value);
});

it('When reading from storage, should check the parent first', async () => {
// Store a different value in storage vs the cache, and make sure the cache is returned
const contractAddress = new Fr(1);
const key = new Fr(2);
const storedValue = new Fr(420);
const parentValue = new Fr(69);
const cachedValue = new Fr(1337);

publicDb.storageRead.mockResolvedValue(Promise.resolve(storedValue));

const childJournal = new AvmWorldStateJournal(journal.hostStorage, journal);

// Get the cache miss
const cacheMissResult = await childJournal.readStorage(contractAddress, key);
expect(cacheMissResult).toEqual(storedValue);

// Write to storage
journal.writeStorage(contractAddress, key, parentValue);
const parentResult = await childJournal.readStorage(contractAddress, key);
expect(parentResult).toEqual(parentValue);

// Get the parent value
childJournal.writeStorage(contractAddress, key, cachedValue);

// Get the storage value
const cachedResult = await childJournal.readStorage(contractAddress, key);
expect(cachedResult).toEqual(cachedValue);
});

it('When reading from storage, should check the cache first, and be appended to read/write journal', async () => {
// Store a different value in storage vs the cache, and make sure the cache is returned
const contractAddress = new Fr(1);
Expand Down
97 changes: 24 additions & 73 deletions yarn-project/simulator/src/avm/journal/journal.ts
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 { PublicStorage } from './public_storage.js';

/**
* Data held within the journal
Expand Down Expand Up @@ -32,6 +33,9 @@ export class AvmWorldStateJournal {
/** Reference to node storage */
public readonly hostStorage: HostStorage;

/** World State's public storage, including cached writes */
private publicStorage: PublicStorage;

// Reading state - must be tracked for vm execution
// contract address -> key -> value[] (array stored in order of reads)
private storageReads: Map<bigint, Map<bigint, Fr[]>> = new Map();
Expand All @@ -45,14 +49,9 @@ export class AvmWorldStateJournal {
private newL1Messages: Fr[][] = [];
private newLogs: Fr[][] = [];

// contract address -> key -> value
private currentStorageValue: Map<bigint, Map<bigint, Fr>> = new Map();

private parentJournal: AvmWorldStateJournal | undefined;

constructor(hostStorage: HostStorage, parentJournal?: AvmWorldStateJournal) {
this.hostStorage = hostStorage;
this.parentJournal = parentJournal;
this.publicStorage = new PublicStorage(hostStorage.publicStateDb, parentJournal?.publicStorage);
}

/**
Expand All @@ -63,47 +62,29 @@ export class AvmWorldStateJournal {
}

/**
* Write storage into journal
* Write to public storage, journal/trace the write.
*
* @param contractAddress -
* @param key -
* @param value -
* @param storageAddress - the address of the contract whose storage is being written to
* @param slot - the slot in the contract's storage being written to
* @param value - the value being written to the slot
*/
public writeStorage(contractAddress: Fr, key: Fr, value: Fr) {
let contractMap = this.currentStorageValue.get(contractAddress.toBigInt());
if (!contractMap) {
contractMap = new Map();
this.currentStorageValue.set(contractAddress.toBigInt(), contractMap);
}
contractMap.set(key.toBigInt(), value);

public writeStorage(storageAddress: Fr, slot: Fr, value: Fr) {
this.publicStorage.write(storageAddress, slot, value);
// We want to keep track of all performed writes in the journal
this.journalWrite(contractAddress, key, value);
this.journalWrite(storageAddress, slot, value);
}

/**
* Read storage from journal
* Read from host storage on cache miss
* Read from public storage, journal/trace the read.
*
* @param contractAddress -
* @param key -
* @returns current value
* @param storageAddress - the address of the contract whose storage is being read from
* @param slot - the slot in the contract's storage being read from
* @returns the latest value written to slot, or 0 if never written to before
*/
public async readStorage(contractAddress: Fr, key: Fr): Promise<Fr> {
// - We first try this journal's storage cache ( if written to before in this call frame )
// - Then we try the parent journal's storage cache ( if it exists ) ( written to earlier in this block )
// - Finally we try the host storage ( a trip to the database )

// Do not early return as we want to keep track of reads in this.storageReads
let value = this.currentStorageValue.get(contractAddress.toBigInt())?.get(key.toBigInt());
if (!value && this.parentJournal) {
value = await this.parentJournal?.readStorage(contractAddress, key);
}
if (!value) {
value = await this.hostStorage.publicStateDb.storageRead(contractAddress, key);
}

this.journalRead(contractAddress, key, value);
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 in the journal
this.journalRead(storageAddress, slot, value);
return Promise.resolve(value);
}

Expand Down Expand Up @@ -158,15 +139,15 @@ export class AvmWorldStateJournal {
* - Public state journals (r/w logs), with the accessing being appended in chronological order
*/
public acceptNestedWorldState(nestedJournal: AvmWorldStateJournal) {
// Merge Public Storage
this.publicStorage.acceptAndMerge(nestedJournal.publicStorage);

// Merge UTXOs
this.newNoteHashes = this.newNoteHashes.concat(nestedJournal.newNoteHashes);
this.newL1Messages = this.newL1Messages.concat(nestedJournal.newL1Messages);
this.newNullifiers = this.newNullifiers.concat(nestedJournal.newNullifiers);
this.newLogs = this.newLogs.concat(nestedJournal.newLogs);

// Merge Public State
mergeCurrentValueMaps(this.currentStorageValue, nestedJournal.currentStorageValue);

// Merge storage read and write journals
mergeContractJournalMaps(this.storageReads, nestedJournal.storageReads);
mergeContractJournalMaps(this.storageWrites, nestedJournal.storageWrites);
Expand Down Expand Up @@ -195,43 +176,13 @@ export class AvmWorldStateJournal {
newNullifiers: this.newNullifiers,
newL1Messages: this.newL1Messages,
newLogs: this.newLogs,
currentStorageValue: this.currentStorageValue,
currentStorageValue: this.publicStorage.getCache().cachePerContract,
storageReads: this.storageReads,
storageWrites: this.storageWrites,
};
}
}

/**
* Merges two contract current value together
* Where childMap keys will take precedent over the hostMap
* The assumption being that the child map is created at a later time
* And thus contains more up to date information
*
* @param hostMap - The map to be merged into
* @param childMap - The map to be merged from
*/
function mergeCurrentValueMaps(hostMap: Map<bigint, Map<bigint, Fr>>, childMap: Map<bigint, Map<bigint, Fr>>) {
for (const [key, value] of childMap) {
const map1Value = hostMap.get(key);
if (!map1Value) {
hostMap.set(key, value);
} else {
mergeStorageCurrentValueMaps(map1Value, value);
}
}
}

/**
* @param hostMap - The map to be merge into
* @param childMap - The map to be merged from
*/
function mergeStorageCurrentValueMaps(hostMap: Map<bigint, Fr>, childMap: Map<bigint, Fr>) {
for (const [key, value] of childMap) {
hostMap.set(key, value);
}
}

/**
* Merges two contract journalling maps together
* For read maps, we just append the childMap arrays into the host map arrays, as the order is important
Expand Down
116 changes: 116 additions & 0 deletions yarn-project/simulator/src/avm/journal/public_storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Fr } from '@aztec/foundation/fields';

import { MockProxy, mock } from 'jest-mock-extended';

import { PublicStateDB } from '../../index.js';
import { PublicStorage } from './public_storage.js';

describe('avm public storage', () => {
let publicDb: MockProxy<PublicStateDB>;
let publicStorage: PublicStorage;

beforeEach(() => {
publicDb = mock<PublicStateDB>();
publicStorage = new PublicStorage(publicDb);
});

describe('AVM Public Storage', () => {
it('Reading an unwritten slot works (gets zero & DNE)', async () => {
const contractAddress = new Fr(1);
const slot = new Fr(2);
// never written!
const [exists, gotValue] = await publicStorage.read(contractAddress, slot);
// doesn't exist, value is zero
expect(exists).toEqual(false);
expect(gotValue).toEqual(Fr.ZERO);
});
it('Should cache storage write, reading works after write', async () => {
const contractAddress = new Fr(1);
const slot = new Fr(2);
const value = new Fr(3);
// Write to cache
publicStorage.write(contractAddress, slot, value);
const [exists, gotValue] = await publicStorage.read(contractAddress, slot);
// exists because it was previously written
expect(exists).toEqual(true);
expect(gotValue).toEqual(value);
});
it('Reading works on fallback to host (gets value & exists)', async () => {
const contractAddress = new Fr(1);
const slot = new Fr(2);
const storedValue = new Fr(420);
// ensure that fallback to host gets a value
publicDb.storageRead.mockResolvedValue(Promise.resolve(storedValue));

const [exists, gotValue] = await publicStorage.read(contractAddress, slot);
// it exists in the host, so it must've been written before
expect(exists).toEqual(true);
expect(gotValue).toEqual(storedValue);
});
it('Reading works on fallback to parent (gets value & exists)', async () => {
const contractAddress = new Fr(1);
const slot = new Fr(2);
const value = new Fr(3);
const childStorage = new PublicStorage(publicDb, publicStorage);

publicStorage.write(contractAddress, slot, value);
const [exists, gotValue] = await childStorage.read(contractAddress, slot);
// exists because it was previously written!
expect(exists).toEqual(true);
expect(gotValue).toEqual(value);
});
it('When reading from storage, should check cache, then parent, then host', async () => {
// Store a different value in storage vs the cache, and make sure the cache is returned
const contractAddress = new Fr(1);
const slot = new Fr(2);
const storedValue = new Fr(420);
const parentValue = new Fr(69);
const cachedValue = new Fr(1337);

publicDb.storageRead.mockResolvedValue(Promise.resolve(storedValue));
const childStorage = new PublicStorage(publicDb, publicStorage);

// Cache miss falls back to host
const [, cacheMissResult] = await childStorage.read(contractAddress, slot);
expect(cacheMissResult).toEqual(storedValue);

// Write to storage
publicStorage.write(contractAddress, slot, parentValue);
// Reading from child should give value written in parent
const [, valueFromParent] = await childStorage.read(contractAddress, slot);
expect(valueFromParent).toEqual(parentValue);

// Now write a value directly in child
childStorage.write(contractAddress, slot, cachedValue);

// Reading should now give the value written in child
const [, cachedResult] = await childStorage.read(contractAddress, slot);
expect(cachedResult).toEqual(cachedValue);
});
});

it('Should be able to merge two public storages together', async () => {
// Checking that child's writes take precedence on marge
const contractAddress = new Fr(1);
const slot = new Fr(2);
// value written initially in parent
const value = new Fr(1);
// value overwritten in child and later merged into parent
const valueT1 = new Fr(2);

// Write initial value to parent
publicStorage.write(contractAddress, slot, value);

const childStorage = new PublicStorage(publicDb, publicStorage);
// Write valueT1 to child
childStorage.write(contractAddress, slot, valueT1);

// Parent accepts child's staged writes
publicStorage.acceptAndMerge(childStorage);

// Read from parent gives latest value written in child before merge (valueT1)
const [exists, result] = await publicStorage.read(contractAddress, slot);
expect(exists).toEqual(true);
expect(result).toEqual(valueT1);
});
});
Loading

0 comments on commit b075401

Please sign in to comment.