diff --git a/yarn-project/archiver/src/archiver/config.ts b/yarn-project/archiver/src/archiver/config.ts index fbfa3a0ac9f..46652800e26 100644 --- a/yarn-project/archiver/src/archiver/config.ts +++ b/yarn-project/archiver/src/archiver/config.ts @@ -41,6 +41,11 @@ export interface ArchiverConfig { * The deployed L1 contract addresses */ l1Contracts: L1ContractAddresses; + + /** + * Optional dir to store data. If omitted will store in memory. + */ + dataDirectory?: string; } /** @@ -59,6 +64,7 @@ export function getConfigEnvVars(): ArchiverConfig { API_KEY, INBOX_CONTRACT_ADDRESS, REGISTRY_CONTRACT_ADDRESS, + DATA_DIRECTORY, } = process.env; // Populate the relevant addresses for use by the archiver. const addresses: L1ContractAddresses = { @@ -78,5 +84,6 @@ export function getConfigEnvVars(): ArchiverConfig { searchStartBlock: SEARCH_START_BLOCK ? +SEARCH_START_BLOCK : 0, apiKey: API_KEY, l1Contracts: addresses, + dataDirectory: DATA_DIRECTORY, }; } diff --git a/yarn-project/aztec-node/package.json b/yarn-project/aztec-node/package.json index 0d8ba3e5e48..0ae9b833431 100644 --- a/yarn-project/aztec-node/package.json +++ b/yarn-project/aztec-node/package.json @@ -53,6 +53,7 @@ "@jest/globals": "^29.5.0", "@rushstack/eslint-patch": "^1.1.4", "@types/jest": "^29.5.0", + "@types/leveldown": "^4.0.4", "@types/levelup": "^5.1.2", "@types/memdown": "^3.0.0", "@types/node": "^18.7.23", diff --git a/yarn-project/aztec-node/src/aztec-node/db.ts b/yarn-project/aztec-node/src/aztec-node/db.ts new file mode 100644 index 00000000000..8cc3740152b --- /dev/null +++ b/yarn-project/aztec-node/src/aztec-node/db.ts @@ -0,0 +1,60 @@ +import { LevelDown, default as leveldown } from 'leveldown'; +import { LevelUp, default as levelup } from 'levelup'; +import { MemDown, default as memdown } from 'memdown'; +import { join } from 'node:path'; + +import { AztecNodeConfig } from './config.js'; + +export const createMemDown = () => (memdown as any)() as MemDown; +export const createLevelDown = (path: string) => (leveldown as any)(path) as LevelDown; + +const DB_SUBDIR = 'aztec-node'; +const NODE_METADATA_KEY = '@@aztec_node_metadata'; + +/** + * The metadata for an aztec node. + */ +type NodeMetadata = { + /** + * The address of the rollup contract on L1 + */ + rollupContractAddress: string; +}; + +/** + * Opens the database for the aztec node. + * @param config - The configuration to be used by the aztec node. + * @returns The database for the aztec node. + */ +export async function openDb(config: AztecNodeConfig): Promise { + const nodeMetadata: NodeMetadata = { + rollupContractAddress: config.l1Contracts.rollupAddress.toString(), + }; + + const db = levelup(config.dataDirectory ? createLevelDown(join(config.dataDirectory, DB_SUBDIR)) : createMemDown()); + const prevNodeMetadata = await getNodeMetadata(db); + + // if the rollup addresses are different, wipe the local database and start over + if (nodeMetadata.rollupContractAddress !== prevNodeMetadata.rollupContractAddress) { + await db.clear(); + } + + await db.put(NODE_METADATA_KEY, JSON.stringify(nodeMetadata)); + return db; +} + +/** + * Gets the metadata for the aztec node. + * @param db - The database for the aztec node. + * @returns Node metadata. + */ +async function getNodeMetadata(db: LevelUp): Promise { + try { + const value: Buffer = await db.get(NODE_METADATA_KEY); + return JSON.parse(value.toString('utf-8')); + } catch { + return { + rollupContractAddress: '', + }; + } +} diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 4092bacaf6b..ce7a98a5f22 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -45,12 +45,10 @@ import { getConfigEnvVars as getWorldStateConfig, } from '@aztec/world-state'; -import { default as levelup } from 'levelup'; -import { MemDown, default as memdown } from 'memdown'; +import levelup from 'levelup'; import { AztecNodeConfig } from './config.js'; - -export const createMemDown = () => (memdown as any)() as MemDown; +import { openDb } from './db.js'; /** * The aztec node. @@ -90,10 +88,10 @@ export class AztecNodeService implements AztecNode { const p2pClient = await createP2PClient(config, new InMemoryTxPool(), archiver); // now create the merkle trees and the world state syncher - const merkleTreesDb = levelup(createMemDown()); - const merkleTrees = await MerkleTrees.new(merkleTreesDb, await CircuitsWasm.get()); + const db = await openDb(config); + const merkleTrees = await MerkleTrees.new(db, await CircuitsWasm.get()); const worldStateConfig: WorldStateConfig = getWorldStateConfig(); - const worldStateSynchronizer = new ServerWorldStateSynchronizer(merkleTrees, archiver, worldStateConfig); + const worldStateSynchronizer = await ServerWorldStateSynchronizer.new(db, merkleTrees, archiver, worldStateConfig); // start both and wait for them to sync from the block source await Promise.all([p2pClient.start(), worldStateSynchronizer.start()]); @@ -120,7 +118,7 @@ export class AztecNodeService implements AztecNode { config.chainId, config.version, getGlobalVariableBuilder(config), - merkleTreesDb, + db, ); } diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index d3a2771d06d..73f29b7ef3c 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -47,6 +47,7 @@ "@types/lodash.times": "^4.3.7", "@types/lodash.zip": "^4.2.7", "@types/lodash.zipwith": "^4.2.7", + "@types/memdown": "^3.0.3", "@types/node": "^18.7.23", "jest": "^29.5.0", "koa": "^2.14.2", @@ -57,6 +58,7 @@ "lodash.times": "^4.3.2", "lodash.zip": "^4.2.0", "lodash.zipwith": "^4.2.0", + "memdown": "^6.1.1", "puppeteer": "^21.3.4", "string-argv": "^0.3.2", "ts-jest": "^29.1.0", diff --git a/yarn-project/end-to-end/src/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/integration_l1_publisher.test.ts index 7a70dff9c38..bd09b24cd74 100644 --- a/yarn-project/end-to-end/src/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/integration_l1_publisher.test.ts @@ -1,4 +1,4 @@ -import { createMemDown, getConfigEnvVars } from '@aztec/aztec-node'; +import { getConfigEnvVars } from '@aztec/aztec-node'; import { AztecAddress, GlobalVariables, @@ -33,6 +33,7 @@ import { MerkleTreeOperations, MerkleTrees } from '@aztec/world-state'; import { beforeEach, describe, expect, it } from '@jest/globals'; import { default as levelup } from 'levelup'; +import memdown from 'memdown'; import { Address, Chain, @@ -123,7 +124,7 @@ describe('L1Publisher integration', () => { publicClient, }); - builderDb = await MerkleTrees.new(levelup(createMemDown())).then(t => t.asLatest()); + builderDb = await MerkleTrees.new(levelup((memdown as any)())).then(t => t.asLatest()); const vks = getVerificationKeys(); const simulator = await WasmRollupCircuitSimulator.new(); const prover = new EmptyRollupProver(); diff --git a/yarn-project/world-state/package.json b/yarn-project/world-state/package.json index 67d3c5a7f36..7384420ac37 100644 --- a/yarn-project/world-state/package.json +++ b/yarn-project/world-state/package.json @@ -48,6 +48,7 @@ "@types/memdown": "^3.0.0", "@types/node": "^18.7.23", "jest": "^29.5.0", + "memdown": "^6.1.1", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typescript": "^5.0.4" diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts index c45e00903a1..f5a7f3472e9 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts @@ -24,7 +24,9 @@ import { } from '@aztec/types'; import { jest } from '@jest/globals'; +import levelup from 'levelup'; import times from 'lodash.times'; +import { default as memdown } from 'memdown'; import { MerkleTreeDb, MerkleTrees, WorldStateConfig } from '../index.js'; import { ServerWorldStateSynchronizer } from './server_world_state_synchronizer.js'; @@ -96,12 +98,25 @@ const getMockBlock = (blockNumber: number, newContractsCommitments?: Buffer[]) = return block; }; -const createSynchronizer = (merkleTreeDb: any, rollupSource: any, blockCheckInterval = 100) => { +const createMockDb = () => levelup((memdown as any)()); + +const createSynchronizer = async ( + db: levelup.LevelUp, + merkleTreeDb: any, + rollupSource: any, + blockCheckInterval = 100, +) => { const worldStateConfig: WorldStateConfig = { worldStateBlockCheckIntervalMS: blockCheckInterval, l2QueueSize: 1000, }; - return new ServerWorldStateSynchronizer(merkleTreeDb as MerkleTrees, rollupSource as L2BlockSource, worldStateConfig); + + return await ServerWorldStateSynchronizer.new( + db, + merkleTreeDb as MerkleTrees, + rollupSource as L2BlockSource, + worldStateConfig, + ); }; const log = createDebugLogger('aztec:server_world_state_synchronizer_test'); @@ -152,12 +167,32 @@ describe('server_world_state_synchronizer', () => { expect(status.syncedToL2Block).toBe(LATEST_BLOCK_NUMBER); }; - it('can be constructed', () => { - expect(() => createSynchronizer(merkleTreeDb, rollupSource)).not.toThrow(); + const performSubsequentSync = async (server: ServerWorldStateSynchronizer, count: number) => { + // test initial state + let status = await server.status(); + expect(status.syncedToL2Block).toEqual(LATEST_BLOCK_NUMBER); + expect(status.state).toEqual(WorldStateRunningState.IDLE); + + // create the initial blocks + nextBlocks = Array(count) + .fill(0) + .map((_, index: number) => getMockBlock(LATEST_BLOCK_NUMBER + index + 1)); + + rollupSource.getBlockNumber.mockReturnValueOnce(LATEST_BLOCK_NUMBER + count); + + // start the sync process and await it + await server.start().catch(err => log.error('Sync not completed: ', err)); + + status = await server.status(); + expect(status.syncedToL2Block).toBe(LATEST_BLOCK_NUMBER + count); + }; + + it('can be constructed', async () => { + await expect(createSynchronizer(createMockDb(), merkleTreeDb, rollupSource)).resolves.toBeTruthy(); }); it('updates sync progress', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource); // test initial state let status = await server.status(); @@ -206,7 +241,7 @@ describe('server_world_state_synchronizer', () => { }); it('enables blocking until synced', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource); let currentBlockNumber = 0; const newBlocks = async () => { @@ -237,7 +272,7 @@ describe('server_world_state_synchronizer', () => { }); it('handles multiple calls to start', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource); let currentBlockNumber = 0; const newBlocks = async () => { @@ -264,7 +299,7 @@ describe('server_world_state_synchronizer', () => { }); it('immediately syncs if no new blocks', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource); rollupSource.getBlockNumber.mockImplementationOnce(() => { return Promise.resolve(0); }); @@ -282,7 +317,7 @@ describe('server_world_state_synchronizer', () => { }); it("can't be started if already stopped", async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource); rollupSource.getBlockNumber.mockImplementationOnce(() => { return Promise.resolve(0); }); @@ -297,7 +332,7 @@ describe('server_world_state_synchronizer', () => { it('adds the received L2 blocks', async () => { merkleTreeDb.handleL2Block.mockReset(); - const server = createSynchronizer(merkleTreeDb, rollupSource); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource); const totalBlocks = LATEST_BLOCK_NUMBER + 1; nextBlocks = Array(totalBlocks) .fill(0) @@ -310,7 +345,7 @@ describe('server_world_state_synchronizer', () => { }); it('can immediately sync to latest', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource, 10000); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource, 10000); await performInitialSync(server); @@ -338,7 +373,7 @@ describe('server_world_state_synchronizer', () => { }); it('can immediately sync to a minimum block number', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource, 10000); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource, 10000); await performInitialSync(server); @@ -363,7 +398,7 @@ describe('server_world_state_synchronizer', () => { }); it('can immediately sync to a minimum block in the past', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource, 10000); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource, 10000); await performInitialSync(server); // syncing to a block in the past should succeed @@ -385,7 +420,7 @@ describe('server_world_state_synchronizer', () => { }); it('throws if you try to sync to an unavailable block', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource, 10000); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource, 10000); await performInitialSync(server); @@ -411,7 +446,7 @@ describe('server_world_state_synchronizer', () => { }); it('throws if you try to immediate sync when not running', async () => { - const server = createSynchronizer(merkleTreeDb, rollupSource, 10000); + const server = await createSynchronizer(createMockDb(), merkleTreeDb, rollupSource, 10000); // test initial state const status = await server.status(); @@ -425,4 +460,31 @@ describe('server_world_state_synchronizer', () => { await expect(server.syncImmediate()).rejects.toThrow(`World State is not running, unable to perform sync`); }); + + it('restores the last synced block', async () => { + const db = createMockDb(); + const initialServer = await createSynchronizer(db, merkleTreeDb, rollupSource, 10000); + + await performInitialSync(initialServer); + await initialServer.stop(); + + const server = await createSynchronizer(db, merkleTreeDb, rollupSource, 10000); + const status = await server.status(); + expect(status).toEqual({ + state: WorldStateRunningState.IDLE, + syncedToL2Block: LATEST_BLOCK_NUMBER, + }); + }); + + it('starts syncing from the last block', async () => { + const db = createMockDb(); + const initialServer = await createSynchronizer(db, merkleTreeDb, rollupSource, 10000); + + await performInitialSync(initialServer); + await initialServer.stop(); + + const server = await createSynchronizer(db, merkleTreeDb, rollupSource, 10000); + await performSubsequentSync(server, 2); + await server.stop(); + }); }); diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index 042a6b344ba..2c936fbe264 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -2,11 +2,15 @@ import { SerialQueue } from '@aztec/foundation/fifo'; import { createDebugLogger } from '@aztec/foundation/log'; import { L2Block, L2BlockDownloader, L2BlockSource } from '@aztec/types'; +import { LevelUp } from 'levelup'; + import { MerkleTreeOperations, MerkleTrees } from '../index.js'; import { MerkleTreeOperationsFacade } from '../merkle-tree/merkle_tree_operations_facade.js'; import { WorldStateConfig } from './config.js'; import { WorldStateRunningState, WorldStateStatus, WorldStateSynchronizer } from './world_state_synchronizer.js'; +const DB_KEY_BLOCK_NUMBER = 'latestBlockNumber'; + /** * Synchronizes the world state with the L2 blocks from a L2BlockSource. * The synchronizer will download the L2 blocks from the L2BlockSource and insert the new commitments into the merkle @@ -24,7 +28,8 @@ export class ServerWorldStateSynchronizer implements WorldStateSynchronizer { private runningPromise: Promise = Promise.resolve(); private currentState: WorldStateRunningState = WorldStateRunningState.IDLE; - constructor( + private constructor( + private db: LevelUp, private merkleTreeDb: MerkleTrees, private l2BlockSource: L2BlockSource, config: WorldStateConfig, @@ -45,6 +50,22 @@ export class ServerWorldStateSynchronizer implements WorldStateSynchronizer { return new MerkleTreeOperationsFacade(this.merkleTreeDb, false); } + public static async new( + db: LevelUp, + merkleTreeDb: MerkleTrees, + l2BlockSource: L2BlockSource, + config: WorldStateConfig, + log = createDebugLogger('aztec:world_state'), + ) { + const server = new ServerWorldStateSynchronizer(db, merkleTreeDb, l2BlockSource, config, log); + await server.#init(); + return server; + } + + async #init() { + await this.restoreCurrentL2BlockNumber(); + } + public async start() { if (this.currentState === WorldStateRunningState.STOPPED) { throw new Error('Synchronizer already stopped'); @@ -92,6 +113,7 @@ export class ServerWorldStateSynchronizer implements WorldStateSynchronizer { await this.jobQueue.cancel(); await this.merkleTreeDb.stop(); await this.runningPromise; + await this.commitCurrentL2BlockNumber(); this.setCurrentState(WorldStateRunningState.STOPPED); } @@ -151,6 +173,7 @@ export class ServerWorldStateSynchronizer implements WorldStateSynchronizer { // This request for blocks will timeout after 1 second if no blocks are received const blocks = await this.l2BlockDownloader.getL2Blocks(1); await this.handleL2Blocks(blocks); + await this.commitCurrentL2BlockNumber(); } /** @@ -189,4 +212,23 @@ export class ServerWorldStateSynchronizer implements WorldStateSynchronizer { this.currentState = newState; this.log(`Moved to state ${WorldStateRunningState[this.currentState]}`); } + + private async commitCurrentL2BlockNumber() { + const hex = this.currentL2BlockNum.toString(16); + const encoded = Buffer.from(hex.length % 2 === 1 ? '0' + hex : hex, 'hex'); + + await this.db.put(DB_KEY_BLOCK_NUMBER, encoded); + this.log.debug(`Committed current L2 block number ${this.currentL2BlockNum} to db`); + } + + private async restoreCurrentL2BlockNumber() { + try { + const encoded: Buffer = await this.db.get(DB_KEY_BLOCK_NUMBER); + this.currentL2BlockNum = parseInt(encoded.toString('hex'), 16); + this.log.debug(`Restored current L2 block number ${this.currentL2BlockNum} from db`); + } catch (err) { + this.log.debug('No current L2 block number found in db, starting from 0'); + this.currentL2BlockNum = 0; + } + } } diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index 9cdf42a854b..ab78dc201d1 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -133,6 +133,7 @@ __metadata: "@jest/globals": ^29.5.0 "@rushstack/eslint-patch": ^1.1.4 "@types/jest": ^29.5.0 + "@types/leveldown": ^4.0.4 "@types/levelup": ^5.1.2 "@types/memdown": ^3.0.0 "@types/node": ^18.7.23 @@ -386,6 +387,7 @@ __metadata: "@types/lodash.times": ^4.3.7 "@types/lodash.zip": ^4.2.7 "@types/lodash.zipwith": ^4.2.7 + "@types/memdown": ^3.0.3 "@types/node": ^18.7.23 concurrently: ^7.6.0 jest: ^29.5.0 @@ -397,6 +399,7 @@ __metadata: lodash.times: ^4.3.2 lodash.zip: ^4.2.0 lodash.zipwith: ^4.2.0 + memdown: ^6.1.1 puppeteer: ^21.3.4 string-argv: ^0.3.2 ts-jest: ^29.1.0 @@ -4489,6 +4492,16 @@ __metadata: languageName: node linkType: hard +"@types/leveldown@npm:^4.0.4": + version: 4.0.4 + resolution: "@types/leveldown@npm:4.0.4" + dependencies: + "@types/abstract-leveldown": "*" + "@types/node": "*" + checksum: 630b2d2d1c48f83d14ab0f6c03ad2af1c427675c3692873c4fd3d673bde4140eabc028ce5736ad3d76aeea20769cf53df6f83468a4f0cf28f6d04dbb435edf48 + languageName: node + linkType: hard + "@types/levelup@npm:^5.1.2": version: 5.1.2 resolution: "@types/levelup@npm:5.1.2" @@ -4698,6 +4711,15 @@ __metadata: languageName: node linkType: hard +"@types/memdown@npm:^3.0.3": + version: 3.0.3 + resolution: "@types/memdown@npm:3.0.3" + dependencies: + "@types/abstract-leveldown": "*" + checksum: 9aa311838574b51e4334878102c80c6abc0e1ec14fe00f98c6ee812f3e6b24db53de632e0f763692730e0147e5c41add0ac4257c3deb3dc7abce176708ed73a4 + languageName: node + linkType: hard + "@types/mime@npm:*": version: 3.0.1 resolution: "@types/mime@npm:3.0.1"