From 86bc62134b0fc1711cd542162525c46ea28cdb21 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:24:48 +0000 Subject: [PATCH 01/20] feat(ipc-codegen): napi transport over the generated FFI entry `--lang cpp --server --ffi --napi` emits _napi.cpp, a Node-API addon over _ipc_ffi_entry: call() on the calling thread and callAsync() on the libuv pool, serialized by one mutex as the FFI entry requires. It depends only on the FFI header. A TS package with `--package-transports napi --package-napi-addon ` gains a napi backend (async and sync) that loads the addon from an explicit path, an env var or its arch package. create() tries the process, then napi, then wasm. A package that spawns nothing no longer declares a bin, and arch packages carry the addon via ipcRuntime.nativeFiles. ipc-runtime gains NapiFfiBackend/NapiFfiBackendSync, a resolver for any per-platform native file, and a pickServiceBackend that orders process, napi and wasm, with tests. --- ipc-codegen/src/cpp_codegen.ts | 123 +++++++++ ipc-codegen/src/generate.ts | 78 +++++- ipc-codegen/src/typescript_package_codegen.ts | 254 +++++++++++++++--- .../ts/scripts/prepare_arch_packages.mjs | 60 +++-- ipc-runtime/ts/src/index.ts | 8 + ipc-runtime/ts/src/napi_ffi.ts | 84 ++++++ ipc-runtime/ts/src/service.test.ts | 88 ++++++ ipc-runtime/ts/src/service.ts | 68 +++-- 8 files changed, 681 insertions(+), 82 deletions(-) create mode 100644 ipc-runtime/ts/src/napi_ffi.ts create mode 100644 ipc-runtime/ts/src/service.test.ts diff --git a/ipc-codegen/src/cpp_codegen.ts b/ipc-codegen/src/cpp_codegen.ts index f00f991ab63..6eaf7ac137d 100644 --- a/ipc-codegen/src/cpp_codegen.ts +++ b/ipc-codegen/src/cpp_codegen.ts @@ -819,6 +819,129 @@ IPC_FFI_EXPORT void ${entry}(const uint8_t* input, size_t input_len, uint8_t** o *output = out; *output_len = response.size(); } +`; + } + + /** + * A Node-API addon over the FFI entry (generateFfiHeader/generateFfiSource): the in-process + * native backend a generated TS package's `napi` transport loads. It adds nothing to the + * contract — bytes in, bytes out through `_ipc_ffi_entry` — so it depends only on + * the FFI header, never on the service's types or handlers. + */ + generateNapiSource(): string { + const { prefix } = this.opts; + const ffiHeader = `${toSnakeCase(prefix)}_ffi.hpp`; + const entry = this.ffiSymbol("ipc_ffi_entry"); + const free = this.ffiSymbol("ipc_ffi_free"); + const moduleName = `${toSnakeCase(prefix) || "ipc"}_napi`; + + return `// AUTOGENERATED FILE - DO NOT EDIT +// Node-API addon for ${prefix}: the in-process FFI entry exposed to JavaScript. +// +// call(input: Uint8Array): Buffer runs on the calling thread +// callAsync(input: Uint8Array): Promise runs on the libuv thread pool +// +// The FFI entry is not thread-safe, so every call, from either form, holds one mutex: async +// calls queue behind each other and behind any synchronous call in progress. +#include "${ffiHeader}" + +#include + +#include +#include +#include +#include + +namespace { + +std::mutex& entry_mutex() +{ + static std::mutex mutex; + return mutex; +} + +std::vector run_entry(const uint8_t* input, size_t input_len) +{ + uint8_t* output = nullptr; + size_t output_len = 0; + { + std::lock_guard lock(entry_mutex()); + ${entry}(input, input_len, &output, &output_len); + } + std::vector response(output, output + output_len); + ${free}(output); + return response; +} + +Napi::Uint8Array input_arg(const Napi::CallbackInfo& info) +{ + if (info.Length() != 1 || !info[0].IsTypedArray() || + info[0].As().TypedArrayType() != napi_uint8_array) { + NAPI_THROW(Napi::TypeError::New(info.Env(), "expected one Uint8Array argument"), Napi::Uint8Array()); + } + return info[0].As(); +} + +Napi::Value call(const Napi::CallbackInfo& info) +{ + Napi::Uint8Array input = input_arg(info); + if (info.Env().IsExceptionPending()) { + return info.Env().Undefined(); + } + std::vector response = run_entry(input.Data(), input.ByteLength()); + return Napi::Buffer::Copy(info.Env(), response.data(), response.size()); +} + +class CallWorker : public Napi::AsyncWorker { + public: + CallWorker(Napi::Env env, const Napi::Uint8Array& input) + : Napi::AsyncWorker(env) + , deferred_(Napi::Promise::Deferred::New(env)) + , input_(input.Data(), input.Data() + input.ByteLength()) + {} + + Napi::Promise promise() const { return deferred_.Promise(); } + + void Execute() override { response_ = run_entry(input_.data(), input_.size()); } + + void OnOK() override + { + deferred_.Resolve(Napi::Buffer::Copy(Env(), response_.data(), response_.size())); + } + + void OnError(const Napi::Error& error) override { deferred_.Reject(error.Value()); } + + private: + Napi::Promise::Deferred deferred_; + // Copied: the caller's buffer may be reused or collected before the worker runs. + std::vector input_; + std::vector response_; +}; + +Napi::Value call_async(const Napi::CallbackInfo& info) +{ + Napi::Uint8Array input = input_arg(info); + if (info.Env().IsExceptionPending()) { + return info.Env().Undefined(); + } + auto* worker = new CallWorker(info.Env(), input); + Napi::Promise promise = worker->promise(); + worker->Queue(); // the worker deletes itself once it has settled the promise + return promise; +} + +Napi::Object init(Napi::Env env, Napi::Object exports) +{ + exports.Set("service", Napi::String::New(env, "${prefix}")); + exports.Set("call", Napi::Function::New(env, call, "call")); + exports.Set("callAsync", Napi::Function::New(env, call_async, "callAsync")); + return exports; +} + +} // namespace + +// NOLINTNEXTLINE +NODE_API_MODULE(${moduleName}, init) `; } } diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts index 3e1fdcb5cec..e98ee591998 100644 --- a/ipc-codegen/src/generate.ts +++ b/ipc-codegen/src/generate.ts @@ -74,6 +74,8 @@ interface Args { packageTransports: string; packageIpcPathArgs: string; packageWasmModule: string; + packageNapiAddon: string; + packageNapiAddonEnvVar: string; packageWasmThreadsModule: string; packageWasmWideModule: string; packageWasmWideThreadsModule: string; @@ -83,6 +85,7 @@ interface Args { cppIncludeDir: string; uds: boolean; ffi: boolean; + napi: boolean; stripMethodPrefix: boolean; stripTypePrefix: boolean; } @@ -106,7 +109,7 @@ Optional: --package-name TS package name for --package --binary-name Native service binary name for --package --binary-env-var Env var overriding the binary path for --package - --package-transports Comma-separated transports for --package (uds,shm,wasm) + --package-transports Comma-separated transports for --package (uds,shm,napi,wasm) --package-ipc-path-args Comma-separated binary args for IPC path; use {path} --package-wasm-module = @@ -121,6 +124,12 @@ Optional: --package-wasm-wide-module =, --package-wasm-wide-threads-module = wasm transport: wide-arithmetic builds of the two, likewise, run instead where the engine supports the proposal + --package-napi-addon + napi transport: file name of the Node-API addon built from + the C++ --napi output, shipped in the arch packages + --package-napi-addon-env-var + napi transport: env var overriding the addon path + (default: _PATH) --ipc-runtime-dependency package.json dependency spec for @aztec-foundation/ipc-runtime --prefix Type prefix (auto-detected when >= 2 commands share one) @@ -133,6 +142,9 @@ Optional: --ffi In-process FFI (rust, zig, cpp). With --client: the FFI client backend. With --server: the exported FFI entry (_ipc_ffi_entry) over the dispatch + --napi With --server --ffi (cpp): also emit _napi.cpp, a + Node-API addon over the FFI entry, for a TS package's + napi transport (--package-transports napi) --cpp-namespace C++ namespace (e.g. my::ns) --cpp-wire-namespace Wire types sub-namespace (default: wire) --cpp-include-dir Include path for generated dir (e.g. myservice/generated) @@ -155,6 +167,8 @@ function parseArgs(argv: string[]): Args { packageTransports: "uds", packageIpcPathArgs: "--socket,{path}", packageWasmModule: "", + packageNapiAddon: "", + packageNapiAddonEnvVar: "", packageWasmThreadsModule: "", packageWasmWideModule: "", packageWasmWideThreadsModule: "", @@ -164,6 +178,7 @@ function parseArgs(argv: string[]): Args { cppIncludeDir: "", uds: false, ffi: false, + napi: false, stripMethodPrefix: false, stripTypePrefix: false, }; @@ -215,6 +230,12 @@ function parseArgs(argv: string[]): Args { case "--package-ipc-path-args": args.packageIpcPathArgs = takeValue(); break; + case "--package-napi-addon": + args.packageNapiAddon = takeValue(); + break; + case "--package-napi-addon-env-var": + args.packageNapiAddonEnvVar = takeValue(); + break; case "--package-wasm-module": args.packageWasmModule = takeValue(); break; @@ -245,6 +266,9 @@ function parseArgs(argv: string[]): Args { case "--ffi": args.ffi = true; break; + case "--napi": + args.napi = true; + break; case "--strip-method-prefix": args.stripMethodPrefix = true; break; @@ -290,6 +314,12 @@ function parseArgs(argv: string[]): Args { ); process.exit(1); } + if (args.napi && !(args.lang === "cpp" && args.server && args.ffi)) { + console.error( + `--napi emits a Node-API addon over the C++ FFI entry; pass --lang cpp --server --ffi`, + ); + process.exit(1); + } if (args.ffi && !args.server && !args.client) { console.error( `--ffi emits the client FFI backend (--client) and/or the exported FFI entry (--server)`, @@ -563,13 +593,29 @@ function generate(args: Args) { // generated from, and lets consumers regenerate bindings. writePackage(schemaFileName, readFileSync(absSchema, "utf-8")); } else { - const binaryName = - args.binaryName || toSnakeCase(prefix).replace(/_/g, "-"); const transports = args.packageTransports .split(",") .map((t) => t.trim()) .filter(Boolean); + const known = ["uds", "shm", "napi", "wasm"]; + const unknown = transports.filter((t) => !known.includes(t)); + if (unknown.length > 0) { + console.error( + `--package-transports: unknown transport(s) ${unknown.join(", ")} (known: ${known.join(", ")})`, + ); + process.exit(1); + } const wasm = transports.includes("wasm"); + const napi = transports.includes("napi"); + const spawned = transports.some((t) => t === "uds" || t === "shm"); + // Only a package that spawns the service has a binary: no bin launcher otherwise. + const binaryName = spawned + ? args.binaryName || toSnakeCase(prefix).replace(/_/g, "-") + : ""; + if (napi && !args.packageNapiAddon) { + console.error(`--package-transports napi needs --package-napi-addon`); + process.exit(1); + } if ( wasm && !args.packageWasmModule && @@ -616,7 +662,9 @@ function generate(args: Args) { prefix, packageName, binaryName, - binaryEnvVar: args.binaryEnvVar || defaultBinaryEnvVar(binaryName), + binaryEnvVar: binaryName + ? args.binaryEnvVar || defaultBinaryEnvVar(binaryName) + : "", ipcRuntimeDependency: args.ipcRuntimeDependency, transports, ipcPathArgs: args.packageIpcPathArgs @@ -627,6 +675,11 @@ function generate(args: Args) { wasmThreadsModule: wasmModules.threads, wasmWideModule: wasmModules.wideSingle, wasmWideThreadsModule: wasmModules.wideThreads, + napiAddon: napi ? args.packageNapiAddon : undefined, + napiAddonEnvVar: napi + ? args.packageNapiAddonEnvVar || + defaultBinaryEnvVar(args.packageNapiAddon.replace(/\.node$/, "")) + : undefined, }); writePackage("package.json", packageGen.generatePackageJson()); writePackage("tsconfig.json", packageGen.generateTsconfig()); @@ -636,10 +689,15 @@ function generate(args: Args) { "src/react-native.ts", packageGen.generateReactNativeIndex(), ); - writePackage("src/platform.ts", packageGen.generatePlatform()); - if (transports.some((t) => t !== "wasm")) { + if (spawned || napi) { + writePackage("src/platform.ts", packageGen.generatePlatform()); + } + if (spawned) { writePackage("src/process.ts", packageGen.generateProcess()); } + if (napi) { + writePackage("src/napi.ts", packageGen.generateNapi()); + } if (binaryName) { writePackage("src/bin.ts", packageGen.generateBin()); } @@ -770,6 +828,14 @@ function generate(args: Args) { gen.generateFfiHeader(), ), ); + if (args.napi) { + cppFiles.push( + writeFile( + `${toSnakeCase(prefix)}_napi.cpp`, + gen.generateNapiSource(), + ), + ); + } cppFiles.push( writeFile( `${toSnakeCase(prefix)}_ffi.cpp`, diff --git a/ipc-codegen/src/typescript_package_codegen.ts b/ipc-codegen/src/typescript_package_codegen.ts index b7618e5ac4e..7762f28a401 100644 --- a/ipc-codegen/src/typescript_package_codegen.ts +++ b/ipc-codegen/src/typescript_package_codegen.ts @@ -23,6 +23,10 @@ export interface TypeScriptPackageOptions { /** wasm transport: wide-arithmetic builds of the two, run instead where the engine supports it. */ wasmWideModule?: PackageWasmModule; wasmWideThreadsModule?: PackageWasmModule; + /** napi transport: file name of the Node-API addon over the FFI entry, shipped in the arch packages. */ + napiAddon?: string; + /** napi transport: environment variable that overrides the addon's path. */ + napiAddonEnvVar?: string; } function className(prefix: string): string { @@ -33,6 +37,10 @@ function binaryFinderName(prefix: string): string { return `find${prefix}Binary`; } +function addonFinderName(prefix: string): string { + return `find${prefix}NapiAddon`; +} + function envName(binaryName: string): string { return `${binaryName.replace(/[^a-zA-Z0-9]+/g, "_").toUpperCase()}_PATH`; } @@ -202,8 +210,29 @@ export class TypeScriptPackageCodegen { return this.opts.transports.includes("wasm"); } + private get napi(): boolean { + return this.opts.transports.includes("napi"); + } + private get processTransports(): string[] { - return this.opts.transports.filter((t) => t !== "wasm"); + return this.opts.transports.filter((t) => t !== "wasm" && t !== "napi"); + } + + /** Whether the package ships per-platform native files: the service binary and/or the addon. */ + private get native(): boolean { + return this.process || this.napi; + } + + /** The service's name in prose: its binary where it has one, else the package stem. */ + private get label(): string { + return this.opts.binaryName || packageStem(this.opts.packageName); + } + + private get nativeFiles(): string[] { + return [ + this.process ? this.opts.binaryName : "", + this.napi ? this.opts.napiAddon ?? "" : "", + ].filter(Boolean); } private get process(): boolean { @@ -228,8 +257,10 @@ export { SyncApi } from './generated/sync.js'; // Clean first: tsc would leave behind the output of a source file the generator no // longer emits, and these packages are a couple of seconds to compile. build: "rm -rf dest .tsbuildinfo && tsc -p tsconfig.json", - prepare_arch_packages: "ipc-runtime-prepare-arch-packages", }; + if (this.native) { + scripts.prepare_arch_packages = "ipc-runtime-prepare-arch-packages"; + } const entry = (name: string) => ({ types: `./dest/${name}.d.ts`, default: `./dest/${name}.js`, @@ -266,17 +297,26 @@ export { SyncApi } from './generated/sync.js'; }, files: ["dest/", ...(this.wasm ? ["wasm/"] : []), "README.md"], scripts, + // Read by ipc-runtime-prepare-arch-packages: native files the arch packages carry besides + // the `bin` binary (the napi transport's addon). + ...(this.napi + ? { ipcRuntime: { nativeFiles: [this.opts.napiAddon] } } + : {}), dependencies: { "@aztec-foundation/ipc-runtime": this.opts.ipcRuntimeDependency, msgpackr: "^1.11.2", tslib: "^2.4.0", }, - optionalDependencies: Object.fromEntries( - Object.values(archPackages).map((packageName) => [ - packageName, - "0.1.0", - ]), - ), + ...(this.native + ? { + optionalDependencies: Object.fromEntries( + Object.values(archPackages).map((packageName) => [ + packageName, + "0.1.0", + ]), + ), + } + : {}), devDependencies: { "@types/node": "^22.15.17", typescript: "^5.3.3", @@ -298,17 +338,20 @@ runServiceBinary(BINARY, '${this.opts.packageName}', process.argv.slice(2)); const pkg = { name: `${this.opts.packageName}-${suffix}`, version: "0.1.0", - description: `Native binary for ${this.opts.packageName} (${suffix})`, + description: `Native files for ${this.opts.packageName} (${suffix})`, license: "MIT", os: [os], cpu: [cpu], - files: [this.opts.binaryName], + files: this.nativeFiles, preferUnplugged: true, }; return JSON.stringify(pkg, null, 2) + "\n"; } generateArchPackageManifests(): Array<{ path: string; content: string }> { + if (!this.native) { + return []; + } const stem = packageStem(this.opts.packageName); return ARCH_PACKAGES.map(({ suffix, os, cpu }) => ({ path: `packages/${stem}-${suffix}/package.json`, @@ -376,10 +419,12 @@ ${syncSpawn}`; const { prefix } = this.opts; const process = this.process; const wasm = this.wasm; + const napi = this.napi; const backendType = backends.join(" | ") || "never"; const defaults = [ process && `the spawned process when the '${this.opts.binaryName}' binary resolves`, + napi && "the napi addon when it resolves", wasm && "the wasm module", ] .filter(Boolean) @@ -398,7 +443,7 @@ export interface ${prefix}CreateOptions { logger?: (msg: string) => void; /** Let node exit while the backend is alive (a process that watches its parent, or the wasm workers). */ unref?: boolean; -${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'logger'>;\n` : ""}} +${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${napi ? ` napi?: ${prefix}NapiOptions;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'logger'>;\n` : ""}} /** Options for ${className(prefix)}Sync.create / createBackendSync. */ export interface ${prefix}CreateSyncOptions { @@ -408,15 +453,19 @@ export interface ${prefix}CreateSyncOptions { threads?: number; logger?: (msg: string) => void; unref?: boolean; -${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'worker' | 'logger'>;\n` : ""}} +${process ? ` process?: Omit<${prefix}ProcessOptions, 'threads' | 'logger'>;\n` : ""}${napi ? ` napi?: ${prefix}NapiOptions;\n` : ""}${wasm ? ` wasm?: Omit<${prefix}WasmOptions, 'threads' | 'worker' | 'logger'>;\n` : ""}} `; } /** The service classes over createBackend/createBackendSync, with the direct constructors this entry offers. */ - private serviceClasses(opts: { process: boolean; wasm: boolean }): string { + private serviceClasses(opts: { + process: boolean; + wasm: boolean; + napi: boolean; + }): string { const { prefix } = this.opts; const svc = className(prefix); - const { process, wasm } = opts; + const { process, wasm, napi } = opts; return `/** * The ${prefix} service: the generated API over whichever backend \`create\` chose, forced, or was * given.${process ? " Process lifecycle stays inside the backend and never leaks onto this API." : ""} @@ -439,6 +488,15 @@ ${ ` : "" }${ + napi + ? ` + /** The service over the in-process napi addon (no fallback); calls run on the libuv pool. */ + static async napi(options: ${prefix}NapiOptions = {}): Promise<${svc}> { + return new ${svc}(await createNapiBackend(options)); + } +` + : "" + }${ wasm ? ` /** The service over the in-process wasm module (no fallback). */ @@ -493,6 +551,15 @@ ${ ` : "" }${ + napi + ? ` + /** The service over the in-process napi addon, on the calling thread (no fallback). */ + static async napi(options: ${prefix}NapiOptions = {}): Promise<${svc}Sync> { + return new ${svc}Sync(createNapiBackendSync(options)); + } +` + : "" + }${ wasm ? ` /** The service over the single-threaded in-process wasm module (no fallback). */ @@ -511,10 +578,17 @@ ${ const findBinary = binaryFinderName(prefix); const process = this.process; const wasm = this.wasm; + const napi = this.napi; const shm = this.shm; - const backends = [process && "'process'", wasm && "'wasm'"].filter( - Boolean, - ) as string[]; + const findAddon = addonFinderName(prefix); + const backends = [ + process && "'process'", + napi && "'napi'", + wasm && "'wasm'", + ].filter(Boolean) as string[]; + const platformExports = [process && findBinary, napi && findAddon] + .filter(Boolean) + .join(", "); return `import { type IpcClientAsync, @@ -523,14 +597,13 @@ ${ } from '@aztec-foundation/ipc-runtime'; import { AsyncApi } from './generated/async.js'; import { SyncApi } from './generated/sync.js'; -${process ? `import { type ${prefix}ProcessOptions, spawnProcessBackend${shm ? ", spawnProcessBackendSync" : ""} } from './process.js';\n` : ""}import { ${findBinary} } from './platform.js'; -${wasm ? `import { type ${prefix}WasmOptions, createWasmBackend, createWasmBackendSync } from './wasm.js';\n` : ""} -${this.generatedExports()}${process ? "export * from './process.js';\n" : ""}${wasm ? "export * from './wasm.js';\n" : ""}export { ${findBinary} } from './platform.js'; +${process ? `import { type ${prefix}ProcessOptions, spawnProcessBackend${shm ? ", spawnProcessBackendSync" : ""} } from './process.js';\n` : ""}${platformExports ? `import { ${platformExports} } from './platform.js';\n` : ""}${napi ? `import { type ${prefix}NapiOptions, createNapiBackend, createNapiBackendSync } from './napi.js';\n` : ""}${wasm ? `import { type ${prefix}WasmOptions, createWasmBackend, createWasmBackendSync } from './wasm.js';\n` : ""} +${this.generatedExports()}${process ? "export * from './process.js';\n" : ""}${napi ? "export * from './napi.js';\n" : ""}${wasm ? "export * from './wasm.js';\n" : ""}${platformExports ? `export { ${platformExports} } from './platform.js';` : ""} ${this.createOptionTypes(backends)} /** * The backend \`${className(prefix)}.create\` would use for \`options\`, for facades that wrap the - * generated API themselves. Unset backend: ${process ? `the process when the binary resolves${wasm ? ", falling back to wasm if it cannot be spawned" : ""}` : "the wasm module"}. + * generated API themselves. ${backends.length > 1 ? `Unset backend: the first of ${backends.join(", ")} that is available,\n * falling back along that order when one cannot be created.` : `Unset backend: ${backends[0] ?? "none"}.`} */ export async function createBackend(options: ${prefix}CreateOptions = {}): Promise { const common = { threads: options.threads, logger: options.logger, unref: options.unref }; @@ -546,6 +619,14 @@ ${ ` : "" }${ + napi + ? ` napi: { + available: () => ${findAddon}(options.napi?.addonPath) !== null, + create: () => createNapiBackend(options.napi), + }, +` + : "" + }${ wasm ? ` wasm: { create: () => createWasmBackend({ ...common, ...options.wasm }) }, ` @@ -553,7 +634,7 @@ ${ } }); } -/** The synchronous counterpart of createBackend${shm ? ": the process over shared memory" : ""}${shm && wasm ? ", else " : ""}${wasm ? "the single-threaded wasm module" : ""}. */ +/** The synchronous counterpart of createBackend${[shm && "the process over shared memory", napi && "the napi addon on the calling thread", wasm && "the single-threaded wasm module"].filter(Boolean).map((s, i) => (i === 0 ? ": " : ", else ") + s).join("")}. */ export async function createBackendSync(options: ${prefix}CreateSyncOptions = {}): Promise { const common = { threads: options.threads, logger: options.logger, unref: options.unref }; return pickServiceBackend(options.backend, { @@ -568,6 +649,14 @@ ${ ` : "" }${ + napi + ? ` napi: { + available: () => ${findAddon}(options.napi?.addonPath) !== null, + create: async () => createNapiBackendSync(options.napi), + }, +` + : "" + }${ wasm ? ` wasm: { create: () => createWasmBackendSync({ logger: options.logger, unref: options.unref, ...options.wasm }) }, ` @@ -575,7 +664,7 @@ ${ } }); } -${this.serviceClasses({ process, wasm })}`; +${this.serviceClasses({ process, wasm, napi })}`; } /** Browser entry: the service runs in-process as a wasm module; there is no process to spawn. */ @@ -629,7 +718,7 @@ export async function createBackendSync(options: ${prefix}CreateSyncOptions = {} return createWasmBackendSync({ logger: options.logger, unref: options.unref, ...options.wasm }); } -${this.serviceClasses({ process: false, wasm: true })}`; +${this.serviceClasses({ process: false, wasm: true, napi: false })}`; } /** @@ -726,7 +815,7 @@ export class ${svc}Sync extends SyncApi { export { sharedMemoryAvailable } from '@aztec-foundation/ipc-runtime/wasm'; -/** Options for running the ${this.opts.binaryName} wasm module in-process. */ +/** Options for running the ${this.label} wasm module in-process. */ export interface ${prefix}WasmOptions { /** * Threads to run with (1 = no worker threads; selects the single-thread module). Default: the @@ -783,7 +872,7 @@ export function resolveThreads(threads?: number): number { } /** - * The ${this.opts.binaryName} wasm module in-process: the main instance in a worker by default, wasi + * The ${this.label} wasm module in-process: the main instance in a worker by default, wasi * threads on further workers. The workers are ipc-runtime's own, spawned with the literal * expression bundlers detect, so they ship as worker chunks of the consuming application. */ @@ -836,6 +925,7 @@ ${ffiExports} */ generatePlatform(): string { const findBinary = binaryFinderName(this.opts.prefix); + const findAddon = addonFinderName(this.opts.prefix); const archPackages = archPackageNames(this.opts.packageName); // Keyed as `process.arch`-`process.platform` reads at runtime. const byPlatform = [ @@ -846,21 +936,23 @@ ${ffiExports} ] .map(([key, name]) => ` '${key}': '${name}',`) .join("\n"); - - return `import * as path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { type ServiceBinary, findServiceBinary } from '@aztec-foundation/ipc-runtime'; - + const imports = [ + this.process && "type ServiceBinary", + this.napi && "type ServiceNativeFile", + "findServiceBinary", + ] + .filter(Boolean) + .join(", "); + const binary = this.process + ? ` /** This package's native binary, for ipc-runtime's resolver and process backends. */ export const BINARY: ServiceBinary = { name: '${this.opts.binaryName}', envVar: '${this.opts.binaryEnvVar}', - archPackages: { -${byPlatform} - }, + archPackages: ARCH_PACKAGES, ipcPathArgs: ${JSON.stringify(this.opts.ipcPathArgs)}, instancePrefix: '${toSnakeCase(this.opts.prefix)}', - packageDir: path.join(path.dirname(fileURLToPath(import.meta.url)), '..'), + packageDir: PACKAGE_DIR, }; /** @@ -871,18 +963,98 @@ ${byPlatform} export function ${findBinary}(customPath?: string): string | null { return findServiceBinary(BINARY, customPath); } +` + : ""; + const addon = this.napi + ? ` +/** This package's Node-API addon over the service's FFI entry, for the napi backend. */ +export const NAPI_ADDON: ServiceNativeFile = { + name: '${this.opts.napiAddon}', + envVar: '${this.opts.napiAddonEnvVar}', + archPackages: ARCH_PACKAGES, + packageDir: PACKAGE_DIR, +}; + +/** + * The '${this.opts.napiAddon}' addon to load: an explicit path if given, else + * \`${this.opts.napiAddonEnvVar}\`, else the installed arch package for this platform. Null when none + * of those yields an existing file. + */ +export function ${findAddon}(customPath?: string): string | null { + return findServiceBinary(NAPI_ADDON, customPath); +} +` + : ""; + + return `import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { ${imports} } from '@aztec-foundation/ipc-runtime'; +/** The per-platform packages carrying this package's native files. */ +const ARCH_PACKAGES: Record = { +${byPlatform} +}; + +const PACKAGE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +${binary}${addon} export const ARCH_PACKAGE_STEM = '${packageStem(this.opts.packageName)}'; `; } + /** The napi transport (node only): the generated Node-API addon over the service's FFI entry. */ + generateNapi(): string { + const { prefix } = this.opts; + const findAddon = addonFinderName(prefix); + return `import { + NapiFfiBackend, + NapiFfiBackendSync, + loadNapiFfiAddon, +} from '@aztec-foundation/ipc-runtime'; +import { ${findAddon} } from './platform.js'; + +/** + * Options for the in-process napi backend. The addon is one per process: node caches a native + * module by path, and the FFI entry behind it holds one service context, so every napi backend + * loaded from the same file shares the service's state. + */ +export interface ${prefix}NapiOptions { + /** The addon to load instead of the installed one (else \`${this.opts.napiAddonEnvVar}\`, else the arch package). */ + addonPath?: string; +} + +function loadAddon(options: ${prefix}NapiOptions) { + const addonPath = ${findAddon}(options.addonPath); + if (!addonPath) { + throw new Error( + "${this.opts.packageName}: napi addon '${this.opts.napiAddon}' not found. Install the matching " + + "'${this.opts.packageName}-' package, set ${this.opts.napiAddonEnvVar}, or pass napi.addonPath.", + ); + } + return loadNapiFfiAddon(addonPath, '${prefix}'); +} + +/** The service in-process through its napi addon; each call runs on the libuv thread pool. */ +export async function createNapiBackend(options: ${prefix}NapiOptions = {}): Promise { + return new NapiFfiBackend(loadAddon(options)); +} + +/** The synchronous form: each call runs on, and blocks, the calling thread. */ +export function createNapiBackendSync(options: ${prefix}NapiOptions = {}): NapiFfiBackendSync { + return new NapiFfiBackendSync(loadAddon(options)); +} +`; + } + generateReadme(): string { const svc = className(this.opts.prefix); const process = this.process; const wasm = this.wasm; + const napi = this.napi; const backends = [ process && `- \`'process'\`: spawns the \`${this.opts.binaryName}\` binary (node) and talks to it over ${this.processTransports.join(" or ")}. The binary is resolved from \`${this.opts.binaryEnvVar}\`, an explicit \`process.binaryPath\`, or the installed arch package (one of this package's optional dependencies).`, + napi && + `- \`'napi'\`: runs the service in-process (node) through its Node-API addon, \`${this.opts.napiAddon}\`, a thin wrapper over the service's FFI entry. Asynchronous calls run on the libuv thread pool and the synchronous backend runs on the calling thread; either way calls are serialized. Node loads a native module once per process, so every napi backend in a process shares one service instance. The addon is resolved from \`${this.opts.napiAddonEnvVar}\`, an explicit \`napi.addonPath\`, or the installed arch package.`, wasm && `- \`'wasm'\`: runs the service's wasm module in-process (node and browsers) through \`@aztec-foundation/ipc-runtime/wasm\`: the main instance in a worker, wasi threads on further workers where a shared memory is available (node, or a browser page served with COOP/COEP headers), otherwise the single-thread module. The worker scripts and the module are referenced with \`new URL(..., import.meta.url)\`, so bundlers emit them as chunks and assets of the application, and only the module actually chosen is ever fetched (Vite users: exclude the package from \`optimizeDeps\`). The module ships uncompressed, which is what lets the browser stream it into \`WebAssembly.compileStreaming\` and cache the compiled code between visits; serve it with your host's own compression. Where that is not possible, \`wasm.module\` takes a compressed copy — or bytes, a \`Response\`, or an already compiled \`Module\`.`, `- an object: anything with \`call(bytes)\`/\`destroy()\`, for a transport of your own (a bridge to a natively linked library, for instance).`, @@ -891,6 +1063,7 @@ export const ARCH_PACKAGE_STEM = '${packageStem(this.opts.packageName)}'; .join("\n"); const defaultPolicy = [ process && `the process when the binary resolves`, + napi && `the napi addon when it resolves`, wasm && `the wasm module`, ] .filter(Boolean) @@ -900,6 +1073,7 @@ export const ARCH_PACKAGE_STEM = '${packageStem(this.opts.packageName)}'; : "(the process reads it from `HARDWARE_CONCURRENCY`/`RAYON_NUM_THREADS`)"; const syncForm = [ this.shm && "shared memory for a process", + napi && "the napi addon on the calling thread", wasm && "the single-threaded wasm module on the calling thread", ] .filter(Boolean) @@ -962,11 +1136,15 @@ The package shell (package.json, tsconfig, \`src/*.ts\`, scripts/) is generated; build through the owning project's \`./bootstrap.sh\`, which regenerates and then runs \`npm install --omit=optional && npm run build\`. -To prepare per-architecture binary packages: +${ + this.native + ? `To prepare per-architecture ${this.napi ? "packages" : "binary packages"}${this.napi ? ` (${this.nativeFiles.map((f) => `\`${f}\``).join(" and ")})` : ""}: \`\`\`sh -npm run prepare_arch_packages -- linux-x64=/path/to/${this.opts.binaryName} +npm run prepare_arch_packages -- ${this.nativeFiles.length === 1 && this.process ? `linux-x64=/path/to/${this.opts.binaryName}` : this.nativeFiles.map((f) => `linux-x64:${f}=/path/to/${f}`).join(" ")} \`\`\` -`; +` + : "" +}`; } } diff --git a/ipc-runtime/ts/scripts/prepare_arch_packages.mjs b/ipc-runtime/ts/scripts/prepare_arch_packages.mjs index d2cb9e94f89..1f8ecbdf0f8 100755 --- a/ipc-runtime/ts/scripts/prepare_arch_packages.mjs +++ b/ipc-runtime/ts/scripts/prepare_arch_packages.mjs @@ -1,12 +1,16 @@ #!/usr/bin/env node -// Stage a generated client package's per-platform binary packages: the optional dependencies its -// binary resolver looks for. Run from the package root; the package name, binary name and version -// come from its own package.json, so this is the same script for every service. +// Stage a generated client package's per-platform packages: the optional dependencies its native +// resolver looks for. Run from the package root; the package name, the native files and the +// version come from its own package.json, so this is the same script for every service. // -// Usage: prepare_arch_packages [= ...] +// The native files are the `bin` binary (a package that spawns its service) and any file listed +// in `ipcRuntime.nativeFiles` (the napi transport's addon). +// +// Usage: prepare_arch_packages [= | := ...] // platform: linux-x64 | linux-arm64 | darwin-x64 | darwin-arm64, or the build directory name // (amd64-linux, arm64-linux, amd64-macos, arm64-macos) -// Without an argument for a platform, build// is used when it exists. +// = names the package's only native file, or its binary when it has several. +// Without an argument for a file, build// is used when it exists. import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; @@ -22,53 +26,73 @@ const pkg = JSON.parse(readFileSync('package.json', 'utf8')); const stem = pkg.name.split('/').pop(); // A string bin names its command after the package, which is the form yarn rewrites a one-entry map to on install. const binaryName = typeof pkg.bin === 'string' ? stem : Object.keys(pkg.bin ?? {})[0]; -if (!binaryName) { - console.error(`prepare_arch_packages: ${pkg.name} declares no bin, so it ships no binary`); +const files = [...(binaryName ? [binaryName] : []), ...(pkg.ipcRuntime?.nativeFiles ?? [])]; +if (files.length === 0) { + console.error(`prepare_arch_packages: ${pkg.name} declares no bin and no ipcRuntime.nativeFiles, so it ships nothing native`); process.exit(1); } +// The file a bare `=` names. +const defaultFile = binaryName ?? files[0]; +// Keyed `:`. const overrides = new Map(); for (const arg of process.argv.slice(2)) { const eq = arg.indexOf('='); if (eq < 0) { - console.error('Usage: prepare_arch_packages [= ...]'); + console.error('Usage: prepare_arch_packages [= | := ...]'); console.error('Platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64'); + console.error(`Files: ${files.join(', ')}`); + process.exit(1); + } + const target = arg.slice(0, eq); + const colon = target.indexOf(':'); + const [platform, file] = colon < 0 ? [target, defaultFile] : [target.slice(0, colon), target.slice(colon + 1)]; + if (!files.includes(file)) { + console.error(`prepare_arch_packages: ${pkg.name} ships no native file '${file}' (it has: ${files.join(', ')})`); process.exit(1); } - overrides.set(arg.slice(0, eq), arg.slice(eq + 1)); + overrides.set(`${platform}:${file}`, arg.slice(eq + 1)); } for (const { buildDir, suffix, os, cpu } of PLATFORMS) { const name = `${pkg.name}-${suffix}`; const outDir = join('packages', `${stem}-${suffix}`); - const binaryPath = - overrides.get(suffix) ?? overrides.get(buildDir) ?? join('build', buildDir, binaryName); + const sources = files.map(file => ({ + file, + path: overrides.get(`${suffix}:${file}`) ?? overrides.get(`${buildDir}:${file}`) ?? join('build', buildDir, file), + })); + const present = sources.filter(({ path }) => existsSync(path)); - if (!existsSync(binaryPath)) { - console.log(`Skipping ${name}: no binary at ${binaryPath}`); + if (present.length === 0) { + console.log(`Skipping ${name}: none of ${sources.map(s => s.path).join(', ')} exists`); continue; } + for (const { path } of sources.filter(s => !present.includes(s))) { + console.log(`Warning: ${name} is missing ${path}; the backend that needs it will not resolve on ${suffix}`); + } rmSync(outDir, { recursive: true, force: true }); mkdirSync(outDir, { recursive: true }); - copyFileSync(binaryPath, join(outDir, binaryName)); - chmodSync(join(outDir, binaryName), 0o755); + for (const { file, path } of present) { + copyFileSync(path, join(outDir, file)); + chmodSync(join(outDir, file), 0o755); + } writeFileSync( join(outDir, 'package.json'), JSON.stringify( { name, version: pkg.version, - description: `Native binary for ${pkg.name} (${suffix})`, + description: `Native files for ${pkg.name} (${suffix})`, license: 'MIT', os: [os], cpu: [cpu], - files: [binaryName], + files: present.map(({ file }) => file), preferUnplugged: true, }, null, 2, ) + '\n', ); - console.log(`Staged ${name} from ${binaryPath}`); + console.log(`Staged ${name} from ${present.map(({ path }) => path).join(', ')}`); } diff --git a/ipc-runtime/ts/src/index.ts b/ipc-runtime/ts/src/index.ts index 85712c580a9..640e1987c20 100644 --- a/ipc-runtime/ts/src/index.ts +++ b/ipc-runtime/ts/src/index.ts @@ -34,7 +34,15 @@ export { type Platform, } from "./native_loader.js"; export { + type NapiFfiAddon, + NapiFfiBackend, + NapiFfiBackendSync, + loadNapiFfiAddon, +} from "./napi_ffi.js"; +export { + type ServiceBackendChoice, type ServiceBinary, + type ServiceNativeFile, type ServiceProcessOptions, findServiceBinary, pickServiceBackend, diff --git a/ipc-runtime/ts/src/napi_ffi.ts b/ipc-runtime/ts/src/napi_ffi.ts new file mode 100644 index 00000000000..4696c0f393f --- /dev/null +++ b/ipc-runtime/ts/src/napi_ffi.ts @@ -0,0 +1,84 @@ +import { createRequire } from "node:module"; +import * as path from "node:path"; +import type { IpcClientAsync, IpcClientSync } from "./types.js"; + +/** + * The surface of a Node-API addon generated by ipc-codegen's `--napi` over a service's FFI entry + * (`_napi.cpp`): bytes in, bytes out, on the calling thread or on the libuv pool. The + * addon serializes calls itself, as the FFI entry requires. + */ +export interface NapiFfiAddon { + /** The schema's service name the addon was generated for. */ + service: string; + call(input: Uint8Array): Uint8Array; + callAsync(input: Uint8Array): Promise; +} + +/** + * Load a generated FFI addon from `addonPath`. Node caches native modules by path, so every load of + * one file returns the same addon, and with it the same service state: the FFI entry has one + * context per loaded library, not one per backend. + */ +export function loadNapiFfiAddon( + addonPath: string, + expectedService?: string, +): NapiFfiAddon { + const require = createRequire(import.meta.url); + const addon = require(path.resolve(addonPath)) as NapiFfiAddon; + if ( + typeof addon?.call !== "function" || + typeof addon?.callAsync !== "function" + ) { + throw new Error( + `${addonPath}: not an ipc-codegen FFI addon (no call/callAsync)`, + ); + } + if (expectedService !== undefined && addon.service !== expectedService) { + throw new Error( + `${addonPath}: addon serves '${addon.service}', expected '${expectedService}'`, + ); + } + return addon; +} + +function asUint8Array(buffer: Uint8Array): Uint8Array { + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +} + +/** Asynchronous backend over a generated FFI addon: each call runs on the libuv thread pool. */ +export class NapiFfiBackend implements IpcClientAsync { + private destroyed = false; + + constructor(private readonly addon: NapiFfiAddon) {} + + async call(input: Uint8Array): Promise { + if (this.destroyed) { + throw new Error(`${this.addon.service}: napi backend used after destroy()`); + } + return asUint8Array(await this.addon.callAsync(input)); + } + + /** The addon stays loaded (node cannot unload one); this only stops further calls. */ + destroy(): Promise { + this.destroyed = true; + return Promise.resolve(); + } +} + +/** Synchronous backend over a generated FFI addon: each call runs on, and blocks, the calling thread. */ +export class NapiFfiBackendSync implements IpcClientSync { + private destroyed = false; + + constructor(private readonly addon: NapiFfiAddon) {} + + call(input: Uint8Array): Uint8Array { + if (this.destroyed) { + throw new Error(`${this.addon.service}: napi backend used after destroy()`); + } + return asUint8Array(this.addon.call(input)); + } + + destroy(): void { + this.destroyed = true; + } +} diff --git a/ipc-runtime/ts/src/service.test.ts b/ipc-runtime/ts/src/service.test.ts new file mode 100644 index 00000000000..98f725520c3 --- /dev/null +++ b/ipc-runtime/ts/src/service.test.ts @@ -0,0 +1,88 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { pickServiceBackend } from "./service.js"; +import type { IpcClientAsync } from "./types.js"; + +function backend(name: string): IpcClientAsync & { name: string } { + return { + name, + call: () => Promise.resolve(new Uint8Array()), + destroy: () => Promise.resolve(), + }; +} + +const ok = (name: string, available?: boolean) => ({ + ...(available === undefined ? {} : { available: () => available }), + create: () => Promise.resolve(backend(name)), +}); +const failing = (available?: boolean) => ({ + ...(available === undefined ? {} : { available: () => available }), + create: (): Promise => Promise.reject(new Error("boom")), +}); + +async function picked( + wanted: Parameters[0], + choices: Omit[1], "label">, +): Promise { + const chosen = await pickServiceBackend(wanted as any, { + label: "svc", + ...(choices as any), + }); + return (chosen as any).name; +} + +test("unset: process first when its binary resolves", async () => { + assert.equal( + await picked(undefined, { process: ok("process", true), napi: ok("napi", true), wasm: ok("wasm") }), + "process", + ); +}); + +test("unset: napi when the process is unavailable", async () => { + assert.equal( + await picked(undefined, { process: ok("process", false), napi: ok("napi", true), wasm: ok("wasm") }), + "napi", + ); +}); + +test("unset: wasm when neither native backend is available", async () => { + assert.equal( + await picked(undefined, { process: ok("process", false), napi: ok("napi", false), wasm: ok("wasm") }), + "wasm", + ); +}); + +test("unset: a failing backend falls through to the next", async () => { + const logs: string[] = []; + const chosen = await pickServiceBackend(undefined, { + label: "svc", + napi: failing(true), + wasm: ok("wasm"), + logger: (m) => logs.push(m), + }); + assert.equal((chosen as any).name, "wasm"); + assert.match(logs[0]!, /svc napi unavailable \(boom\); falling back to wasm/); +}); + +test("unset: the last candidate is tried even when it reports unavailable", async () => { + assert.equal(await picked(undefined, { process: ok("process", false) }), "process"); +}); + +test("unset: a failure of the last candidate propagates", async () => { + await assert.rejects(picked(undefined, { napi: failing(true) }), /boom/); +}); + +test("a named backend is forced, with no fallback", async () => { + assert.equal(await picked("napi", { napi: ok("napi", false), wasm: ok("wasm") }), "napi"); + await assert.rejects(picked("napi", { napi: failing(true), wasm: ok("wasm") }), /boom/); +}); + +test("a named backend the host lacks is an error", async () => { + await assert.rejects(picked("napi", { wasm: ok("wasm") }), /svc: no such backend here: napi/); +}); + +test("a backend object is used as is", async () => { + const mine = backend("mine"); + const chosen = await pickServiceBackend(mine, { label: "svc", wasm: ok("wasm") }); + assert.equal(chosen, mine); +}); diff --git a/ipc-runtime/ts/src/service.ts b/ipc-runtime/ts/src/service.ts index 071d2447b7c..d67e9861d53 100644 --- a/ipc-runtime/ts/src/service.ts +++ b/ipc-runtime/ts/src/service.ts @@ -29,6 +29,15 @@ export interface ServiceBinary { packageDir: string; } +/** + * A per-platform native file a generated package ships in its arch packages: the service binary, + * or a Node-API addon over the service's FFI entry. Resolved the same way for both. + */ +export type ServiceNativeFile = Pick< + ServiceBinary, + "name" | "envVar" | "archPackages" | "packageDir" +>; + /** Options a caller may give when the service runs as a spawned process. */ export interface ServiceProcessOptions { binaryPath?: string; @@ -50,7 +59,7 @@ function platformKey(): string { return `${process.arch}-${process.platform}`; } -function archPackageDir(binary: ServiceBinary): string | null { +function archPackageDir(binary: ServiceNativeFile): string | null { const packageName = binary.archPackages[platformKey()]; if (!packageName) { return null; @@ -75,7 +84,7 @@ function archPackageDir(binary: ServiceBinary): string | null { * installed arch package for this platform. Null when none of those yields an existing file. */ export function findServiceBinary( - binary: ServiceBinary, + binary: ServiceNativeFile, customPath?: string, ): string | null { const explicit = customPath ?? process.env[binary.envVar]; @@ -195,43 +204,62 @@ export function runServiceBinary( process.exit(result.status ?? 1); } +/** One backend a host can offer, for pickServiceBackend. */ +export interface ServiceBackendChoice { + /** Whether the default policy should try it; absent means always (it is the last resort). */ + available?: () => boolean; + create: () => Promise; +} + /** - * Which backend to use, given what this host can offer. Unset: the process when its binary - * resolves, else wasm. A name forces one, with no fallback to the other. + * Which backend to use, given what this host can offer. Unset: the first of process, napi, wasm + * whose `available()` holds (or that has no such check), trying the next one if creating it + * fails; the last candidate is tried regardless. A name forces one, with no fallback. */ export async function pickServiceBackend< T extends IpcClientAsync | IpcClientSync, >( - wanted: "process" | "wasm" | T | undefined, + wanted: "process" | "napi" | "wasm" | T | undefined, choices: { label: string; - process?: { available: () => boolean; create: () => Promise }; - wasm?: { create: () => Promise }; + process?: ServiceBackendChoice; + napi?: ServiceBackendChoice; + wasm?: ServiceBackendChoice; logger?: (msg: string) => void; }, ): Promise { if (typeof wanted === "object") { return wanted; } - const { process: proc, wasm } = choices; - if ( - proc && - (wanted === "process" || - (wanted === undefined && (!wasm || proc.available()))) - ) { + const candidates = (["process", "napi", "wasm"] as const) + .map((name) => ({ name, choice: choices[name] })) + .filter( + (c): c is { name: "process" | "napi" | "wasm"; choice: ServiceBackendChoice } => + c.choice !== undefined, + ); + if (wanted !== undefined) { + const forced = candidates.find((c) => c.name === wanted); + if (!forced) { + throw new Error(`${choices.label}: no such backend here: ${String(wanted)}`); + } + return forced.choice.create(); + } + for (let i = 0; i < candidates.length; i++) { + const { name, choice } = candidates[i]!; + const last = i === candidates.length - 1; + if (!last && choice.available && !choice.available()) { + continue; + } try { - return await proc.create(); + return await choice.create(); } catch (err) { - if (wanted === "process" || !wasm) { + if (last) { throw err; } choices.logger?.( - `${choices.label} process unavailable (${(err as Error).message}); falling back to wasm`, + `${choices.label} ${name} unavailable (${(err as Error).message}); falling back to ${candidates[i + 1]!.name}`, ); } } - if (wasm && (wanted === undefined || wanted === "wasm")) { - return wasm.create(); - } - throw new Error(`${choices.label}: no such backend here: ${String(wanted)}`); + throw new Error(`${choices.label}: no backend available`); } From 2c959a566a8a33800ba7a614a2905465abd341bb Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:44:34 +0000 Subject: [PATCH 02/20] feat(bb): wsdb-ref, the reference world state over a wsdb base contract world_state_reference/wsdb_base_schema.jsonc is the world-state contract the protocol needs: tree reads, membership and low-leaf witnesses, appends, sequential inserts, archive updates and checkpoints. Its definitions are copied verbatim from the node world state's schema, so that schema can `extends` this one. ReferenceWorldState adds the archive tree, prefilled genesis leaves and the genesis block header hash to the in-memory trees. It starts at the canonical genesis: the protocol contracts' registration nullifiers, which regenerate_genesis_constants.sh now also writes into genesis_protocol_nullifiers.hpp here. Its tests check the genesis against GENESIS_NULLIFIER_TREE_ROOT, GENESIS_BLOCK_HEADER_HASH and GENESIS_ARCHIVE_ROOT. The wsdb_ref module serves the contract over the generated dispatch and FFI entry: a Node-API addon (wsdb_ref_napi.node) in native builds and a WASI reactor (wsdb-ref.wasm) in wasm builds. Requests the reference does not model (other forks, block history) are answered with errors rather than exceptions, which a wasm build cannot throw. The reference now compiles for wasm32. --- barretenberg/cpp/CMakePresets.json | 11 +- barretenberg/cpp/src/CMakeLists.txt | 7 + .../genesis_protocol_nullifiers.hpp | 56 ++ .../memory_merkle_db.cpp | 24 +- .../memory_merkle_db.hpp | 56 +- .../world_state_reference/merkle_tree_id.cpp | 4 +- .../reference_world_state.cpp | 342 +++++++++++ .../reference_world_state.hpp | 146 +++++ .../reference_world_state.test.cpp | 218 +++++++ .../wsdb_base_schema.jsonc | 270 +++++++++ .../src/barretenberg/wsdb_ref/CMakeLists.txt | 104 ++++ .../barretenberg/wsdb_ref/wsdb_ref.test.cpp | 256 +++++++++ .../wsdb_ref/wsdb_ref_handlers.cpp | 540 ++++++++++++++++++ .../wsdb_ref/wsdb_ref_handlers.hpp | 90 +++ .../scripts/regenerate_genesis_constants.sh | 53 +- 15 files changed, 2125 insertions(+), 52 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp create mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp create mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp create mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp create mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index 5dfec5c889a..288d9b441a9 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -770,7 +770,8 @@ "barretenberg.wasm.gz", "barretenberg-debug.wasm", "bb-avm-sim.wasm", - "avm_ffi_tests" + "avm_ffi_tests", + "wsdb-ref.wasm" ] }, { @@ -803,25 +804,25 @@ "name": "amd64-linux", "configurePreset": "amd64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb_ref_napi"] }, { "name": "arm64-linux", "configurePreset": "arm64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim", "wsdb_ref_napi"] }, { "name": "amd64-macos", "configurePreset": "amd64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb_ref_napi"] }, { "name": "arm64-macos", "configurePreset": "arm64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb_ref_napi"] }, { "name": "amd64-windows", diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index 24f10bdc9f3..fe247f1a8f0 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -121,6 +121,7 @@ add_subdirectory(barretenberg/vm2_stub) # AVM simulator (aztec, world_state_reference, yes yes - FUZZING_AVM # vm2; vm2 defines vm2_sim whatever AVM says) # bb-avm-sim (avm) yes yes - - +# wsdb-ref (wsdb_ref) yes yes - - # wsdb and cdb IPC clients (vm2_wsdb, cdb) yes - - - # ipc-runtime yes yes - - # world_state_reference and api in the archives yes - - - @@ -128,18 +129,21 @@ add_subdirectory(barretenberg/vm2_stub) # The AVM fuzzer (FUZZING_AVM) builds on top of either a fuzzing or a native build. set(BB_WITH_AVM_SIM OFF) set(BB_WITH_AVM_SERVICE OFF) +set(BB_WITH_WSDB_REF OFF) set(BB_WITH_IPC_CLIENTS OFF) set(BB_WITH_IPC_RUNTIME OFF) set(BB_ARCHIVE_WORLD_STATE_REFERENCE OFF) if(WASM) set(BB_WITH_AVM_SIM ON) set(BB_WITH_AVM_SERVICE ON) + set(BB_WITH_WSDB_REF ON) set(BB_WITH_IPC_RUNTIME ON) elseif(FUZZING) set(BB_WITH_AVM_SIM ${FUZZING_AVM}) elseif(NOT BB_LITE) set(BB_WITH_AVM_SIM ON) set(BB_WITH_AVM_SERVICE ON) + set(BB_WITH_WSDB_REF ON) set(BB_WITH_IPC_CLIENTS ON) set(BB_WITH_IPC_RUNTIME ON) set(BB_ARCHIVE_WORLD_STATE_REFERENCE ON) @@ -151,6 +155,9 @@ if(BB_WITH_AVM_SIM) # NOTE: Do not conditionally base this on the AVM flag as it defines a necessary vm2_sim library. add_subdirectory(barretenberg/vm2) endif() +if(BB_WITH_WSDB_REF) + add_subdirectory(barretenberg/wsdb_ref) +endif() if(BB_WITH_IPC_CLIENTS) add_subdirectory(barretenberg/vm2_wsdb) add_subdirectory(barretenberg/cdb) diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp new file mode 100644 index 00000000000..27a578b3f51 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp @@ -0,0 +1,56 @@ +// GENERATED FILE - DO NOT EDIT. +// Regenerate with noir-projects/fnd/scripts/regenerate_genesis_constants.sh. +#pragma once + +#include "barretenberg/ecc/curves/bn254/fr.hpp" + +#include + +namespace bb::world_state { + +/** + * @brief The protocol contracts' registration nullifiers, seeded into the nullifier tree of a production genesis. + * + * Derived from the protocol contract artifacts: per protocol contract, the class registration nullifier siloed by + * ContractClassRegistry and the instance publication nullifier siloed by ContractInstanceRegistry. Sorted ascending + * because the indexed nullifier tree requires unique, strictly increasing prefilled leaves. These determine the + * three roots below, and GENESIS_NULLIFIER_TREE_ROOT, GENESIS_BLOCK_HEADER_HASH and GENESIS_ARCHIVE_ROOT in + * constants.nr, so all of them are rewritten together and never on their own. + */ +inline std::vector genesis_protocol_nullifiers() +{ + return { + bb::fr("0x04287df03953b0d68b0e83e08a89067afc602626defd6002277cd6df34696818"), + bb::fr("0x0d99507b7ecac720c73bf197a0e7366a5ed80c1c1b0afe8ff8c6ecc7b5a7aefe"), + bb::fr("0x0eb50b367fb754d3a7d1238bfc105cc9b391a02e187be69038876ae9a502e877"), + bb::fr("0x16f917edd8e333013e5bb0633f7f484725bd19403854d6112992dccef4961e32"), + bb::fr("0x1cea539e01abaa5db980e7ff52ef0d2a7772310306ac625783ae435756ee326d"), + bb::fr("0x2c3a57c8d7c387652babd36c4d79ab03c0fe593e160315a4dc098f92caf592c3"), + }; +} + +/** + * @brief The genesis roots the seed vector above produces. + * + * Measured from the protocol contract artifacts together with the seeds, so that this package can check its tree + * implementation against them without reaching for the protocol constants: the GENESIS_* macros in + * common/aztec_constants.hpp come from the release named in foundation.pin, which lags this tree whenever the genesis + * moves. constants.nr remains the protocol's source of truth, and is held to this same measurement by the script that + * writes this file. + */ +inline bb::fr genesis_nullifier_tree_root() +{ + return bb::fr("0x15c67f4d7495a669ed57c5a9468533c63ace43f86b787c52edabefec85e4afc0"); +} + +inline bb::fr genesis_block_header_hash() +{ + return bb::fr("0x199b52350e8f18eeb9cd455fd074bc6fa62ea6a8e3253e2c663aa4909bbd5718"); +} + +inline bb::fr genesis_archive_root() +{ + return bb::fr("0x29eb2c527f8d45276430363214e6c8d709ef3f657a3670ebac3179373d41e5c4"); +} + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.cpp index 88c7af336ec..ef90f5736ff 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.cpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.cpp @@ -1,16 +1,19 @@ #include "barretenberg/world_state_reference/memory_merkle_db.hpp" -#include #include #include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/common/throw_or_abort.hpp" namespace bb::world_state { -MemoryMerkleDB::MemoryMerkleDB(size_t nullifier_tree_prefill, size_t public_data_tree_prefill) +MemoryMerkleDB::MemoryMerkleDB(size_t nullifier_tree_prefill, + size_t public_data_tree_prefill, + std::span prefilled_nullifiers, + std::span prefilled_public_data) : state_{ - .nullifier_tree = NullifierTree(NULLIFIER_TREE_HEIGHT, nullifier_tree_prefill), - .public_data_tree = PublicDataTree(PUBLIC_DATA_TREE_HEIGHT, public_data_tree_prefill), + .nullifier_tree = NullifierTree(NULLIFIER_TREE_HEIGHT, nullifier_tree_prefill, prefilled_nullifiers), + .public_data_tree = PublicDataTree(PUBLIC_DATA_TREE_HEIGHT, public_data_tree_prefill, prefilled_public_data), .note_hash_tree = NoteHashTree(NOTE_HASH_TREE_HEIGHT), .l1_to_l2_message_tree = L1ToL2MessageTree(L1_TO_L2_MSG_TREE_HEIGHT), } @@ -38,8 +41,7 @@ SiblingPath MemoryMerkleDB::get_sibling_path(MerkleTreeId tree_id, index_t leaf_ case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: return state_.l1_to_l2_message_tree.get_sibling_path(leaf_index); default: - throw std::runtime_error("get_sibling_path: unsupported tree id " + - std::to_string(static_cast(tree_id))); + throw_or_abort("get_sibling_path: unsupported tree id " + std::to_string(static_cast(tree_id))); } } @@ -51,8 +53,7 @@ GetLowIndexedLeafResponse MemoryMerkleDB::get_low_indexed_leaf(MerkleTreeId tree case MerkleTreeId::PUBLIC_DATA_TREE: return state_.public_data_tree.get_low_indexed_leaf(value); default: - throw std::runtime_error("get_low_indexed_leaf: unsupported tree id " + - std::to_string(static_cast(tree_id))); + throw_or_abort("get_low_indexed_leaf: unsupported tree id " + std::to_string(static_cast(tree_id))); } } @@ -68,8 +69,7 @@ FF MemoryMerkleDB::get_leaf_value(MerkleTreeId tree_id, index_t leaf_index) cons case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: return state_.l1_to_l2_message_tree.get_leaf_value(leaf_index); default: - throw std::runtime_error("get_leaf_value: unsupported tree id " + - std::to_string(static_cast(tree_id))); + throw_or_abort("get_leaf_value: unsupported tree id " + std::to_string(static_cast(tree_id))); } } @@ -105,7 +105,7 @@ void MemoryMerkleDB::append_leaves(MerkleTreeId tree_id, std::span lea state_.l1_to_l2_message_tree.append_leaves(leaves); break; default: - throw std::runtime_error("append_leaves is only supported for NOTE_HASH_TREE and L1_TO_L2_MESSAGE_TREE"); + throw_or_abort("append_leaves is only supported for NOTE_HASH_TREE and L1_TO_L2_MESSAGE_TREE"); } } @@ -119,7 +119,7 @@ void MemoryMerkleDB::pad_tree(MerkleTreeId tree_id, size_t num_leaves) state_.note_hash_tree.pad_leaves(num_leaves); break; default: - throw std::runtime_error("Padding not supported for tree " + std::to_string(static_cast(tree_id))); + throw_or_abort("Padding not supported for tree " + std::to_string(static_cast(tree_id))); } } diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp index a1f9c392871..579c33fe5f6 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db.hpp @@ -2,11 +2,14 @@ #include #include +#include #include #include +#include #include #include "barretenberg/aztec/aztec_hash_policy.hpp" +#include "barretenberg/common/throw_or_abort.hpp" #include "barretenberg/crypto/merkle_tree/indexed_leaf.hpp" #include "barretenberg/crypto/merkle_tree/response.hpp" #include "barretenberg/numeric/uint256/uint256.hpp" @@ -86,26 +89,59 @@ template class MemoryAppendOnlyTree { * @brief A full-height, sparse, indexed Merkle tree over a SparseMemoryTree. * * Used for the nullifier and public-data trees. The genesis state matches the WorldState's - * ContentAddressedIndexedTree: `initial_size` prefill leaves, all LeafType::padding(i) for i in - * [0, initial_size), linked as an ascending chain. Insert / low-leaf logic mirrors - * IndexedMemoryTree, but the backing is sparse so it works at full tree height. + * ContentAddressedIndexedTree: `initial_size` prefill leaves, LeafType::padding(i) for the first + * initial_size - prefilled.size() of them followed by `prefilled`, linked as an ascending chain. + * Insert / low-leaf logic mirrors IndexedMemoryTree, but the backing is sparse so it works at full + * tree height. */ template class MemoryIndexedTree { public: using Leaf = crypto::merkle_tree::IndexedLeaf; - MemoryIndexedTree(size_t depth, size_t initial_size) + /** + * @brief Why `prefilled` cannot seed a tree of `initial_size` leaves, or nullopt when it can. + * @details The prefill must fit, and its keys must be strictly increasing and above the + * padding keys, since the genesis leaves form one ascending chain. + */ + static std::optional check_prefill(size_t initial_size, std::span prefilled) + { + if (initial_size < 2) { + return "Indexed trees must have initial size > 1"; + } + if (prefilled.size() > initial_size) { + return "Number of prefilled values can't be more than initial size"; + } + size_t num_padding = initial_size - prefilled.size(); + uint256_t previous = + num_padding > 0 ? static_cast(LeafType::padding(num_padding - 1).get_key()) : uint256_t(0); + for (size_t i = 0; i < prefilled.size(); ++i) { + uint256_t key = static_cast(prefilled[i].get_key()); + if ((i > 0 || num_padding > 0) && key <= previous) { + return i == 0 ? "Prefilled values must not be the same as the default values" + : "Prefilled values must be unique and sorted"; + } + previous = key; + } + return std::nullopt; + } + + MemoryIndexedTree(size_t depth, size_t initial_size, std::span prefilled = {}) : tree_(depth) { - BB_ASSERT_GT(initial_size, static_cast(1), "Indexed trees must have initial size > 1"); + auto problem = check_prefill(initial_size, prefilled); + BB_ASSERT(!problem.has_value(), problem.value_or("")); + size_t num_padding = initial_size - prefilled.size(); leaves_.reserve(initial_size); - for (size_t i = 0; i < initial_size; ++i) { + for (size_t i = 0; i < num_padding; ++i) { leaves_.push_back(Leaf(LeafType::padding(i), /*nextIndex=*/0, /*nextKey=*/0)); } + for (const auto& value : prefilled) { + leaves_.push_back(Leaf(value, /*nextIndex=*/0, /*nextKey=*/0)); + } for (size_t i = 0; i < initial_size; ++i) { size_t next_index = i == (initial_size - 1) ? 0 : i + 1; leaves_[i].nextIndex = next_index; - leaves_[i].nextKey = leaves_[next_index].leaf.get_key(); + leaves_[i].nextKey = leaves_[static_cast(next_index)].leaf.get_key(); tree_.update_element(i, HashingPolicy::hash(leaves_[i].get_hash_inputs())); } } @@ -171,7 +207,7 @@ template class MemoryIndexedTree { tree_.update_element(find_low_leaf_result.index, HashingPolicy::hash(low_leaf.get_hash_inputs())); result.insertion_witness_data.emplace_back(Leaf::empty(), 0, SiblingPath{}); } else { - throw std::runtime_error("Leaf is not updateable"); + throw_or_abort("Leaf is not updateable"); } return result; @@ -233,7 +269,9 @@ class MemoryMerkleDB { static constexpr size_t DEFAULT_PUBLIC_DATA_TREE_PREFILL = 128; MemoryMerkleDB(size_t nullifier_tree_prefill = DEFAULT_NULLIFIER_TREE_PREFILL, - size_t public_data_tree_prefill = DEFAULT_PUBLIC_DATA_TREE_PREFILL); + size_t public_data_tree_prefill = DEFAULT_PUBLIC_DATA_TREE_PREFILL, + std::span prefilled_nullifiers = {}, + std::span prefilled_public_data = {}); TreeRoots get_tree_roots() const; diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/merkle_tree_id.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/merkle_tree_id.cpp index f442f0fffac..ae5ef034cbd 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/merkle_tree_id.cpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/merkle_tree_id.cpp @@ -1,6 +1,6 @@ #include "barretenberg/world_state_reference/merkle_tree_id.hpp" -#include +#include "barretenberg/common/throw_or_abort.hpp" #include namespace bb::world_state { @@ -19,7 +19,7 @@ std::string getMerkleTreeName(MerkleTreeId id) case MerkleTreeId::ARCHIVE: return "ArchiveTree"; default: - throw std::invalid_argument("Unknown MerkleTreeId"); + throw_or_abort("Unknown MerkleTreeId"); } } diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp new file mode 100644 index 00000000000..53560b93937 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp @@ -0,0 +1,342 @@ +#include "barretenberg/world_state_reference/reference_world_state.hpp" + +#include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/common/assert.hpp" +#include "barretenberg/crypto/merkle_tree/hash.hpp" +#include "barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp" + +namespace bb::world_state { + +namespace { + +// The trees a state reference covers, in the order the reference reports them. A node world state +// builds its reference from an unordered map, so clients match entries by tree id, not position. +constexpr MerkleTreeId STATE_TREES[] = { + MerkleTreeId::NULLIFIER_TREE, + MerkleTreeId::NOTE_HASH_TREE, + MerkleTreeId::PUBLIC_DATA_TREE, + MerkleTreeId::L1_TO_L2_MESSAGE_TREE, +}; + +TreeSnapshot snapshot_of(const TreeRoots& roots, MerkleTreeId tree_id) +{ + switch (tree_id) { + case MerkleTreeId::NULLIFIER_TREE: + return roots.nullifier_tree; + case MerkleTreeId::NOTE_HASH_TREE: + return roots.note_hash_tree; + case MerkleTreeId::PUBLIC_DATA_TREE: + return roots.public_data_tree; + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return roots.l1_to_l2_message_tree; + default: + throw_or_abort("snapshot_of: not a state-reference tree"); + } +} + +bool is_append_only(MerkleTreeId tree_id) +{ + return tree_id == MerkleTreeId::NOTE_HASH_TREE || tree_id == MerkleTreeId::L1_TO_L2_MESSAGE_TREE || + tree_id == MerkleTreeId::ARCHIVE; +} + +} // namespace + +GenesisConfig GenesisConfig::canonical() +{ + GenesisConfig genesis; + for (const auto& nullifier : genesis_protocol_nullifiers()) { + genesis.prefilled_nullifiers.emplace_back(nullifier); + } + return genesis; +} + +ReferenceWorldState::ReferenceWorldState(const GenesisConfig& genesis) + : genesis_{ .trees = MemoryMerkleDB(INDEXED_TREE_INITIAL_SIZE, + INDEXED_TREE_INITIAL_SIZE, + genesis.prefilled_nullifiers, + genesis.prefilled_public_data), + .archive = ArchiveTree(ARCHIVE_HEIGHT) } + , current_(genesis_) +{ + genesis_block_header_hash_ = + compute_genesis_block_header_hash(genesis_.trees.get_tree_roots(), genesis.genesis_timestamp); + genesis_.archive.append_leaves(std::span(&genesis_block_header_hash_, 1)); + current_ = genesis_; +} + +std::optional ReferenceWorldState::check_genesis(const GenesisConfig& genesis) +{ + if (auto problem = + MemoryMerkleDB::NullifierTree::check_prefill(INDEXED_TREE_INITIAL_SIZE, genesis.prefilled_nullifiers)) { + return "prefilled nullifiers: " + *problem; + } + if (auto problem = + MemoryMerkleDB::PublicDataTree::check_prefill(INDEXED_TREE_INITIAL_SIZE, genesis.prefilled_public_data)) { + return "prefilled public data: " + *problem; + } + return std::nullopt; +} + +std::optional ReferenceWorldState::check_revision(const WorldStateRevision& revision) +{ + if (auto problem = check_fork(revision.forkId)) { + return problem; + } + if (revision.blockNumber != 0 && revision.blockNumber != WorldStateRevision::LATEST) { + return "the reference world state keeps no block history: read block 0 or the latest block (" + + std::to_string(WorldStateRevision::LATEST) + "), not " + std::to_string(revision.blockNumber); + } + return std::nullopt; +} + +std::optional ReferenceWorldState::check_fork(uint64_t fork_id) +{ + if (fork_id != 0) { + return "the reference world state has one fork (0), not " + std::to_string(fork_id); + } + return std::nullopt; +} + +uint32_t ReferenceWorldState::tree_height(MerkleTreeId tree_id) +{ + switch (tree_id) { + case MerkleTreeId::NULLIFIER_TREE: + return NULLIFIER_TREE_HEIGHT; + case MerkleTreeId::NOTE_HASH_TREE: + return NOTE_HASH_TREE_HEIGHT; + case MerkleTreeId::PUBLIC_DATA_TREE: + return PUBLIC_DATA_TREE_HEIGHT; + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return L1_TO_L2_MSG_TREE_HEIGHT; + case MerkleTreeId::ARCHIVE: + return ARCHIVE_HEIGHT; + } + throw_or_abort("tree_height: unknown tree id"); +} + +const ReferenceWorldState::State& ReferenceWorldState::view(const WorldStateRevision& revision) const +{ + BB_ASSERT(!check_revision(revision).has_value()); + // Block 0 is the genesis. The latest block is the genesis too until the first commit, which the + // base contract has no command for; only uncommitted reads see the current state. + if (revision.blockNumber == 0 || !revision.includeUncommitted) { + return genesis_; + } + return current_; +} + +std::vector ReferenceWorldState::get_state_reference(const WorldStateRevision& revision) const +{ + const auto roots = view(revision).trees.get_tree_roots(); + std::vector state; + for (auto tree_id : STATE_TREES) { + const auto snapshot = snapshot_of(roots, tree_id); + state.push_back({ tree_id, snapshot.root, snapshot.next_available_leaf_index }); + } + return state; +} + +std::vector ReferenceWorldState::get_initial_state_reference() const +{ + return get_state_reference(WorldStateRevision{ .blockNumber = 0 }); +} + +TreeState ReferenceWorldState::get_tree_info(const WorldStateRevision& revision, MerkleTreeId tree_id) const +{ + const State& state = view(revision); + const TreeSnapshot snapshot = tree_id == MerkleTreeId::ARCHIVE ? state.archive.get_snapshot() + : snapshot_of(state.trees.get_tree_roots(), tree_id); + return { tree_id, snapshot.root, snapshot.next_available_leaf_index }; +} + +std::optional ReferenceWorldState::get_leaf_value(const WorldStateRevision& revision, + MerkleTreeId tree_id, + uint64_t index) const +{ + BB_ASSERT(is_append_only(tree_id)); + if (index >= get_tree_info(revision, tree_id).size) { + return std::nullopt; + } + const State& state = view(revision); + return tree_id == MerkleTreeId::ARCHIVE ? state.archive.get_leaf_value(index) + : state.trees.get_leaf_value(tree_id, index); +} + +std::optional> ReferenceWorldState::get_nullifier_preimage( + const WorldStateRevision& revision, uint64_t index) const +{ + if (index >= get_tree_info(revision, MerkleTreeId::NULLIFIER_TREE).size) { + return std::nullopt; + } + return view(revision).trees.get_leaf_preimage_nullifier_tree(index); +} + +std::optional> ReferenceWorldState::get_public_data_preimage( + const WorldStateRevision& revision, uint64_t index) const +{ + if (index >= get_tree_info(revision, MerkleTreeId::PUBLIC_DATA_TREE).size) { + return std::nullopt; + } + return view(revision).trees.get_leaf_preimage_public_data_tree(index); +} + +SiblingPath ReferenceWorldState::get_sibling_path(const WorldStateRevision& revision, + MerkleTreeId tree_id, + uint64_t index) const +{ + const State& state = view(revision); + return tree_id == MerkleTreeId::ARCHIVE ? state.archive.get_sibling_path(index) + : state.trees.get_sibling_path(tree_id, index); +} + +std::optional ReferenceWorldState::find_leaf_index(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& leaf, + uint64_t start) const +{ + const uint64_t size = get_tree_info(revision, tree_id).size; + for (uint64_t index = start; index < size; ++index) { + if (get_leaf_value(revision, tree_id, index) == leaf) { + return index; + } + } + return std::nullopt; +} + +std::optional ReferenceWorldState::find_indexed_leaf_index(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key, + uint64_t start) const +{ + const uint64_t size = get_tree_info(revision, tree_id).size; + for (uint64_t index = start; index < size; ++index) { + const FF leaf_key = tree_id == MerkleTreeId::NULLIFIER_TREE + ? get_nullifier_preimage(revision, index)->leaf.get_key() + : get_public_data_preimage(revision, index)->leaf.get_key(); + if (leaf_key == key) { + return index; + } + } + return std::nullopt; +} + +GetLowIndexedLeafResponse ReferenceWorldState::find_low_leaf(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key) const +{ + return view(revision).trees.get_low_indexed_leaf(tree_id, key); +} + +void ReferenceWorldState::append_leaves(MerkleTreeId tree_id, std::span leaves) +{ + BB_ASSERT(is_append_only(tree_id)); + if (tree_id == MerkleTreeId::ARCHIVE) { + current_.archive.append_leaves(leaves); + } else { + current_.trees.append_leaves(tree_id, leaves); + } +} + +SequentialInsertionResult ReferenceWorldState::insert_nullifier(const NullifierLeafValue& leaf) +{ + return current_.trees.insert_indexed_leaves_nullifier_tree(leaf); +} + +SequentialInsertionResult ReferenceWorldState::insert_public_data(const PublicDataLeafValue& leaf) +{ + return current_.trees.insert_indexed_leaves_public_data_tree(leaf); +} + +bool ReferenceWorldState::has_nullifier(const FF& nullifier) const +{ + return current_.trees.get_low_indexed_leaf(MerkleTreeId::NULLIFIER_TREE, nullifier).is_already_present; +} + +bool ReferenceWorldState::state_matches(std::span state) const +{ + const auto current = get_state_reference(WorldStateRevision{ .includeUncommitted = true }); + if (state.size() != current.size()) { + return false; + } + for (const auto& expected : current) { + bool found = false; + for (const auto& given : state) { + found = found || given == expected; + } + if (!found) { + return false; + } + } + return true; +} + +void ReferenceWorldState::update_archive(const FF& block_header_hash) +{ + current_.archive.append_leaves(std::span(&block_header_hash, 1)); +} + +void ReferenceWorldState::create_checkpoint() +{ + checkpoints_.push(current_); +} + +void ReferenceWorldState::commit_checkpoint() +{ + BB_ASSERT(has_checkpoint()); + checkpoints_.pop(); +} + +void ReferenceWorldState::revert_checkpoint() +{ + BB_ASSERT(has_checkpoint()); + current_ = checkpoints_.top(); + checkpoints_.pop(); +} + +void ReferenceWorldState::commit_all_checkpoints() +{ + checkpoints_ = {}; +} + +void ReferenceWorldState::revert_all_checkpoints() +{ + while (has_checkpoint()) { + revert_checkpoint(); + } +} + +FF ReferenceWorldState::compute_genesis_block_header_hash(const TreeRoots& trees, uint64_t genesis_timestamp) +{ + // BlockHeader::serialize (block_header.nr) with every field zero except the state: the last + // archive is empty, and the sponge blob hash, tx effects root, global variables (other than the + // timestamp), total fees and mana used are all zero at genesis. + return crypto::merkle_tree::Poseidon2HashPolicy::hash({ + FF(DOM_SEP__BLOCK_HEADER_HASH), + FF(0), // last archive root + FF(0), // last archive next_available_leaf_index + trees.l1_to_l2_message_tree.root, + FF(trees.l1_to_l2_message_tree.next_available_leaf_index), + trees.note_hash_tree.root, + FF(trees.note_hash_tree.next_available_leaf_index), + trees.nullifier_tree.root, + FF(trees.nullifier_tree.next_available_leaf_index), + trees.public_data_tree.root, + FF(trees.public_data_tree.next_available_leaf_index), + FF(0), // sponge_blob_hash + FF(0), // tx_effects_tree_root + FF(0), // chain_id + FF(0), // version + FF(0), // block_number + FF(0), // slot_number + FF(genesis_timestamp), + FF(0), // coinbase + FF(0), // fee_recipient + FF(0), // gas_fees.fee_per_da_gas + FF(0), // gas_fees.fee_per_l2_gas + FF(0), // total_fees + FF(0), // total_mana_used + }); +} + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp new file mode 100644 index 00000000000..957cc7a921d --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "barretenberg/world_state_reference/memory_merkle_db.hpp" +#include "barretenberg/world_state_reference/merkle_tree_id.hpp" + +namespace bb::world_state { + +/** + * @brief The genesis a world state starts from: indexed-tree prefill and the genesis block's timestamp. + * @details canonical() is the protocol's: the protocol contracts' registration nullifiers, no public + * data, timestamp 0. Anything else is for tests of the reference itself. + */ +struct GenesisConfig { + std::vector prefilled_nullifiers; + std::vector prefilled_public_data; + uint64_t genesis_timestamp = 0; + + static GenesisConfig canonical(); +}; + +/** @brief One tree's root and size, as a state reference reports it. */ +struct TreeState { + MerkleTreeId tree_id; + FF root; + uint64_t size; + bool operator==(const TreeState& other) const = default; +}; + +/** + * @brief The in-memory reference world state behind the wsdb base contract (wsdb_base_schema.jsonc). + * + * All five trees at protocol height: the four MemoryMerkleDB trees plus the archive. It is one fork + * (fork 0) with no block history, which is all the protocol's own flows need: build a genesis, read + * witnesses, append and insert leaves, and advance the archive. + * + * The genesis snapshot never changes (the base contract has no Commit), so reads resolve against one + * of two states: the genesis, for block 0 or for committed reads of the latest block, and the current + * state, for uncommitted reads of the latest block. That is what a node world state answers on fork 0 + * before its first commit. Checkpoints copy the current state onto a stack. + * + * Genesis matches the node world state: the indexed trees hold their prefill, and the archive holds + * one leaf, the hash of the genesis block header built from the four trees' genesis roots. + * + * Methods take already validated input; the check_* helpers return why a request cannot be served, + * so a caller can answer with an error instead of aborting (wasm builds have no exceptions). + */ +class ReferenceWorldState { + public: + using ArchiveTree = MemoryAppendOnlyTree; + + /** Leaves each indexed tree starts with, prefill included, as on a node. */ + static constexpr size_t INDEXED_TREE_INITIAL_SIZE = 128; + + explicit ReferenceWorldState(const GenesisConfig& genesis = GenesisConfig::canonical()); + + /** Why `genesis` cannot seed a world state, or nullopt when it can. */ + static std::optional check_genesis(const GenesisConfig& genesis); + + /** + * @brief Why a read at this revision cannot be served, or nullopt when it can. + * @details Fork 0 only; the block must be 0 (the genesis) or WorldStateRevision::LATEST. + */ + static std::optional check_revision(const WorldStateRevision& revision); + + /** Why a write to this fork cannot be served, or nullopt when it can. */ + static std::optional check_fork(uint64_t fork_id); + + /** The tree height the protocol fixes for `tree_id`. */ + static uint32_t tree_height(MerkleTreeId tree_id); + + // Reads. `revision` must have passed check_revision. + std::vector get_state_reference(const WorldStateRevision& revision) const; + std::vector get_initial_state_reference() const; + TreeState get_tree_info(const WorldStateRevision& revision, MerkleTreeId tree_id) const; + /** Leaf of an append-only tree (note hash, L1-to-L2 message, archive); nullopt past its size. */ + std::optional get_leaf_value(const WorldStateRevision& revision, MerkleTreeId tree_id, uint64_t index) const; + std::optional> get_nullifier_preimage(const WorldStateRevision& revision, + uint64_t index) const; + std::optional> get_public_data_preimage(const WorldStateRevision& revision, + uint64_t index) const; + SiblingPath get_sibling_path(const WorldStateRevision& revision, MerkleTreeId tree_id, uint64_t index) const; + /** First index at or after `start` holding `leaf` in an append-only tree. */ + std::optional find_leaf_index(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& leaf, + uint64_t start) const; + /** First index at or after `start` of the indexed-tree leaf with `key` (a nullifier, or a slot). */ + std::optional find_indexed_leaf_index(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key, + uint64_t start) const; + GetLowIndexedLeafResponse find_low_leaf(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key) const; + + // Writes, all to the current state. + /** Append to an append-only tree (note hash, L1-to-L2 message, archive). */ + void append_leaves(MerkleTreeId tree_id, std::span leaves); + SequentialInsertionResult insert_nullifier(const NullifierLeafValue& leaf); + SequentialInsertionResult insert_public_data(const PublicDataLeafValue& leaf); + /** Whether `nullifier` is already in the current nullifier tree (inserting it again is an error). */ + bool has_nullifier(const FF& nullifier) const; + /** Whether `state` equals the current state of the four non-archive trees, as UpdateArchive requires. */ + bool state_matches(std::span state) const; + void update_archive(const FF& block_header_hash); + + void create_checkpoint(); + void commit_checkpoint(); + void revert_checkpoint(); + void commit_all_checkpoints(); + void revert_all_checkpoints(); + bool has_checkpoint() const { return !checkpoints_.empty(); } + + /** The genesis block header hash: the archive's first leaf. */ + const FF& genesis_block_header_hash() const { return genesis_block_header_hash_; } + + /** + * @brief Hash of the genesis block header over `trees` (nullifier, note hash, public data, L1-to-L2). + * @details Must match BlockHeader::hash in noir-protocol-circuits' block_header.nr for a header whose + * only non-zero fields are the four trees' state and the timestamp. + */ + static FF compute_genesis_block_header_hash(const TreeRoots& trees, uint64_t genesis_timestamp); + + private: + struct State { + MemoryMerkleDB trees; + ArchiveTree archive; + }; + + const State& view(const WorldStateRevision& revision) const; + + State genesis_; + State current_; + std::stack checkpoints_; + FF genesis_block_header_hash_; +}; + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp new file mode 100644 index 00000000000..7d49999bd66 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp @@ -0,0 +1,218 @@ +#include "barretenberg/world_state_reference/reference_world_state.hpp" + +#include "barretenberg/aztec/aztec_constants.hpp" + +#include + +#include + +using namespace bb; +using namespace bb::world_state; + +namespace { + +const WorldStateRevision LATEST_UNCOMMITTED{ .includeUncommitted = true }; +const WorldStateRevision LATEST_COMMITTED{ .includeUncommitted = false }; +const WorldStateRevision GENESIS_BLOCK{ .blockNumber = 0 }; + +// Recompute a tree root from a leaf hash and its sibling path. +template FF root_from_path(FF node, uint64_t index, const SiblingPath& path) +{ + for (const auto& sibling : path) { + node = (index & 1) == 0 ? HashPolicy::hash_pair(node, sibling) : HashPolicy::hash_pair(sibling, node); + index >>= 1; + } + return node; +} + +} // namespace + +TEST(ReferenceWorldState, CanonicalGenesisMatchesProtocolConstants) +{ + ReferenceWorldState ws; + + auto nullifiers = ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE); + EXPECT_EQ(nullifiers.root, FF(GENESIS_NULLIFIER_TREE_ROOT)); + EXPECT_EQ(nullifiers.size, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); + + EXPECT_EQ(ws.genesis_block_header_hash(), FF(GENESIS_BLOCK_HEADER_HASH)); + auto archive = ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::ARCHIVE); + EXPECT_EQ(archive.root, FF(GENESIS_ARCHIVE_ROOT)); + EXPECT_EQ(archive.size, 1); + EXPECT_EQ(ws.get_leaf_value(LATEST_COMMITTED, MerkleTreeId::ARCHIVE, 0), FF(GENESIS_BLOCK_HEADER_HASH)); +} + +TEST(ReferenceWorldState, EmptyGenesisMatchesEmptyWorldStateRoots) +{ + // The empty-tree roots hash_of_genesis_block_header in block_header.nr uses for the unseeded trees. + ReferenceWorldState ws(GenesisConfig{}); + auto state = ws.get_initial_state_reference(); + ASSERT_EQ(state.size(), 4); + for (const auto& tree : state) { + switch (tree.tree_id) { + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + EXPECT_EQ(tree.root, FF("0x0fef6d80d31109ddb56d6b3f607cbc9c0af0bff3ea0d43e8f278983c64c11f7a")); + EXPECT_EQ(tree.size, 0); + break; + case MerkleTreeId::NOTE_HASH_TREE: + EXPECT_EQ(tree.root, FF("0x2590f2aab19dd791700b4a43d3f52bb88ef2409a3731da8e848663559202e4c6")); + EXPECT_EQ(tree.size, 0); + break; + case MerkleTreeId::PUBLIC_DATA_TREE: + EXPECT_EQ(tree.root, FF("0x1bef38b621017d3c7416663d0cd81369424560710526a3fbaaec13e356b9d084")); + EXPECT_EQ(tree.size, 128); + break; + case MerkleTreeId::NULLIFIER_TREE: + EXPECT_EQ(tree.size, 128); + EXPECT_NE(tree.root, FF(GENESIS_NULLIFIER_TREE_ROOT)); + break; + default: + ADD_FAILURE() << "unexpected tree in state reference"; + } + } +} + +TEST(ReferenceWorldState, GenesisPrefillValidation) +{ + auto with_nullifiers = [](std::vector keys) { + GenesisConfig genesis; + for (auto key : keys) { + genesis.prefilled_nullifiers.emplace_back(key); + } + return ReferenceWorldState::check_genesis(genesis); + }; + EXPECT_FALSE(with_nullifiers({ FF(1000), FF(2000) }).has_value()); + EXPECT_EQ(with_nullifiers({ FF(2000), FF(1000) }), + "prefilled nullifiers: Prefilled values must be unique and sorted"); + EXPECT_EQ(with_nullifiers({ FF(1000), FF(1000) }), + "prefilled nullifiers: Prefilled values must be unique and sorted"); + // Padding occupies keys 0..127 when one value is prefilled, so a key of 5 collides with it. + EXPECT_EQ(with_nullifiers({ FF(5) }), + "prefilled nullifiers: Prefilled values must not be the same as the default values"); + std::vector too_many; + for (size_t i = 0; i < ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + 1; ++i) { + too_many.emplace_back(1000 + i); + } + EXPECT_EQ(with_nullifiers(too_many), + "prefilled nullifiers: Number of prefilled values can't be more than initial size"); + + GenesisConfig public_data; + public_data.prefilled_public_data = { PublicDataLeafValue(FF(2000), FF(1)), PublicDataLeafValue(FF(1000), FF(2)) }; + EXPECT_EQ(ReferenceWorldState::check_genesis(public_data), + "prefilled public data: Prefilled values must be unique and sorted"); +} + +TEST(ReferenceWorldState, RevisionAndForkChecks) +{ + EXPECT_FALSE(ReferenceWorldState::check_revision(LATEST_UNCOMMITTED).has_value()); + EXPECT_FALSE(ReferenceWorldState::check_revision(GENESIS_BLOCK).has_value()); + EXPECT_TRUE(ReferenceWorldState::check_revision(WorldStateRevision{ .blockNumber = 3 }).has_value()); + EXPECT_TRUE(ReferenceWorldState::check_revision(WorldStateRevision{ .forkId = 1 }).has_value()); + EXPECT_FALSE(ReferenceWorldState::check_fork(0).has_value()); + EXPECT_EQ(ReferenceWorldState::check_fork(2), "the reference world state has one fork (0), not 2"); +} + +TEST(ReferenceWorldState, CommittedReadsSeeGenesisUncommittedSeeCurrent) +{ + ReferenceWorldState ws; + const std::vector note_hashes{ FF(11), FF(22), FF(33) }; + ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); + + EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 3); + EXPECT_EQ(ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 0); + EXPECT_EQ(ws.get_tree_info(GENESIS_BLOCK, MerkleTreeId::NOTE_HASH_TREE).size, 0); + + EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 1), FF(22)); + EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 3), std::nullopt); + EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(33), 0), 2); + EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(11), 1), std::nullopt); + EXPECT_EQ(ws.find_leaf_index(LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(33), 0), std::nullopt); + + auto path = ws.get_sibling_path(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 1); + EXPECT_EQ(path.size(), NOTE_HASH_TREE_HEIGHT); + EXPECT_EQ(root_from_path(FF(22), 1, path), + ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).root); +} + +TEST(ReferenceWorldState, NullifierInsertionWitnessesAreConsistent) +{ + ReferenceWorldState ws; + const FF nullifier(0x1234); + ASSERT_FALSE(ws.has_nullifier(nullifier)); + auto low = ws.find_low_leaf(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, nullifier); + EXPECT_FALSE(low.is_already_present); + + auto result = ws.insert_nullifier(NullifierLeafValue(nullifier)); + ASSERT_EQ(result.low_leaf_witness_data.size(), 1); + ASSERT_EQ(result.insertion_witness_data.size(), 1); + EXPECT_EQ(result.low_leaf_witness_data[0].index, low.index); + EXPECT_EQ(result.insertion_witness_data[0].index, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); + + EXPECT_TRUE(ws.has_nullifier(nullifier)); + auto index = ws.find_indexed_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, nullifier, 0); + ASSERT_EQ(index, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); + auto preimage = ws.get_nullifier_preimage(LATEST_UNCOMMITTED, *index); + ASSERT_TRUE(preimage.has_value()); + EXPECT_EQ(preimage->leaf.nullifier, nullifier); + // The low leaf now points at the new leaf. + EXPECT_EQ(ws.get_nullifier_preimage(LATEST_UNCOMMITTED, low.index)->nextKey, nullifier); + + // A membership witness read back from the tree reproduces its root. + auto path = ws.get_sibling_path(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, *index); + FF leaf_hash = aztec::NullifierMerkleHashPolicy::hash(preimage->get_hash_inputs()); + EXPECT_EQ(root_from_path(leaf_hash, *index, path), + ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE).root); + + // The genesis view is untouched. + EXPECT_EQ(ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE).root, FF(GENESIS_NULLIFIER_TREE_ROOT)); +} + +TEST(ReferenceWorldState, PublicDataUpdatesInPlace) +{ + ReferenceWorldState ws; + ws.insert_public_data(PublicDataLeafValue(FF(5000), FF(1))); + auto size_after_insert = ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE).size; + ws.insert_public_data(PublicDataLeafValue(FF(5000), FF(2))); + EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE).size, size_after_insert); + auto index = ws.find_indexed_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE, FF(5000), 0); + ASSERT_TRUE(index.has_value()); + EXPECT_EQ(ws.get_public_data_preimage(LATEST_UNCOMMITTED, *index)->leaf.value, FF(2)); +} + +TEST(ReferenceWorldState, CheckpointsRevertAndCommit) +{ + ReferenceWorldState ws; + ws.create_checkpoint(); + ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(1) }); + ws.create_checkpoint(); + ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(2) }); + ws.revert_checkpoint(); + EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 1); + ws.commit_checkpoint(); + EXPECT_FALSE(ws.has_checkpoint()); + EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 1); + + ws.create_checkpoint(); + ws.create_checkpoint(); + ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(3) }); + ws.revert_all_checkpoints(); + EXPECT_FALSE(ws.has_checkpoint()); + EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 1); +} + +TEST(ReferenceWorldState, UpdateArchiveRequiresTheCurrentState) +{ + ReferenceWorldState ws; + ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(7) }); + auto current = ws.get_state_reference(LATEST_UNCOMMITTED); + EXPECT_TRUE(ws.state_matches(current)); + EXPECT_FALSE(ws.state_matches(ws.get_initial_state_reference())); + // Order is not part of the contract. + std::reverse(current.begin(), current.end()); + EXPECT_TRUE(ws.state_matches(current)); + + ws.update_archive(FF(99)); + EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE).size, 2); + EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE, 1), FF(99)); + EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE, FF(99), 0), 1); +} diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc b/barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc new file mode 100644 index 00000000000..aed3f5c62cc --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc @@ -0,0 +1,270 @@ +// The world-state commands the protocol itself needs: reading tree state, membership and low-leaf +// witnesses, appending and inserting leaves, and advancing the archive. Code written against this +// contract runs against any world state that implements it: the in-memory reference +// (@aztec-foundation/wsdb-ref, generated from this file) and a node world state, whose schema +// extends this one with forks, block history and storage. +// +// Every definition here is the one the node world state already uses, so an extension can add to +// it without redefining anything. Revisions and fork ids keep their full shape; an implementation +// may answer anything but fork 0 at block 0 or the latest block (0xffffffff) with an error. +// +// A world state starts at the canonical genesis: empty trees, except the indexed trees' prefill, +// whose last nullifiers are the protocol contracts' registration nullifiers, and an archive holding +// the genesis block header hash (GENESIS_* in constants.nr). How it gets there is not part of this +// contract. +// +// Tree ids: 0 nullifier, 1 note hash, 2 public data, 3 L1-to-L2 message, 4 archive. +{ + "service": "Wsdb", + "aliases": { + "MerkleTreeId": "u32", + "ForkId": "u64", + "BlockNumber": "u32", + "Fr": "bin32", + "LeafIndex": "u64", + "PublicDataSlot": "bin32", + "PublicDataValue": "bin32", + "Nullifier": "bin32", + "BlockHeaderHash": "bin32" + }, + "types": { + "WorldStateRevision": { + "forkId": "ForkId", + "blockNumber": "BlockNumber", + "includeUncommitted": "bool" + }, + "TreeStateReference": { + "treeId": "MerkleTreeId", + "root": "Fr", + "size": "LeafIndex" + }, + "PublicDataLeafValue": { + "slot": "PublicDataSlot", + "value": "PublicDataValue" + }, + "NullifierLeafValue": { + "nullifier": "Nullifier" + }, + "IndexedPublicDataLeafValue": { + "leaf": "PublicDataLeafValue", + "nextIndex": "LeafIndex", + "nextKey": "Fr" + }, + "IndexedNullifierLeafValue": { + "leaf": "NullifierLeafValue", + "nextIndex": "LeafIndex", + "nextKey": "Fr" + }, + "PublicDataLeafUpdateWitnessData": { + "leaf": "IndexedPublicDataLeafValue", + "index": "LeafIndex", + "path": "Fr[]" + }, + "NullifierLeafUpdateWitnessData": { + "leaf": "IndexedNullifierLeafValue", + "index": "LeafIndex", + "path": "Fr[]" + }, + "SequentialInsertionResultPublicData": { + "lowLeafWitnessData": "PublicDataLeafUpdateWitnessData[]", + "insertionWitnessData": "PublicDataLeafUpdateWitnessData[]" + }, + "SequentialInsertionResultNullifier": { + "lowLeafWitnessData": "NullifierLeafUpdateWitnessData[]", + "insertionWitnessData": "NullifierLeafUpdateWitnessData[]" + } + }, + "error": { + "message": "string" + }, + "commands": { + "GetTreeInfo": { + "request": { + "treeId": "MerkleTreeId", + "revision": "WorldStateRevision" + }, + "response": { + "treeId": "MerkleTreeId", + "root": "Fr", + "size": "LeafIndex", + "depth": "u32" + } + }, + "GetStateReference": { + "request": { + "revision": "WorldStateRevision" + }, + "response": { + "state": "TreeStateReference[]" + } + }, + "GetInitialStateReference": { + "request": {}, + "response": { + "state": "TreeStateReference[]" + } + }, + "GetLeafValue": { + "request": { + "treeId": "MerkleTreeId", + "revision": "WorldStateRevision", + "leafIndex": "LeafIndex" + }, + "response": { + "value": "Fr?" + } + }, + "GetPublicDataLeafValue": { + "request": { + "revision": "WorldStateRevision", + "leafIndex": "LeafIndex" + }, + "response": { + "value": "PublicDataLeafValue?" + } + }, + "GetNullifierLeafValue": { + "request": { + "revision": "WorldStateRevision", + "leafIndex": "LeafIndex" + }, + "response": { + "value": "NullifierLeafValue?" + } + }, + "GetPublicDataLeafPreimage": { + "request": { + "revision": "WorldStateRevision", + "leafIndex": "LeafIndex" + }, + "response": { + "preimage": "IndexedPublicDataLeafValue?" + } + }, + "GetNullifierLeafPreimage": { + "request": { + "revision": "WorldStateRevision", + "leafIndex": "LeafIndex" + }, + "response": { + "preimage": "IndexedNullifierLeafValue?" + } + }, + "GetSiblingPath": { + "request": { + "treeId": "MerkleTreeId", + "revision": "WorldStateRevision", + "leafIndex": "LeafIndex" + }, + "response": { + "path": "Fr[]" + } + }, + "FindLeafIndices": { + "request": { + "treeId": "MerkleTreeId", + "revision": "WorldStateRevision", + "leaves": "Fr[]", + "startIndex": "LeafIndex" + }, + "response": { + "indices": "LeafIndex?[]" + } + }, + "FindPublicDataLeafIndices": { + "request": { + "revision": "WorldStateRevision", + "leaves": "PublicDataLeafValue[]", + "startIndex": "LeafIndex" + }, + "response": { + "indices": "LeafIndex?[]" + } + }, + "FindNullifierLeafIndices": { + "request": { + "revision": "WorldStateRevision", + "leaves": "NullifierLeafValue[]", + "startIndex": "LeafIndex" + }, + "response": { + "indices": "LeafIndex?[]" + } + }, + "FindLowLeaf": { + "request": { + "treeId": "MerkleTreeId", + "revision": "WorldStateRevision", + "key": "Fr" + }, + "response": { + "alreadyPresent": "bool", + "index": "LeafIndex" + } + }, + "AppendLeaves": { + "request": { + "treeId": "MerkleTreeId", + "leaves": "Fr[]", + "forkId": "ForkId" + }, + "response": {} + }, + "SequentialInsertPublicData": { + "request": { + "leaves": "PublicDataLeafValue[]", + "forkId": "ForkId" + }, + "response": { + "result": "SequentialInsertionResultPublicData" + } + }, + "SequentialInsertNullifier": { + "request": { + "leaves": "NullifierLeafValue[]", + "forkId": "ForkId" + }, + "response": { + "result": "SequentialInsertionResultNullifier" + } + }, + "UpdateArchive": { + "request": { + "blockStateRef": "TreeStateReference[]", + "blockHeaderHash": "BlockHeaderHash", + "forkId": "ForkId" + }, + "response": {} + }, + "CreateCheckpoint": { + "request": { + "forkId": "ForkId" + }, + "response": {} + }, + "CommitCheckpoint": { + "request": { + "forkId": "ForkId" + }, + "response": {} + }, + "RevertCheckpoint": { + "request": { + "forkId": "ForkId" + }, + "response": {} + }, + "CommitAllCheckpoints": { + "request": { + "forkId": "ForkId" + }, + "response": {} + }, + "RevertAllCheckpoints": { + "request": { + "forkId": "ForkId" + }, + "response": {} + } + } +} diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt new file mode 100644 index 00000000000..844160891aa --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt @@ -0,0 +1,104 @@ +# wsdb-ref: the in-memory reference world state (world_state_reference) as a service over the wsdb +# base contract, ../world_state_reference/wsdb_base_schema.jsonc. The generated dispatch runs the +# handlers here; the generated FFI entry (wsdb_ipc_ffi_entry) runs the dispatch in-process. Two +# artifacts carry that entry to @aztec-foundation/wsdb-ref (barretenberg/ts/wsdb-ref): +# - wsdb-ref.wasm, a WASI reactor (wasm builds), for the package's wasm transport; +# - wsdb_ref_napi.node, a Node-API addon over the entry (native builds), for its napi transport. +set(WSDB_REF_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/../world_state_reference/wsdb_base_schema.jsonc) +set(WSDB_REF_GEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/generated) +set(WSDB_REF_GEN_OUTPUTS + ${WSDB_REF_GEN_DIR}/wsdb_dispatch.hpp + ${WSDB_REF_GEN_DIR}/wsdb_ffi.cpp + ${WSDB_REF_GEN_DIR}/wsdb_ffi.hpp + ${WSDB_REF_GEN_DIR}/wsdb_ipc_server.hpp + ${WSDB_REF_GEN_DIR}/wsdb_napi.cpp + ${WSDB_REF_GEN_DIR}/wsdb_types.hpp + ${WSDB_REF_GEN_DIR}/ipc_codegen/msgpack_adaptor.hpp + ${WSDB_REF_GEN_DIR}/ipc_codegen/msgpack_include.hpp + ${WSDB_REF_GEN_DIR}/ipc_codegen/throw.hpp +) +set(IPC_CODEGEN_DIR ${CMAKE_SOURCE_DIR}/../../ipc-codegen) +file(GLOB_RECURSE IPC_CODEGEN_SRC ${IPC_CODEGEN_DIR}/src/*.ts ${IPC_CODEGEN_DIR}/templates/cpp/*.hpp) +add_custom_command( + OUTPUT ${WSDB_REF_GEN_OUTPUTS} + COMMAND node --experimental-strip-types --no-warnings + ${IPC_CODEGEN_DIR}/src/generate.ts + --schema ${WSDB_REF_SCHEMA} + --lang cpp + --out ${WSDB_REF_GEN_DIR} + --server + --ffi + --napi + --cpp-namespace bb::wsdb_ref + --cpp-include-dir barretenberg/wsdb_ref/generated + DEPENDS ${WSDB_REF_SCHEMA} ${IPC_CODEGEN_SRC} + COMMENT "Generating the wsdb-ref dispatch, FFI entry and Node-API addon from wsdb_base_schema.jsonc" + VERBATIM +) +add_custom_target(wsdb_ref_generated DEPENDS ${WSDB_REF_GEN_OUTPUTS}) + +# Explicit sources: the generated Node-API addon must stay out of the module (it needs napi.h), and +# the generated FFI entry does not exist yet when a clean tree is configured. +file(GLOB WSDB_REF_TESTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.test.cpp) +barretenberg_module_with_sources( + wsdb_ref + SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_ref_handlers.cpp ${WSDB_REF_GEN_DIR}/wsdb_ffi.cpp + HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_ref_handlers.hpp + TEST_SOURCE_FILES ${WSDB_REF_TESTS} + DEPENDENCIES world_state_reference +) +add_dependencies(wsdb_ref_objects wsdb_ref_generated) +if(TARGET wsdb_ref_test_objects) + add_dependencies(wsdb_ref_test_objects wsdb_ref_generated) +endif() +set_target_properties(wsdb_ref_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) + +if(WASM) + # A standard WASI reactor exporting the FFI entry, like barretenberg.wasm: wasi-sdk's reactor + # startup provides _initialize, and the module imports nothing but WASI. + add_executable( + wsdb-ref-debug.wasm + $ + ) + target_link_libraries(wsdb-ref-debug.wasm PRIVATE world_state_reference) + target_link_options(wsdb-ref-debug.wasm PRIVATE -mexec-model=reactor -Wl,--export-dynamic) + add_custom_command( + OUTPUT ${CMAKE_BINARY_DIR}/bin/wsdb-ref.wasm + COMMAND ${CMAKE_STRIP} ${CMAKE_BINARY_DIR}/bin/wsdb-ref-debug.wasm -o ${CMAKE_BINARY_DIR}/bin/wsdb-ref.wasm + DEPENDS wsdb-ref-debug.wasm + COMMENT "Stripping debug info to create wsdb-ref.wasm" + ) + add_custom_target(wsdb-ref.wasm ALL DEPENDS ${CMAKE_BINARY_DIR}/bin/wsdb-ref.wasm) +else() + # The Node-API addon over the FFI entry. Its headers come from the node-addon-api and + # node-api-headers packages ipc-runtime already pins for its own addon. + set(NAPI_PACKAGE_DIR ${CMAKE_SOURCE_DIR}/../../ipc-runtime/cpp/napi) + execute_process(COMMAND yarn --immutable WORKING_DIRECTORY ${NAPI_PACKAGE_DIR} OUTPUT_QUIET) + execute_process( + COMMAND node -p "require('node-addon-api').include" + WORKING_DIRECTORY ${NAPI_PACKAGE_DIR} + OUTPUT_VARIABLE NODE_ADDON_API_DIR + ) + execute_process( + COMMAND node -p "require('node-api-headers').include_dir" + WORKING_DIRECTORY ${NAPI_PACKAGE_DIR} + OUTPUT_VARIABLE NODE_API_HEADERS_DIR + ) + string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}") + string(REGEX REPLACE "[\r\n\"]" "" NODE_API_HEADERS_DIR "${NODE_API_HEADERS_DIR}") + + add_library(wsdb_ref_napi SHARED ${WSDB_REF_GEN_DIR}/wsdb_napi.cpp) + add_dependencies(wsdb_ref_napi wsdb_ref_generated) + target_compile_definitions(wsdb_ref_napi PRIVATE NAPI_VERSION=9) + target_include_directories(wsdb_ref_napi SYSTEM PRIVATE ${NODE_API_HEADERS_DIR} ${NODE_ADDON_API_DIR}) + target_link_libraries(wsdb_ref_napi PRIVATE wsdb_ref world_state_reference) + set_target_properties(wsdb_ref_napi PROPERTIES + PREFIX "" + SUFFIX ".node" + OUTPUT_NAME wsdb_ref_napi + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + # Node provides the N-API symbols at load time. + if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + target_link_options(wsdb_ref_napi PRIVATE -undefined dynamic_lookup) + endif() +endif() diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp new file mode 100644 index 00000000000..76e973dffa9 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp @@ -0,0 +1,256 @@ +#include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" + +#include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" + +#include + +#include +#include +#include + +using namespace bb; +using namespace bb::wsdb_ref; + +namespace { + +constexpr uint32_t LATEST = std::numeric_limits::max(); +const wire::WorldStateRevision LATEST_UNCOMMITTED{ .forkId = 0, .blockNumber = LATEST, .includeUncommitted = true }; +const wire::WorldStateRevision LATEST_COMMITTED{ .forkId = 0, .blockNumber = LATEST, .includeUncommitted = false }; + +std::vector encode_request(const char* name, const auto& cmd) +{ + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string(name)); + pk.pack(cmd); + return { buf.data(), buf.data() + buf.size() }; +} + +/** A decoded response: the payload, or the service's error message. */ +template using Result = std::variant; + +template Result decode_response(const std::vector& frame, const char* expected) +{ + auto unpacked = msgpack::unpack(reinterpret_cast(frame.data()), frame.size()); + auto obj = unpacked.get(); + std::string tag(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size); + if (tag == "WsdbErrorResponse") { + wire::WsdbErrorResponse error; + obj.via.array.ptr[1].convert(error); + return error.message; + } + EXPECT_EQ(tag, expected); + Resp resp; + obj.via.array.ptr[1].convert(resp); + return resp; +} + +/** Call a service through the generated in-process FFI entry: the path the napi and wasm transports take. */ +template Result call_ffi(const char* name, const auto& cmd, const char* expected) +{ + auto request = encode_request(name, cmd); + uint8_t* output = nullptr; + size_t output_len = 0; + wsdb_ipc_ffi_entry(request.data(), request.size(), &output, &output_len); + std::vector frame(output, output + output_len); + wsdb_ipc_ffi_free(output); + return decode_response(frame, expected); +} + +/** Call a fresh service instance through the generated dispatch, independent of the FFI's shared state. */ +struct Service { + WsdbRefContext ctx; + AsyncDispatchHandler handler = make_wsdb_handler(ctx); + + template Result call(const char* name, const auto& cmd, const char* expected) + { + auto request = encode_request(name, cmd); + std::vector frame; + handler(request, [&frame](std::vector response) { frame = std::move(response); }); + return decode_response(frame, expected); + } +}; + +template const Resp& ok(const Result& result) +{ + if (const auto* error = std::get_if(&result)) { + ADD_FAILURE() << "service error: " << *error; + static Resp empty{}; + return empty; + } + return std::get(result); +} + +template std::string error_of(const Result& result) +{ + const auto* error = std::get_if(&result); + return error ? *error : std::string(""); +} + +template W bytes_of(const fr& value) +{ + W w{}; + fr::serialize_to_buffer(value, w.data()); + return w; +} + +fr fr_of(const auto& w) +{ + return fr::serialize_from_buffer(w.data()); +} + +} // namespace + +TEST(WsdbRef, FfiEntryServesTheCanonicalGenesis) +{ + auto archive = + ok(call_ffi("WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 4, .revision = LATEST_COMMITTED }, + "WsdbGetTreeInfoResponse")); + EXPECT_EQ(fr_of(archive.root), fr(GENESIS_ARCHIVE_ROOT)); + EXPECT_EQ(archive.size, 1); + EXPECT_EQ(archive.depth, ARCHIVE_HEIGHT); + + auto header = ok(call_ffi( + "WsdbGetLeafValue", + wire::WsdbGetLeafValue{ .treeId = 4, .revision = LATEST_COMMITTED, .leafIndex = 0 }, + "WsdbGetLeafValueResponse")); + ASSERT_TRUE(header.value.has_value()); + EXPECT_EQ(fr_of(*header.value), fr(GENESIS_BLOCK_HEADER_HASH)); + + auto initial = ok(call_ffi( + "WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}, "WsdbGetInitialStateReferenceResponse")); + ASSERT_EQ(initial.state.size(), 4); + bool saw_nullifier_tree = false; + for (const auto& tree : initial.state) { + if (tree.treeId == 0) { + saw_nullifier_tree = true; + EXPECT_EQ(fr_of(tree.root), fr(GENESIS_NULLIFIER_TREE_ROOT)); + EXPECT_EQ(tree.size, 128); + } + } + EXPECT_TRUE(saw_nullifier_tree); +} + +TEST(WsdbRef, FfiEntryAnswersBadRequestsWithErrors) +{ + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(std::string("WsdbSyncBlock")); + pk.pack_map(0); + uint8_t* output = nullptr; + size_t output_len = 0; + wsdb_ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &output, &output_len); + std::vector frame(output, output + output_len); + wsdb_ipc_ffi_free(output); + EXPECT_EQ(error_of(decode_response(frame, "")), "unknown command: WsdbSyncBlock"); + + EXPECT_EQ(error_of(call_ffi( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 9, .revision = LATEST_COMMITTED }, + "WsdbGetTreeInfoResponse")), + "unknown tree id 9"); +} + +TEST(WsdbRef, RejectsWhatTheReferenceDoesNotModel) +{ + Service service; + EXPECT_EQ(error_of(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 1, .revision = { .forkId = 3, .blockNumber = LATEST, .includeUncommitted = true } }, + "WsdbGetTreeInfoResponse")), + "the reference world state has one fork (0), not 3"); + EXPECT_NE(error_of(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 1, .revision = { .forkId = 0, .blockNumber = 5, .includeUncommitted = true } }, + "WsdbGetTreeInfoResponse")) + .find("keeps no block history"), + std::string::npos); + EXPECT_EQ(error_of(service.call( + "WsdbGetLeafValue", + wire::WsdbGetLeafValue{ .treeId = 0, .revision = LATEST_UNCOMMITTED, .leafIndex = 0 }, + "WsdbGetLeafValueResponse")), + "get_leaf_value is not supported for tree 0"); + EXPECT_EQ(error_of(service.call( + "WsdbRevertCheckpoint", wire::WsdbRevertCheckpoint{ .forkId = 0 }, "WsdbRevertCheckpointResponse")), + "no checkpoint to revert"); +} + +TEST(WsdbRef, InsertsNullifiersWithWitnessesAndRejectsDuplicates) +{ + Service service; + const fr nullifier(0xabcdef); + auto inserted = ok(service.call( + "WsdbSequentialInsertNullifier", + wire::WsdbSequentialInsertNullifier{ .leaves = { { .nullifier = bytes_of(nullifier) } }, + .forkId = 0 }, + "WsdbSequentialInsertNullifierResponse")); + ASSERT_EQ(inserted.result.insertionWitnessData.size(), 1); + EXPECT_EQ(inserted.result.insertionWitnessData[0].index, 128); + EXPECT_EQ(inserted.result.lowLeafWitnessData[0].path.size(), NULLIFIER_TREE_HEIGHT); + + auto found = ok(service.call( + "WsdbFindNullifierLeafIndices", + wire::WsdbFindNullifierLeafIndices{ .revision = LATEST_UNCOMMITTED, + .leaves = { { .nullifier = bytes_of(nullifier) } }, + .startIndex = 0 }, + "WsdbFindNullifierLeafIndicesResponse")); + ASSERT_EQ(found.indices.size(), 1); + EXPECT_EQ(found.indices[0], 128); + + // Rejected as a whole: the fresh nullifier in the same batch is not inserted either. + const fr fresh(0x777); + auto duplicate = service.call( + "WsdbSequentialInsertNullifier", + wire::WsdbSequentialInsertNullifier{ .leaves = { { .nullifier = bytes_of(fresh) }, + { .nullifier = bytes_of(nullifier) } }, + .forkId = 0 }, + "WsdbSequentialInsertNullifierResponse"); + EXPECT_NE(error_of(duplicate).find("already exists"), std::string::npos); + auto low = ok(service.call( + "WsdbFindLowLeaf", + wire::WsdbFindLowLeaf{ .treeId = 0, .revision = LATEST_UNCOMMITTED, .key = bytes_of(fresh) }, + "WsdbFindLowLeafResponse")); + EXPECT_FALSE(low.alreadyPresent); +} + +TEST(WsdbRef, UpdateArchiveChecksTheBlockState) +{ + Service service; + ok(service.call( + "WsdbAppendLeaves", + wire::WsdbAppendLeaves{ .treeId = 1, .leaves = { bytes_of(fr(42)) }, .forkId = 0 }, + "WsdbAppendLeavesResponse")); + auto state = ok( + service.call("WsdbGetStateReference", + wire::WsdbGetStateReference{ .revision = LATEST_UNCOMMITTED }, + "WsdbGetStateReferenceResponse")); + + auto stale = ok(service.call( + "WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}, "WsdbGetInitialStateReferenceResponse")); + EXPECT_EQ( + error_of(service.call( + "WsdbUpdateArchive", + wire::WsdbUpdateArchive{ + .blockStateRef = stale.state, .blockHeaderHash = bytes_of(fr(1)), .forkId = 0 }, + "WsdbUpdateArchiveResponse")), + "Can't update archive tree: Block state does not match world state"); + + ok(service.call( + "WsdbUpdateArchive", + wire::WsdbUpdateArchive{ + .blockStateRef = state.state, .blockHeaderHash = bytes_of(fr(1)), .forkId = 0 }, + "WsdbUpdateArchiveResponse")); + auto archive = ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 4, .revision = LATEST_UNCOMMITTED }, + "WsdbGetTreeInfoResponse")); + EXPECT_EQ(archive.size, 2); +} diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp new file mode 100644 index 00000000000..b0e80fb99a3 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp @@ -0,0 +1,540 @@ +#include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" +#include "barretenberg/common/log.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" + +#include +#include +#include + +namespace bb::wsdb_ref { + +using world_state::MerkleTreeId; +using world_state::ReferenceWorldState; +using world_state::TreeState; +using world_state::WorldStateRevision; +using FF = bb::fr; +using crypto::merkle_tree::IndexedLeaf; +using crypto::merkle_tree::LeafUpdateWitnessData; +using crypto::merkle_tree::NullifierLeafValue; +using crypto::merkle_tree::PublicDataLeafValue; + +namespace { + +// Field elements cross the wire as 32 canonical bytes under whichever nominal alias the field declares. +template FF to_fr(const W& w) +{ + return FF::serialize_from_buffer(w.data()); +} + +template W from_fr(const FF& f) +{ + W w{}; + FF::serialize_to_buffer(f, w.data()); + return w; +} + +std::vector path_to_wire(const std::vector& path) +{ + std::vector out; + out.reserve(path.size()); + for (const auto& node : path) { + out.push_back(from_fr(node)); + } + return out; +} + +std::optional tree_id_of(uint32_t id) +{ + if (id > static_cast(MerkleTreeId::ARCHIVE)) { + return std::nullopt; + } + return static_cast(id); +} + +bool is_append_only(MerkleTreeId id) +{ + return id == MerkleTreeId::NOTE_HASH_TREE || id == MerkleTreeId::L1_TO_L2_MESSAGE_TREE || + id == MerkleTreeId::ARCHIVE; +} + +bool is_indexed(MerkleTreeId id) +{ + return id == MerkleTreeId::NULLIFIER_TREE || id == MerkleTreeId::PUBLIC_DATA_TREE; +} + +WorldStateRevision revision_of(const wire::WorldStateRevision& w) +{ + return WorldStateRevision{ .forkId = w.forkId, + .blockNumber = w.blockNumber, + .includeUncommitted = w.includeUncommitted }; +} + +std::vector state_to_wire(const std::vector& state) +{ + std::vector out; + out.reserve(state.size()); + for (const auto& tree : state) { + out.push_back( + { .treeId = static_cast(tree.tree_id), .root = from_fr(tree.root), .size = tree.size }); + } + return out; +} + +wire::NullifierLeafValue to_wire(const NullifierLeafValue& leaf) +{ + return { .nullifier = from_fr(leaf.nullifier) }; +} + +wire::PublicDataLeafValue to_wire(const PublicDataLeafValue& leaf) +{ + return { .slot = from_fr(leaf.slot), .value = from_fr(leaf.value) }; +} + +wire::IndexedNullifierLeafValue to_wire(const IndexedLeaf& leaf) +{ + return { .leaf = to_wire(leaf.leaf), .nextIndex = leaf.nextIndex, .nextKey = from_fr(leaf.nextKey) }; +} + +wire::IndexedPublicDataLeafValue to_wire(const IndexedLeaf& leaf) +{ + return { .leaf = to_wire(leaf.leaf), .nextIndex = leaf.nextIndex, .nextKey = from_fr(leaf.nextKey) }; +} + +wire::NullifierLeafUpdateWitnessData to_wire(const LeafUpdateWitnessData& w) +{ + return { .leaf = to_wire(w.leaf), .index = w.index, .path = path_to_wire(w.path) }; +} + +wire::PublicDataLeafUpdateWitnessData to_wire(const LeafUpdateWitnessData& w) +{ + return { .leaf = to_wire(w.leaf), .index = w.index, .path = path_to_wire(w.path) }; +} + +template std::vector all_to_wire(const std::vector& in) +{ + std::vector out; + out.reserve(in.size()); + for (const auto& item : in) { + out.push_back(to_wire(item)); + } + return out; +} + +std::vector> to_wire_indices(const std::vector>& indices) +{ + return indices; +} + +/** + * Resolve the tree id and revision of a read, or answer the request with why it cannot be served. + * `allowed` says which trees the command reads. + */ +template +std::optional check_read(uint32_t tree_id, + const WorldStateRevision& revision, + Allowed allowed, + const char* what, + const Responder& respond) +{ + auto id = tree_id_of(tree_id); + if (!id.has_value()) { + respond.error("unknown tree id " + std::to_string(tree_id)); + return std::nullopt; + } + if (!allowed(*id)) { + respond.error(std::string(what) + " is not supported for tree " + std::to_string(tree_id)); + return std::nullopt; + } + if (auto problem = ReferenceWorldState::check_revision(revision)) { + respond.error(*problem); + return std::nullopt; + } + return id; +} + +template bool check_revision(const WorldStateRevision& revision, const Responder& respond) +{ + if (auto problem = ReferenceWorldState::check_revision(revision)) { + respond.error(*problem); + return false; + } + return true; +} + +template bool check_fork(uint64_t fork_id, const Responder& respond) +{ + if (auto problem = ReferenceWorldState::check_fork(fork_id)) { + respond.error(*problem); + return false; + } + return true; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Tree state +// --------------------------------------------------------------------------- + +void handle_get_tree_info(WsdbRefContext& ctx, + wire::WsdbGetTreeInfo&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + auto id = check_read(cmd.treeId, revision, [](MerkleTreeId) { return true; }, "get_tree_info", respond); + if (!id) { + return; + } + auto info = ctx.world_state.get_tree_info(revision, *id); + respond.ok({ .treeId = cmd.treeId, + .root = from_fr(info.root), + .size = info.size, + .depth = ReferenceWorldState::tree_height(*id) }); +} + +void handle_get_state_reference(WsdbRefContext& ctx, + wire::WsdbGetStateReference&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + if (!check_revision(revision, respond)) { + return; + } + respond.ok({ .state = state_to_wire(ctx.world_state.get_state_reference(revision)) }); +} + +void handle_get_initial_state_reference(WsdbRefContext& ctx, + wire::WsdbGetInitialStateReference&& /*cmd*/, + Responder respond) +{ + respond.ok({ .state = state_to_wire(ctx.world_state.get_initial_state_reference()) }); +} + +// --------------------------------------------------------------------------- +// Leaf reads +// --------------------------------------------------------------------------- + +void handle_get_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetLeafValue&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + auto id = check_read(cmd.treeId, revision, is_append_only, "get_leaf_value", respond); + if (!id) { + return; + } + auto leaf = ctx.world_state.get_leaf_value(revision, *id, cmd.leafIndex); + respond.ok({ .value = leaf ? std::optional(from_fr(*leaf)) : std::nullopt }); +} + +void handle_get_public_data_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetPublicDataLeafValue&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + if (!check_revision(revision, respond)) { + return; + } + auto preimage = ctx.world_state.get_public_data_preimage(revision, cmd.leafIndex); + respond.ok({ .value = preimage ? std::optional(to_wire(preimage->leaf)) : std::nullopt }); +} + +void handle_get_nullifier_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetNullifierLeafValue&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + if (!check_revision(revision, respond)) { + return; + } + auto preimage = ctx.world_state.get_nullifier_preimage(revision, cmd.leafIndex); + respond.ok({ .value = preimage ? std::optional(to_wire(preimage->leaf)) : std::nullopt }); +} + +void handle_get_public_data_leaf_preimage(WsdbRefContext& ctx, + wire::WsdbGetPublicDataLeafPreimage&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + if (!check_revision(revision, respond)) { + return; + } + auto preimage = ctx.world_state.get_public_data_preimage(revision, cmd.leafIndex); + respond.ok({ .preimage = preimage ? std::optional(to_wire(*preimage)) : std::nullopt }); +} + +void handle_get_nullifier_leaf_preimage(WsdbRefContext& ctx, + wire::WsdbGetNullifierLeafPreimage&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + if (!check_revision(revision, respond)) { + return; + } + auto preimage = ctx.world_state.get_nullifier_preimage(revision, cmd.leafIndex); + respond.ok({ .preimage = preimage ? std::optional(to_wire(*preimage)) : std::nullopt }); +} + +void handle_get_sibling_path(WsdbRefContext& ctx, + wire::WsdbGetSiblingPath&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + auto id = check_read(cmd.treeId, revision, [](MerkleTreeId) { return true; }, "get_sibling_path", respond); + if (!id) { + return; + } + if (cmd.leafIndex >= (uint64_t(1) << ReferenceWorldState::tree_height(*id))) { + respond.error("leaf index " + std::to_string(cmd.leafIndex) + " is outside tree " + std::to_string(cmd.treeId)); + return; + } + respond.ok({ .path = path_to_wire(ctx.world_state.get_sibling_path(revision, *id, cmd.leafIndex)) }); +} + +// --------------------------------------------------------------------------- +// Lookups +// --------------------------------------------------------------------------- + +void handle_find_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindLeafIndices&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + auto id = check_read(cmd.treeId, revision, is_append_only, "find_leaf_indices", respond); + if (!id) { + return; + } + std::vector> indices; + for (const auto& leaf : cmd.leaves) { + indices.push_back(ctx.world_state.find_leaf_index(revision, *id, to_fr(leaf), cmd.startIndex)); + } + respond.ok({ .indices = to_wire_indices(indices) }); +} + +void handle_find_public_data_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindPublicDataLeafIndices&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + if (!check_revision(revision, respond)) { + return; + } + // Public data is keyed by slot: the leaf at a slot is found whatever value it now holds. + std::vector> indices; + for (const auto& leaf : cmd.leaves) { + indices.push_back(ctx.world_state.find_indexed_leaf_index( + revision, MerkleTreeId::PUBLIC_DATA_TREE, to_fr(leaf.slot), cmd.startIndex)); + } + respond.ok({ .indices = to_wire_indices(indices) }); +} + +void handle_find_nullifier_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindNullifierLeafIndices&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + if (!check_revision(revision, respond)) { + return; + } + std::vector> indices; + for (const auto& leaf : cmd.leaves) { + indices.push_back(ctx.world_state.find_indexed_leaf_index( + revision, MerkleTreeId::NULLIFIER_TREE, to_fr(leaf.nullifier), cmd.startIndex)); + } + respond.ok({ .indices = to_wire_indices(indices) }); +} + +void handle_find_low_leaf(WsdbRefContext& ctx, + wire::WsdbFindLowLeaf&& cmd, + Responder respond) +{ + auto revision = revision_of(cmd.revision); + auto id = check_read(cmd.treeId, revision, is_indexed, "find_low_leaf", respond); + if (!id) { + return; + } + auto low = ctx.world_state.find_low_leaf(revision, *id, to_fr(cmd.key)); + respond.ok({ .alreadyPresent = low.is_already_present, .index = low.index }); +} + +// --------------------------------------------------------------------------- +// Writes +// --------------------------------------------------------------------------- + +void handle_append_leaves(WsdbRefContext& ctx, + wire::WsdbAppendLeaves&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + auto id = tree_id_of(cmd.treeId); + if (!id || !is_append_only(*id)) { + respond.error("append_leaves is not supported for tree " + std::to_string(cmd.treeId)); + return; + } + std::vector leaves; + leaves.reserve(cmd.leaves.size()); + for (const auto& leaf : cmd.leaves) { + leaves.push_back(to_fr(leaf)); + } + ctx.world_state.append_leaves(*id, leaves); + respond.ok({}); +} + +void handle_sequential_insert_public_data(WsdbRefContext& ctx, + wire::WsdbSequentialInsertPublicData&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + wire::SequentialInsertionResultPublicData result; + for (const auto& leaf : cmd.leaves) { + auto inserted = ctx.world_state.insert_public_data(PublicDataLeafValue(to_fr(leaf.slot), to_fr(leaf.value))); + for (const auto& w : inserted.low_leaf_witness_data) { + result.lowLeafWitnessData.push_back(to_wire(w)); + } + for (const auto& w : inserted.insertion_witness_data) { + result.insertionWitnessData.push_back(to_wire(w)); + } + } + respond.ok({ .result = std::move(result) }); +} + +void handle_sequential_insert_nullifier(WsdbRefContext& ctx, + wire::WsdbSequentialInsertNullifier&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + // Validate the whole batch first, so a rejected request changes nothing. + std::vector nullifiers; + for (const auto& leaf : cmd.leaves) { + FF nullifier = to_fr(leaf.nullifier); + bool repeated = ctx.world_state.has_nullifier(nullifier); + for (const auto& earlier : nullifiers) { + repeated = repeated || earlier == nullifier; + } + if (repeated) { + respond.error("nullifier " + format(nullifier) + " already exists"); + return; + } + nullifiers.push_back(nullifier); + } + wire::SequentialInsertionResultNullifier result; + for (const auto& nullifier : nullifiers) { + auto inserted = ctx.world_state.insert_nullifier(NullifierLeafValue(nullifier)); + for (const auto& w : inserted.low_leaf_witness_data) { + result.lowLeafWitnessData.push_back(to_wire(w)); + } + for (const auto& w : inserted.insertion_witness_data) { + result.insertionWitnessData.push_back(to_wire(w)); + } + } + respond.ok({ .result = std::move(result) }); +} + +void handle_update_archive(WsdbRefContext& ctx, + wire::WsdbUpdateArchive&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + std::vector state; + for (const auto& tree : cmd.blockStateRef) { + auto id = tree_id_of(tree.treeId); + if (!id) { + respond.error("unknown tree id " + std::to_string(tree.treeId)); + return; + } + state.push_back({ *id, to_fr(tree.root), tree.size }); + } + if (!ctx.world_state.state_matches(state)) { + respond.error("Can't update archive tree: Block state does not match world state"); + return; + } + ctx.world_state.update_archive(to_fr(cmd.blockHeaderHash)); + respond.ok({}); +} + +// --------------------------------------------------------------------------- +// Checkpoints +// --------------------------------------------------------------------------- + +void handle_create_checkpoint(WsdbRefContext& ctx, + wire::WsdbCreateCheckpoint&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + ctx.world_state.create_checkpoint(); + respond.ok({}); +} + +void handle_commit_checkpoint(WsdbRefContext& ctx, + wire::WsdbCommitCheckpoint&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + if (!ctx.world_state.has_checkpoint()) { + respond.error("no checkpoint to commit"); + return; + } + ctx.world_state.commit_checkpoint(); + respond.ok({}); +} + +void handle_revert_checkpoint(WsdbRefContext& ctx, + wire::WsdbRevertCheckpoint&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + if (!ctx.world_state.has_checkpoint()) { + respond.error("no checkpoint to revert"); + return; + } + ctx.world_state.revert_checkpoint(); + respond.ok({}); +} + +void handle_commit_all_checkpoints(WsdbRefContext& ctx, + wire::WsdbCommitAllCheckpoints&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + ctx.world_state.commit_all_checkpoints(); + respond.ok({}); +} + +void handle_revert_all_checkpoints(WsdbRefContext& ctx, + wire::WsdbRevertAllCheckpoints&& cmd, + Responder respond) +{ + if (!check_fork(cmd.forkId, respond)) { + return; + } + ctx.world_state.revert_all_checkpoints(); + respond.ok({}); +} + +// The in-process FFI entry's context (generated/wsdb_ffi.hpp): one reference world state per loaded +// module, at the canonical genesis, shared by every call as over one transport connection. +AsyncDispatchHandler& ipc_ffi_dispatcher() +{ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static WsdbRefContext ctx; + static AsyncDispatchHandler handler = make_wsdb_handler(ctx); + return handler; +} + +} // namespace bb::wsdb_ref diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp new file mode 100644 index 00000000000..0e72637a103 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp @@ -0,0 +1,90 @@ +#pragma once +/** + * @file wsdb_ref_handlers.hpp + * @brief The wsdb base contract (wsdb_base_schema.jsonc) served by the in-memory reference world state. + * + * The generated dispatch (generated/wsdb_dispatch.hpp) declares template handle_; the + * overloads below, for Ctx = WsdbRefContext, are preferred at its instantiation in + * make_wsdb_handler. Every handler answers synchronously. A request the reference cannot + * serve (another fork, a historical block, an unsupported tree, a duplicate nullifier) is answered with + * an error, never an exception, so a wasm build (no exceptions) keeps running after it. + */ +#include "barretenberg/world_state_reference/reference_world_state.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_dispatch.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_types.hpp" + +namespace bb::wsdb_ref { + +/** @brief The state a wsdb-ref service runs over: one reference world state at the canonical genesis. */ +struct WsdbRefContext { + world_state::ReferenceWorldState world_state; +}; + +void handle_get_tree_info(WsdbRefContext& ctx, + wire::WsdbGetTreeInfo&& cmd, + Responder respond); +void handle_get_state_reference(WsdbRefContext& ctx, + wire::WsdbGetStateReference&& cmd, + Responder respond); +void handle_get_initial_state_reference(WsdbRefContext& ctx, + wire::WsdbGetInitialStateReference&& cmd, + Responder respond); +void handle_get_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetLeafValue&& cmd, + Responder respond); +void handle_get_public_data_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetPublicDataLeafValue&& cmd, + Responder respond); +void handle_get_nullifier_leaf_value(WsdbRefContext& ctx, + wire::WsdbGetNullifierLeafValue&& cmd, + Responder respond); +void handle_get_public_data_leaf_preimage(WsdbRefContext& ctx, + wire::WsdbGetPublicDataLeafPreimage&& cmd, + Responder respond); +void handle_get_nullifier_leaf_preimage(WsdbRefContext& ctx, + wire::WsdbGetNullifierLeafPreimage&& cmd, + Responder respond); +void handle_get_sibling_path(WsdbRefContext& ctx, + wire::WsdbGetSiblingPath&& cmd, + Responder respond); +void handle_find_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindLeafIndices&& cmd, + Responder respond); +void handle_find_public_data_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindPublicDataLeafIndices&& cmd, + Responder respond); +void handle_find_nullifier_leaf_indices(WsdbRefContext& ctx, + wire::WsdbFindNullifierLeafIndices&& cmd, + Responder respond); +void handle_find_low_leaf(WsdbRefContext& ctx, + wire::WsdbFindLowLeaf&& cmd, + Responder respond); +void handle_append_leaves(WsdbRefContext& ctx, + wire::WsdbAppendLeaves&& cmd, + Responder respond); +void handle_sequential_insert_public_data(WsdbRefContext& ctx, + wire::WsdbSequentialInsertPublicData&& cmd, + Responder respond); +void handle_sequential_insert_nullifier(WsdbRefContext& ctx, + wire::WsdbSequentialInsertNullifier&& cmd, + Responder respond); +void handle_update_archive(WsdbRefContext& ctx, + wire::WsdbUpdateArchive&& cmd, + Responder respond); +void handle_create_checkpoint(WsdbRefContext& ctx, + wire::WsdbCreateCheckpoint&& cmd, + Responder respond); +void handle_commit_checkpoint(WsdbRefContext& ctx, + wire::WsdbCommitCheckpoint&& cmd, + Responder respond); +void handle_revert_checkpoint(WsdbRefContext& ctx, + wire::WsdbRevertCheckpoint&& cmd, + Responder respond); +void handle_commit_all_checkpoints(WsdbRefContext& ctx, + wire::WsdbCommitAllCheckpoints&& cmd, + Responder respond); +void handle_revert_all_checkpoints(WsdbRefContext& ctx, + wire::WsdbRevertAllCheckpoints&& cmd, + Responder respond); + +} // namespace bb::wsdb_ref diff --git a/noir-projects/fnd/scripts/regenerate_genesis_constants.sh b/noir-projects/fnd/scripts/regenerate_genesis_constants.sh index 7449b80191e..8c6fad9a327 100755 --- a/noir-projects/fnd/scripts/regenerate_genesis_constants.sh +++ b/noir-projects/fnd/scripts/regenerate_genesis_constants.sh @@ -9,7 +9,8 @@ # 1. noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr # GENESIS_NULLIFIER_TREE_ROOT, GENESIS_BLOCK_HEADER_HASH, GENESIS_ARCHIVE_ROOT. # Source of truth: aztec_constants.hpp, ConstantsGen.sol and labs' constants.gen.ts all derive from it. -# 2. native-packages/wsdb/cpp/src/world_state/genesis_protocol_nullifiers.hpp +# 2. barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp +# native-packages/wsdb/cpp/src/world_state/genesis_protocol_nullifiers.hpp # The seed vector the C++ world-state test builds its genesis from. # 3. l1-contracts/test/fixtures/{empty,mixed,single_tx}_checkpoint_{1,2}.json (--fixtures) # Checkpoint 1 of each family starts from the genesis archive; checkpoint 2 chains off checkpoint 1. @@ -48,6 +49,7 @@ done constants_nr=noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr seeds_hpp=native-packages/wsdb/cpp/src/world_state/genesis_protocol_nullifiers.hpp +bb_seeds_hpp=barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp cpp_constants=barretenberg/cpp/src/barretenberg/aztec/aztec_constants.hpp sol_constants=l1-contracts/src/core/libraries/ConstantsGen.sol ts_constants=labs/yarn-project/constants/src/constants.gen.ts @@ -90,19 +92,21 @@ echo " GENESIS_BLOCK_HEADER_HASH = $HEADER_HASH" echo " GENESIS_ARCHIVE_ROOT = $ARCHIVE_ROOT" echo " seeds = $(echo "$SEEDS" | wc -w)" +# The same seed vector for two C++ trees: wsdb's (its own field type) and barretenberg's reference world +# state, whose tests rebuild the canonical genesis from it. function write_seeds_header { - local file=$1 + local file=$1 field_include=$2 namespace=$3 field=$4 { - cat <<'EOF' + cat < -namespace azteclabs::wsdb::world_state { +namespace $namespace { /** * @brief The protocol contracts' registration nullifiers, seeded into the nullifier tree of a production genesis. @@ -113,12 +117,12 @@ namespace azteclabs::wsdb::world_state { * three roots below, and GENESIS_NULLIFIER_TREE_ROOT, GENESIS_BLOCK_HEADER_HASH and GENESIS_ARCHIVE_ROOT in * constants.nr, so all of them are rewritten together and never on their own. */ -inline std::vector genesis_protocol_nullifiers() +inline std::vector<$field> genesis_protocol_nullifiers() { return { EOF - for seed in $SEEDS; do echo " fr(\"$seed\"),"; done - cat <<'EOF' + for seed in $SEEDS; do echo " $field(\"$seed\"),"; done + cat <"$file" } @@ -164,10 +164,14 @@ if [ "$check_only" = 1 ]; then grep -q "$2" "$constants_nr" || { echo " stale: $1 in $constants_nr"; stale=1; } done for seed in $SEEDS; do - grep -q "$seed" "$seeds_hpp" || { echo " stale: $seed missing from $seeds_hpp"; stale=1; } + for header in "$seeds_hpp" "$bb_seeds_hpp"; do + grep -q "$seed" "$header" || { echo " stale: $seed missing from $header"; stale=1; } + done done for root in "$NULLIFIER_ROOT" "$HEADER_HASH" "$ARCHIVE_ROOT"; do - grep -q "$root" "$seeds_hpp" || { echo " stale: $root missing from $seeds_hpp"; stale=1; } + for header in "$seeds_hpp" "$bb_seeds_hpp"; do + grep -q "$root" "$header" || { echo " stale: $root missing from $header"; stale=1; } + done done grep -q "$ARCHIVE_ROOT" l1-contracts/test/fixtures/empty_checkpoint_1.json || { echo " stale: l1-contracts/test/fixtures/*_checkpoint_*.json (rerun with --fixtures)"; stale=1; } @@ -178,7 +182,8 @@ fi phase "Update the pinned sources" node "$helper" write-constants "$constants_nr" "$NULLIFIER_ROOT" "$HEADER_HASH" "$ARCHIVE_ROOT" -write_seeds_header "$seeds_hpp" +write_seeds_header "$seeds_hpp" field/field_element.hpp azteclabs::wsdb::world_state fr +write_seeds_header "$bb_seeds_hpp" barretenberg/ecc/curves/bn254/fr.hpp bb::world_state bb::fr phase "Regenerate the derived constants" # constants-codegen embeds constants.nr at build time, so it must be rebuilt before the remake scripts run, or they @@ -216,11 +221,11 @@ if [ "$with_fixtures" = 1 ]; then fi phase "Stage" -git add "$constants_nr" "$seeds_hpp" +git add "$constants_nr" "$seeds_hpp" "$bb_seeds_hpp" if [ "$with_fixtures" = 1 ]; then git add l1-contracts/test/fixtures/; fi echo -git --no-pager diff --cached --stat -- "$constants_nr" "$seeds_hpp" l1-contracts/test/fixtures/ +git --no-pager diff --cached --stat -- "$constants_nr" "$seeds_hpp" "$bb_seeds_hpp" l1-contracts/test/fixtures/ echo if [ "$with_fixtures" = 1 ]; then echo "Done. Review the staged diff and commit." From 166046a3c2910948dab3b5624bb4cd2e1e29473f Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:44:34 +0000 Subject: [PATCH 03/20] feat(bb.js): generated @aztec-foundation/wsdb-ref package The in-memory reference world state as a fully generated client package, from wsdb_base_schema.jsonc with --package-transports napi,wasm: the wasm module in node and browsers, or the Node-API addon in node, which create() prefers when its arch package resolves. Built by `make bb-wsdb-ref`, cross-copied and published with the other barretenberg packages, and tested on both backends by `make bb-wsdb-ref-tests`. --- Makefile | 18 +++- barretenberg/ts/.gitignore | 1 + barretenberg/ts/.rebuild_patterns | 1 + barretenberg/ts/bootstrap.sh | 106 ++++++++++++++++++- barretenberg/ts/package.json | 4 +- barretenberg/ts/scripts/wsdb_ref.test.mjs | 123 ++++++++++++++++++++++ barretenberg/ts/scripts/wsdb_ref_test.sh | 7 ++ barretenberg/ts/yarn.lock | 49 +++++++++ ipc-codegen/README.md | 11 +- 9 files changed, 311 insertions(+), 9 deletions(-) create mode 100644 barretenberg/ts/scripts/wsdb_ref.test.mjs create mode 100755 barretenberg/ts/scripts/wsdb_ref_test.sh diff --git a/Makefile b/Makefile index 584f0284ab7..9e28f6559c0 100644 --- a/Makefile +++ b/Makefile @@ -104,7 +104,7 @@ bench-labs: labs-bench bench: bench-foundation bench-labs # Release. Everything plus copy bb cross compiles to ts projects. -release-foundation: fast-foundation bb-cpp-release-dir bb-ts-cross-copy bb-avm-sim-cross-copy bb-bin-cross-copy ipc-runtime-cross +release-foundation: fast-foundation bb-cpp-release-dir bb-ts-cross-copy bb-avm-sim-cross-copy bb-wsdb-ref-cross-copy bb-bin-cross-copy ipc-runtime-cross release-labs: fast-labs @@ -143,7 +143,7 @@ avm-transpiler-cross: avm-transpiler-cross-amd64-macos avm-transpiler-cross-arm6 #============================================================================== # Barretenberg - Aggregate target for all barretenberg sub-projects. -barretenberg: bb-cpp bb-ts bb-avm-sim bb-cdb bb-bin bb-rs bb-acir bb-docs bb-sol bb-bbup bb-crs +barretenberg: bb-cpp bb-ts bb-avm-sim bb-cdb bb-wsdb-ref bb-bin bb-rs bb-acir bb-docs bb-sol bb-bbup bb-crs # BB C++ - Main aggregate target. bb-cpp: bb-cpp-native bb-cpp-wasm bb-cpp-wasm-threads @@ -290,6 +290,15 @@ bb-avm-sim-cross-copy: bb-avm-sim bb-cdb bb-ts-cross-copy bb-cpp-cross bb-cdb: ipc-codegen ipc-runtime bb-avm-sim $(call build,$@,barretenberg/ts,build_cdb) +# @aztec-foundation/wsdb-ref: the in-memory reference world state, run in-process through its +# wasm module or its Node-API addon. Ordered after bb-cdb because every barretenberg/ts target +# regenerates the same workspaces and installs into the same node_modules. +bb-wsdb-ref: ipc-codegen ipc-runtime bb-cpp-native bb-cpp-wasm bb-cdb + $(call build,$@,barretenberg/ts,build_wsdb_ref) + +bb-wsdb-ref-cross-copy: bb-wsdb-ref bb-avm-sim-cross-copy bb-cpp-cross + $(call build,$@,barretenberg/ts,cross_copy_wsdb_ref) + # bb and bb-avm as npm packages (meta + one package per platform). bb-bin: bb-cpp-native $(call build,$@,barretenberg/ts,build_bb_bin) @@ -347,6 +356,9 @@ bb-ts-tests: bb-ts bb-avm-sim-tests: bb-avm-sim bb-cdb $(call test,$@,barretenberg/ts,bb_avm_sim) +bb-wsdb-ref-tests: bb-wsdb-ref + $(call test,$@,barretenberg/ts,wsdb_ref) + bb-sol-tests: bb-sol $(call test,$@,barretenberg/sol) @@ -359,7 +371,7 @@ bb-bbup-tests: bb-bbup bb-rs-tests: bb-rs $(call test,$@,barretenberg/rust) -bb-tests: bb-cpp-native-tests bb-cpp-wasm-tests bb-acir-tests bb-ts-tests bb-avm-sim-tests bb-sol-tests bb-bbup-tests bb-docs-tests bb-rs-tests +bb-tests: bb-cpp-native-tests bb-cpp-wasm-tests bb-acir-tests bb-ts-tests bb-avm-sim-tests bb-wsdb-ref-tests bb-sol-tests bb-bbup-tests bb-docs-tests bb-rs-tests bb-full-tests: bb-cpp-wasm-threads-tests bb-cpp-asan-tests bb-cpp-smt-tests diff --git a/barretenberg/ts/.gitignore b/barretenberg/ts/.gitignore index ecde99a3073..177321fcb6f 100644 --- a/barretenberg/ts/.gitignore +++ b/barretenberg/ts/.gitignore @@ -10,6 +10,7 @@ bb.js/*.tsbuildinfo bb-avm-sim/ bb.js-api/ cdb/ +wsdb-ref/ .tsbuildinfo* *.tsbuildinfo *.log diff --git a/barretenberg/ts/.rebuild_patterns b/barretenberg/ts/.rebuild_patterns index 7c88bc2f15a..7f7971394db 100644 --- a/barretenberg/ts/.rebuild_patterns +++ b/barretenberg/ts/.rebuild_patterns @@ -6,3 +6,4 @@ ^barretenberg/cpp/src/barretenberg/bbapi/bb_schema\.json$ ^barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants\.json$ ^barretenberg/cpp/src/barretenberg/cdb/cdb_schema\.json$ +^barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema\.jsonc$ diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index fe148f8f1c2..17be017c379 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -7,6 +7,8 @@ BB_AVM_SIM_BINARY=bb-avm-sim BB_AVM_SIM_PACKAGE=@aztec-foundation/bb-avm-sim CDB_PACKAGE=@aztec-foundation/cdb BB_JS_API_PACKAGE=@aztec-foundation/bb.js-api +WSDB_REF_PACKAGE=@aztec-foundation/wsdb-ref +WSDB_REF_ADDON=wsdb_ref_napi.node hash=$(hash_str \ $(bb.js/bootstrap.sh hash) \ @@ -80,13 +82,30 @@ function generate_bb_js_api_package { --package-wasm-threads-module "barretenberg-threads.wasm=$ROOT/barretenberg/cpp/build-wasm-threads/bin/barretenberg.wasm" } -# bb-avm-sim, cdb and bb.js-api are gitignored workspaces declared in package.json, so +# The in-memory reference world state over the wsdb base contract, fully generated: in-process +# through its wasm module (node and browsers) or, opted into here with napi, its Node-API addon. +# Both run the one FFI entry the bb build generates from the same schema (cpp/.../wsdb_ref). +function generate_wsdb_ref_package { + node --experimental-strip-types --no-warnings \ + "$ROOT/ipc-codegen/src/generate.ts" \ + --schema "$ROOT/barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc" \ + --lang ts \ + --package "$ROOT/barretenberg/ts/wsdb-ref" \ + --package-name "$WSDB_REF_PACKAGE" \ + --package-transports napi,wasm \ + --package-napi-addon "$WSDB_REF_ADDON" \ + --package-napi-addon-env-var WSDB_REF_NAPI_PATH \ + --package-wasm-module wsdb-ref.wasm +} + +# bb-avm-sim, cdb, bb.js-api and wsdb-ref are gitignored workspaces declared in package.json, so # `yarn install --immutable` fails against the committed lockfile unless all exist. # Generate them together before installing, whichever one we're about to build. function generate_packages { generate_bb_avm_sim_package generate_cdb_package generate_bb_js_api_package + generate_wsdb_ref_package } function copy_bb_js_api_native { @@ -163,6 +182,48 @@ function build_bb_js { (cd bb.js && ./bootstrap.sh) } +# The single-thread module only: the reference world state runs no threads. +function copy_wsdb_ref_wasm { + rm -rf wsdb-ref/wasm + mkdir -p wsdb-ref/wasm + cp "$ROOT/barretenberg/cpp/build-wasm/bin/wsdb-ref.wasm" wsdb-ref/wasm/wsdb-ref.wasm +} + +function copy_wsdb_ref_native { + local target_dir="wsdb-ref/build/$(arch)-$(os)" + mkdir -p "$target_dir" + cp "$ROOT/barretenberg/cpp/build/lib/$WSDB_REF_ADDON" "$target_dir/$WSDB_REF_ADDON" +} + +function copy_wsdb_ref_cross { + if [ -n "${1:-}" ]; then + local cross_arch="$1" + mkdir -p "wsdb-ref/build/$cross_arch" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/lib/$WSDB_REF_ADDON" "wsdb-ref/build/$cross_arch/$WSDB_REF_ADDON" + elif semver check "${REF_NAME:-}" && [ "$(arch)" == "amd64" ]; then + for cross_arch in arm64-linux amd64-macos arm64-macos; do + mkdir -p "wsdb-ref/build/$cross_arch" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/lib/$WSDB_REF_ADDON" "wsdb-ref/build/$cross_arch/$WSDB_REF_ADDON" + done + else + echo "This task is expected to be run with an explicit arch or in an x86 release context." + fi +} + +function prepare_wsdb_ref_arch_packages { + yarn workspace "$WSDB_REF_PACKAGE" run prepare_arch_packages "$@" +} + +function build_wsdb_ref { + echo_header "wsdb-ref package build" + generate_packages + copy_wsdb_ref_wasm + copy_wsdb_ref_native + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$WSDB_REF_PACKAGE" build + prepare_wsdb_ref_arch_packages +} + function build_bb_avm_sim { echo_header "bb-avm-sim package build" generate_packages @@ -224,13 +285,19 @@ function build { build_cdb } -# `test_cmds bb_avm_sim` lists the bb-avm-sim package's tests alone: they need its build -# (bb-avm-sim), which bb.js's tests do not. +# `test_cmds bb_avm_sim` and `test_cmds wsdb_ref` list those packages' tests alone: they need their +# builds (bb-avm-sim, bb-wsdb-ref), which bb.js's tests do not. function test_cmds { case "${1:-}" in bb_avm_sim) echo "$hash barretenberg/ts/scripts/bb_avm_sim_test.sh" ;; + wsdb_ref) + local backend + for backend in napi wasm; do + echo "$hash barretenberg/ts/scripts/wsdb_ref_test.sh $backend" + done + ;; *) (cd bb.js && ./bootstrap.sh test_cmds) ;; @@ -250,6 +317,14 @@ function cross_copy_bb_js { cross_copy_bb_js_api "$@" } +function cross_copy_wsdb_ref { + generate_packages + copy_wsdb_ref_cross "$@" + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$WSDB_REF_PACKAGE" build + prepare_wsdb_ref_arch_packages +} + function cross_copy_bb_avm_sim { generate_packages copy_bb_avm_sim_cross "$@" @@ -279,6 +354,12 @@ function get_projects { if [ -d cdb ]; then echo "$PWD/cdb" fi + if [ -d wsdb-ref ]; then + for package_dir in wsdb-ref/packages/*; do + [ -d "$package_dir" ] && echo "$PWD/$package_dir" + done + echo "$PWD/wsdb-ref" + fi } function release_bb_avm_sim { @@ -294,6 +375,23 @@ function release_bb_avm_sim { (cd bb-avm-sim && retry "deploy_npm ${REF_NAME#v}") } +function release_wsdb_ref { + generate_packages + copy_wsdb_ref_wasm + copy_wsdb_ref_native + copy_wsdb_ref_cross + npm_install_deps "$IPC_RUNTIME_PKG" + yarn workspace "$WSDB_REF_PACKAGE" build + prepare_wsdb_ref_arch_packages + local package_dir + for package_dir in wsdb-ref/packages/*; do + # A platform whose addon was not built has only the generated manifest: nothing to publish. + [ -f "$package_dir/$WSDB_REF_ADDON" ] || continue + (cd "$package_dir" && retry "deploy_npm ${REF_NAME#v}") + done + (cd wsdb-ref && retry "deploy_npm ${REF_NAME#v}") +} + function release_cdb { generate_packages npm_install_deps "$IPC_RUNTIME_PKG" @@ -328,12 +426,14 @@ function release { (cd bb.js && ./bootstrap.sh release) release_bb_avm_sim release_cdb + release_wsdb_ref release_bb_bin } export -f generate_bb_avm_sim_package copy_bb_avm_sim_native copy_bb_avm_sim_cross generate_cdb_package generate_packages export -f generate_bb_js_api_package copy_bb_js_api_native copy_bb_js_api_cross prepare_bb_js_api_arch_packages build_bb_js_api_ts build_bb_js_api export -f build_bb_js build_bb_avm_sim build_cdb build cross_copy_bb_js cross_copy_bb_avm_sim cross_copy_bb_js_api release release_cdb release_bb_js_api +export -f generate_wsdb_ref_package copy_wsdb_ref_wasm copy_wsdb_ref_native copy_wsdb_ref_cross prepare_wsdb_ref_arch_packages build_wsdb_ref cross_copy_wsdb_ref release_wsdb_ref case "$cmd" in "") diff --git a/barretenberg/ts/package.json b/barretenberg/ts/package.json index ab6588c9187..022fc779009 100644 --- a/barretenberg/ts/package.json +++ b/barretenberg/ts/package.json @@ -12,7 +12,9 @@ "bb.js-api/packages/*", "bb-avm-sim", "bb-avm-sim/packages/*", - "cdb" + "cdb", + "wsdb-ref", + "wsdb-ref/packages/*" ], "resolutions": { "@aztec-foundation/ipc-runtime": "portal:../../ipc-runtime/ts" diff --git a/barretenberg/ts/scripts/wsdb_ref.test.mjs b/barretenberg/ts/scripts/wsdb_ref.test.mjs new file mode 100644 index 00000000000..c79ff196c7d --- /dev/null +++ b/barretenberg/ts/scripts/wsdb_ref.test.mjs @@ -0,0 +1,123 @@ +// Tests of the generated @aztec-foundation/wsdb-ref package against one backend, from its build. +// Run through wsdb_ref_test.sh, which sets WSDB_REF_TEST_BACKEND to napi or wasm. +// +// Every napi backend in a process shares one world state (node loads the addon once), so the +// tests read the genesis through committed reads, which writes never change, and write only fresh +// random keys. +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { randomBytes } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { after, before, test } from 'node:test'; + +import { WsdbError, WsdbService, WsdbServiceSync } from '@aztec-foundation/wsdb-ref'; + +const backend = process.env.WSDB_REF_TEST_BACKEND; +assert.ok(backend === 'napi' || backend === 'wasm', `WSDB_REF_TEST_BACKEND must be napi or wasm, got ${backend}`); + +const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim(); +const constantsNr = readFileSync( + join(root, 'noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr'), + 'utf8', +); +function protocolConstant(name) { + const match = constantsNr.match(new RegExp(`pub global ${name}: Field =\\s*(0x[0-9a-f]+);`)); + assert.ok(match, `${name} not found in constants.nr`); + return BigInt(match[1]); +} + +const NULLIFIER_TREE = 0; +const NOTE_HASH_TREE = 1; +const ARCHIVE = 4; +const LATEST = 0xffffffff; +const committed = { forkId: 0, blockNumber: LATEST, includeUncommitted: false }; +const uncommitted = { forkId: 0, blockNumber: LATEST, includeUncommitted: true }; + +const toBigInt = bytes => BigInt('0x' + Buffer.from(bytes).toString('hex')); +// A random field element: 31 random bytes stay below the modulus. +const randomFr = () => new Uint8Array([0, ...randomBytes(31)]); + +let service; +before(async () => { + service = await WsdbService.create({ backend }); +}); +after(async () => { + await service.destroy(); +}); + +test('serves the canonical genesis', async () => { + const nullifiers = await service.getTreeInfo({ treeId: NULLIFIER_TREE, revision: committed }); + assert.equal(toBigInt(nullifiers.root), protocolConstant('GENESIS_NULLIFIER_TREE_ROOT')); + assert.equal(nullifiers.size, 128); + + const archive = await service.getTreeInfo({ treeId: ARCHIVE, revision: committed }); + assert.equal(toBigInt(archive.root), protocolConstant('GENESIS_ARCHIVE_ROOT')); + assert.equal(archive.size, 1); + + const header = await service.getLeafValue({ treeId: ARCHIVE, revision: committed, leafIndex: 0 }); + assert.equal(toBigInt(header.value), protocolConstant('GENESIS_BLOCK_HEADER_HASH')); + + const { state } = await service.getInitialStateReference({}); + assert.deepEqual(state.map(t => t.treeId).sort(), [0, 1, 2, 3]); +}); + +test('appended leaves are read back with membership witnesses', async () => { + const leaf = randomFr(); + await service.appendLeaves({ treeId: NOTE_HASH_TREE, leaves: [leaf], forkId: 0 }); + const { + indices: [index], + } = await service.findLeafIndices({ treeId: NOTE_HASH_TREE, revision: uncommitted, leaves: [leaf], startIndex: 0 }); + assert.equal(typeof index, 'number'); + const { value } = await service.getLeafValue({ treeId: NOTE_HASH_TREE, revision: uncommitted, leafIndex: index }); + assert.deepEqual(Buffer.from(value), Buffer.from(leaf)); + const { path } = await service.getSiblingPath({ treeId: NOTE_HASH_TREE, revision: uncommitted, leafIndex: index }); + assert.equal(path.length, 42); + // Committed reads still see the genesis. + const { indices } = await service.findLeafIndices({ + treeId: NOTE_HASH_TREE, + revision: committed, + leaves: [leaf], + startIndex: 0, + }); + assert.deepEqual(indices, [null]); +}); + +test('nullifier inserts return witnesses, and duplicates are the service saying no', async () => { + const nullifier = randomFr(); + const { result } = await service.sequentialInsertNullifier({ leaves: [{ nullifier }], forkId: 0 }); + assert.equal(result.lowLeafWitnessData.length, 1); + assert.equal(result.insertionWitnessData.length, 1); + const low = await service.findLowLeaf({ treeId: NULLIFIER_TREE, revision: uncommitted, key: nullifier }); + assert.equal(low.alreadyPresent, true); + + await assert.rejects(service.sequentialInsertNullifier({ leaves: [{ nullifier }], forkId: 0 }), error => { + assert.ok(error instanceof WsdbError); + assert.match(error.message, /already exists/); + return true; + }); +}); + +test('requests outside the reference are errors, not crashes', async () => { + await assert.rejects( + service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, forkId: 1 } }), + /the reference world state has one fork \(0\), not 1/, + ); + await assert.rejects( + service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, blockNumber: 7 } }), + /keeps no block history/, + ); + // The service still answers afterwards (a wasm module that aborted would not). + const info = await service.getTreeInfo({ treeId: ARCHIVE, revision: committed }); + assert.equal(info.size, 1); +}); + +test('the synchronous service reaches the same world state', async () => { + const sync = await WsdbServiceSync.create({ backend }); + try { + const archive = sync.getTreeInfo({ treeId: ARCHIVE, revision: committed }); + assert.equal(toBigInt(archive.root), protocolConstant('GENESIS_ARCHIVE_ROOT')); + } finally { + sync.destroy(); + } +}); diff --git a/barretenberg/ts/scripts/wsdb_ref_test.sh b/barretenberg/ts/scripts/wsdb_ref_test.sh new file mode 100755 index 00000000000..2fb0ba4878a --- /dev/null +++ b/barretenberg/ts/scripts/wsdb_ref_test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Runs the @aztec-foundation/wsdb-ref package tests against one backend: napi or wasm. +# Needs the package built (make bb-wsdb-ref, or ./bootstrap.sh build_wsdb_ref here). +set -euo pipefail +backend=${1:?usage: wsdb_ref_test.sh } +cd "$(dirname "$0")/.." +WSDB_REF_TEST_BACKEND="$backend" node --test --test-reporter=spec scripts/wsdb_ref.test.mjs diff --git a/barretenberg/ts/yarn.lock b/barretenberg/ts/yarn.lock index ef513e4233d..20edce0514a 100644 --- a/barretenberg/ts/yarn.lock +++ b/barretenberg/ts/yarn.lock @@ -169,6 +169,55 @@ __metadata: languageName: node linkType: soft +"@aztec-foundation/wsdb-ref-darwin-arm64@npm:0.1.0, @aztec-foundation/wsdb-ref-darwin-arm64@workspace:wsdb-ref/packages/wsdb-ref-darwin-arm64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref-darwin-arm64@workspace:wsdb-ref/packages/wsdb-ref-darwin-arm64" + languageName: unknown + linkType: soft + +"@aztec-foundation/wsdb-ref-darwin-x64@npm:0.1.0, @aztec-foundation/wsdb-ref-darwin-x64@workspace:wsdb-ref/packages/wsdb-ref-darwin-x64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref-darwin-x64@workspace:wsdb-ref/packages/wsdb-ref-darwin-x64" + languageName: unknown + linkType: soft + +"@aztec-foundation/wsdb-ref-linux-arm64@npm:0.1.0, @aztec-foundation/wsdb-ref-linux-arm64@workspace:wsdb-ref/packages/wsdb-ref-linux-arm64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref-linux-arm64@workspace:wsdb-ref/packages/wsdb-ref-linux-arm64" + languageName: unknown + linkType: soft + +"@aztec-foundation/wsdb-ref-linux-x64@npm:0.1.0, @aztec-foundation/wsdb-ref-linux-x64@workspace:wsdb-ref/packages/wsdb-ref-linux-x64": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref-linux-x64@workspace:wsdb-ref/packages/wsdb-ref-linux-x64" + languageName: unknown + linkType: soft + +"@aztec-foundation/wsdb-ref@workspace:wsdb-ref": + version: 0.0.0-use.local + resolution: "@aztec-foundation/wsdb-ref@workspace:wsdb-ref" + dependencies: + "@aztec-foundation/ipc-runtime": "@aztec-foundation/ipc-runtime" + "@aztec-foundation/wsdb-ref-darwin-arm64": "npm:0.1.0" + "@aztec-foundation/wsdb-ref-darwin-x64": "npm:0.1.0" + "@aztec-foundation/wsdb-ref-linux-arm64": "npm:0.1.0" + "@aztec-foundation/wsdb-ref-linux-x64": "npm:0.1.0" + "@types/node": "npm:^22.15.17" + msgpackr: "npm:^1.11.2" + tslib: "npm:^2.4.0" + typescript: "npm:^5.3.3" + dependenciesMeta: + "@aztec-foundation/wsdb-ref-darwin-arm64": + optional: true + "@aztec-foundation/wsdb-ref-darwin-x64": + optional: true + "@aztec-foundation/wsdb-ref-linux-arm64": + optional: true + "@aztec-foundation/wsdb-ref-linux-x64": + optional: true + languageName: unknown + linkType: soft + "@aztec/barretenberg-ts-packages@workspace:.": version: 0.0.0-use.local resolution: "@aztec/barretenberg-ts-packages@workspace:." diff --git a/ipc-codegen/README.md b/ipc-codegen/README.md index 3b4730433da..42b003d1ad4 100644 --- a/ipc-codegen/README.md +++ b/ipc-codegen/README.md @@ -106,10 +106,12 @@ node --experimental-strip-types --no-warnings \ |---|---| | `--server` | Emit server dispatch (matches request name to handler, deserialises, calls handler, serialises response). Pair it with an `ipc::IpcServer` from ipc-runtime. | | `--client` | Emit the typed client: one method per command over a byte backend, the same shape in every language — `Api` (Rust), `Api` (C++), `Client(Backend)` (Zig), `AsyncApi`/`SyncApi` over `IpcClientAsync`/`IpcClientSync` (TS). A backend takes the msgpack request bytes and returns the response bytes: ipc-runtime's transport (C++: `ipc_codegen::IpcBackend`), the service in-process (`--ffi`), or your own. | -| `--package ` | TS only. Emit a complete client package: the generated API over every backend the package has (`--package-transports`), with a `Service.create(options)` that picks one by default (the spawned process when the binary resolves, else the wasm module), can be forced to one, or takes a backend object of the consumer's own; one entry per host (node, `browser`, `react-native`) so each host sees only the backends that exist there; the binary resolved from an override path, an environment variable, or the installed per-platform arch package (this package's optional dependencies). With `--server` and no `--client`: a pure-TS server binding package instead, holding wire types, the `Handler` interface and `handleRequest`/`dispatch`, plus the schema file at the package root, with no binary launcher and no arch packages. The byte transport is then supplied by the consumer, e.g. `UdsIpcServer` from ipc-runtime. | +| `--package ` | TS only. Emit a complete client package: the generated API over every backend the package has (`--package-transports`), with a `Service.create(options)` that picks one by default (the first of the spawned process, the napi addon and the wasm module that is available, falling back along that order), can be forced to one, or takes a backend object of the consumer's own; one entry per host (node, `browser`, `react-native`) so each host sees only the backends that exist there; the binary resolved from an override path, an environment variable, or the installed per-platform arch package (this package's optional dependencies). With `--server` and no `--client`: a pure-TS server binding package instead, holding wire types, the `Handler` interface and `handleRequest`/`dispatch`, plus the schema file at the package root, with no binary launcher and no arch packages. The byte transport is then supplied by the consumer, e.g. `UdsIpcServer` from ipc-runtime. | | `--uds` | Rust/Zig only. Copies the `Backend` trait template (and `error.rs` for Rust) into `` so consumers can plug ipc-runtime — or any custom transport — behind the generated client. The flag name is historical: the trait is transport-agnostic. | | `--ffi` | In-process FFI, both directions of the contract in `SCHEMA_SPEC.md` ("FFI entry"). With `--client` (Rust/Zig/C++): generates the FFI backend (`ffi_backend.{rs,zig}`, `_ffi_backend.hpp`), which calls a linked library's `_ipc_ffi_entry`. With `--server` (Rust/Zig/C++): emits the entry itself — `_ffi.rs` with an `export__ffi!` macro over a `Handler + Default` type, `_ffi.zig` with `exportFfi(Handler)` over a default-initialised handler, or `_ffi.{hpp,cpp}` defining `_ipc_ffi_entry`/`_alloc`/`_free` over the generated dispatch (the service defines `ipc_ffi_dispatcher()`). Each example's FFI test runs the generated client over the FFI backend against its own entry, in-process. The service prefix on the symbols lets several services be linked into one binary. A wasm reactor built from these is what the TS `wasm` transport runs. | -| `--package-transports ` | TS `--package` only. Comma-separated transports the package offers: `uds`, `shm` (spawned process) and `wasm` (the service's wasm module, run in-process through `@aztec-foundation/ipc-runtime/wasm`; adds a browser entry). | +| `--napi` | C++ with `--server --ffi`. Also emits `_napi.cpp`, a Node-API addon over the FFI entry: `call(bytes)` on the calling thread and `callAsync(bytes)` on the libuv thread pool, serialized as the entry requires. It needs only the FFI header and `node-addon-api`; build it as a shared library named `*.node`. It is what the TS `napi` transport loads. | +| `--package-transports ` | TS `--package` only. Comma-separated transports the package offers: `uds`, `shm` (spawned process), `napi` (the service's Node-API addon, run in-process in node) and `wasm` (the service's wasm module, run in-process through `@aztec-foundation/ipc-runtime/wasm`; adds a browser entry). A package without `uds`/`shm` has no binary and no `bin`. | +| `--package-napi-addon `, `--package-napi-addon-env-var ` | TS `napi` transport. File name of the addon the owning project builds from `--napi` and stages into the arch packages (listed under `ipcRuntime.nativeFiles` in `package.json`, which `ipc-runtime-prepare-arch-packages` reads), and the environment variable that overrides its path (default `_PATH`). Node loads an addon once per process, so every napi backend in a process shares one service context. | | `--package-wasm-module =`, `--package-wasm-threads-module =` | TS `wasm` transport. The single-thread and threads builds at ``, installed into the package's `wasm/` directory as ``; at least one is required. Generation reads each module's memory import and records it in the package, since a host must create an imported memory matching it and JS cannot read it from a compiled module. | | `--package-wasm-wide-module =`, `--package-wasm-wide-threads-module =` | TS `wasm` transport. Optional wide-arithmetic builds of the two, installed the same way; the package runs one in place of its counterpart where the engine validates the wide-arithmetic proposal. | @@ -272,6 +274,11 @@ close to what it describes and tracks with that code. Whenever the wire contract changes, edit the schema, regenerate the bindings, and commit the diff. Both sides regenerate from the same committed schema, so they stay byte-compatible. +A schema may `imports` other schemas' aliases and types, so a type is written +once, and may `extends` another service's schema, becoming a strict superset of +its contract (see `SCHEMA_SPEC.md`). Code typed against a parent schema's +generated interface then accepts the client of any schema that extends it. + Each generated file embeds a `SCHEMA_HASH` (a hash of the committed schema) so callers can detect at connection time that their bindings predate the server. From 0ea04f64fb332616bbaad786300dbc6d89b33f92 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:49:21 +0000 Subject: [PATCH 04/20] feat(bb): wsdb-ref serves the full wsdb schema, request for request like aztec-wsdb wsdb-ref now generates its dispatch from native-packages/wsdb/wsdb_schema.jsonc, the schema the AVM's wsdb client is generated from, so it is a drop-in for aztec-wsdb: wsdb_base_schema.jsonc and the base/extends split are gone. ReferenceWorldState is rebuilt over trees whose versions share structure: a path-copying Merkle tree whose leaves carry their preimages, and a treap from leaf key to index. Copying a world state costs ten pointers, so it keeps a state per committed block, any number of forks (at the tip or any committed block), and nested checkpoints, all for free. On that it serves every command the node and the AVM send: reads at any revision, block numbers for leaf indices, leaf and low-leaf searches, appends, batch and sequential insertions with their witnesses, archive updates, checkpoints, forks, commit, rollback, SyncBlock, unwind, finalization, history removal and status. A failed write changes nothing. CopyStores, which copies LMDB stores, is the one command answered with an error. The schema gains InitGenesis, which restarts an in-process world state at a given genesis (prefilled public data and nullifiers, timestamp, header generator point): wsdb-ref has no command line to take it on. aztec-wsdb takes its genesis on the command line and answers InitGenesis with an error. wsdb_ref_native_diff.test.cpp drives wsdb-ref and an aztec-wsdb process with the same encoded requests through a seeded, node-shaped workload (blocks built on forks then synced, reads at every revision, fork writes, checkpoints, unwinds, finalization, history removal) and requires that a request fails on both or on neither and that every success is byte-identical. Error messages are not compared. It is opt-in through WSDB_NATIVE_BIN. Where aztec-wsdb answers by storage accident rather than by contract, the test says so: - reading a slot a batch left empty returns the empty leaf or fails depending on how aztec-wsdb stored the nodes above it; wsdb-ref returns the empty leaf; - a failed batch or sequential insertion leaves aztec-wsdb's leaf caches half-updated for that fork, so may-fail insertions go to scratch forks that are then deleted; - aztec-wsdb's Commit returns an unset status summary; - historical reads through a fork other than 0 cap index searches at that fork's block; the test sends historical reads to fork 0, as clients do. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G5s7nEWrYapDK2CWpEzcAf --- .../persistent_key_index.hpp | 123 +++ .../persistent_merkle_tree.hpp | 164 ++++ .../reference_world_state.cpp | 925 +++++++++++++++--- .../reference_world_state.hpp | 268 +++-- .../reference_world_state.test.cpp | 283 +++++- .../wsdb_base_schema.jsonc | 270 ----- .../src/barretenberg/wsdb_ref/CMakeLists.txt | 22 +- .../barretenberg/wsdb_ref/wsdb_ref.test.cpp | 96 +- .../wsdb_ref/wsdb_ref_handlers.cpp | 766 ++++++++++----- .../wsdb_ref/wsdb_ref_handlers.hpp | 121 +-- .../wsdb_ref/wsdb_ref_native_diff.test.cpp | 875 +++++++++++++++++ barretenberg/ts/.rebuild_patterns | 2 +- barretenberg/ts/bootstrap.sh | 4 +- barretenberg/ts/scripts/wsdb_ref.test.mjs | 17 +- .../wsdb/cpp/src/wsdb/wsdb_handlers.cpp | 10 + .../wsdb/cpp/src/wsdb/wsdb_handlers.hpp | 3 + native-packages/wsdb/wsdb_schema.jsonc | 11 + 17 files changed, 3079 insertions(+), 881 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/persistent_key_index.hpp create mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/persistent_merkle_tree.hpp delete mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_key_index.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_key_index.hpp new file mode 100644 index 00000000000..6846f030547 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_key_index.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include +#include + +#include "barretenberg/numeric/uint256/uint256.hpp" + +namespace bb::world_state { + +/** + * @brief An ordered map from a leaf key to its leaf index whose versions share structure. + * + * A treap with path copying: an insert copies O(log n) nodes and shares the rest, so copying the + * index is copying one pointer, like PersistentMerkleTree. Priorities are a hash of the key, so the + * shape depends only on the set of keys and every run builds the same tree. + * + * A key keeps the first index it was inserted with. + */ +class PersistentKeyIndex { + public: + using Key = numeric::uint256_t; + + std::optional find(const Key& key) const + { + const Node* node = root_.get(); + while (node != nullptr) { + if (key == node->key) { + return node->index; + } + node = (key < node->key ? node->left : node->right).get(); + } + return std::nullopt; + } + + /** The entry with the largest key strictly below `key`. */ + std::optional> predecessor(const Key& key) const + { + std::optional> best; + const Node* node = root_.get(); + while (node != nullptr) { + if (node->key < key) { + best = std::make_pair(node->key, node->index); + node = node->right.get(); + } else { + node = node->left.get(); + } + } + return best; + } + + /** A copy of this index holding `key` at `index`, unless `key` is already present. */ + PersistentKeyIndex with(const Key& key, uint64_t index) const + { + if (find(key).has_value()) { + return *this; + } + PersistentKeyIndex out; + out.root_ = insert(root_, std::make_shared(Node{ key, index, priority(key), nullptr, nullptr })); + return out; + } + + private: + struct Node { + Key key; + uint64_t index; + uint64_t priority; + std::shared_ptr left; + std::shared_ptr right; + }; + using NodePtr = std::shared_ptr; + + static uint64_t priority(const Key& key) + { + // splitmix64 over the key's limbs. + uint64_t x = key.data[0] ^ (key.data[1] * 0x9e3779b97f4a7c15ULL) ^ (key.data[2] * 0xbf58476d1ce4e5b9ULL) ^ + (key.data[3] * 0x94d049bb133111ebULL); + x += 0x9e3779b97f4a7c15ULL; + x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL; + x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL; + return x ^ (x >> 31); + } + + static NodePtr with_children(const NodePtr& node, NodePtr left, NodePtr right) + { + return std::make_shared( + Node{ node->key, node->index, node->priority, std::move(left), std::move(right) }); + } + + /** Splits `node` into the keys below `key` and the keys above it (`key` itself is absent). */ + static std::pair split(const NodePtr& node, const Key& key) + { + if (!node) { + return { nullptr, nullptr }; + } + if (node->key < key) { + auto [below, above] = split(node->right, key); + return { with_children(node, node->left, std::move(below)), std::move(above) }; + } + auto [below, above] = split(node->left, key); + return { std::move(below), with_children(node, std::move(above), node->right) }; + } + + static NodePtr insert(const NodePtr& node, const NodePtr& fresh) + { + if (!node) { + return fresh; + } + if (fresh->priority > node->priority) { + auto [below, above] = split(node, fresh->key); + return with_children(fresh, std::move(below), std::move(above)); + } + if (fresh->key < node->key) { + return with_children(node, insert(node->left, fresh), node->right); + } + return with_children(node, node->left, insert(node->right, fresh)); + } + + NodePtr root_; +}; + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_merkle_tree.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_merkle_tree.hpp new file mode 100644 index 00000000000..a40c810df7d --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/persistent_merkle_tree.hpp @@ -0,0 +1,164 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "barretenberg/common/assert.hpp" +#include "barretenberg/crypto/merkle_tree/hash_path.hpp" +#include "barretenberg/ecc/curves/bn254/fr.hpp" + +namespace bb::world_state { + +/** + * @brief A full-height Merkle tree whose versions share structure. + * + * Every node is immutable. A write copies the path from the root to the leaves it touches and shares + * every other subtree with the version it was made from, so copying a tree is copying one pointer. + * That is what makes a fork, a checkpoint or a per-block snapshot free. An empty subtree is a null + * pointer and hashes to the zero-subtree root for its level. + * + * Each leaf may carry a payload (an indexed tree's leaf preimage). Node hashing is + * HashingPolicy::hash_pair; the caller computes each leaf's hash. + * + * Levels are numbered from the leaves: level 0 holds leaves, level `depth` holds the root. + */ +template class PersistentMerkleTree { + public: + using FF = bb::fr; + + struct Leaf { + FF hash; + std::shared_ptr payload; + }; + + explicit PersistentMerkleTree(size_t depth) + : zero_hashes_(make_zero_hashes(depth)) + {} + + size_t depth() const { return zero_hashes_->size() - 1; } + + FF root() const { return hash_of(root_, depth()); } + + /** The hash at (level, index); untouched nodes are zero-subtree roots. */ + FF node(size_t level, uint64_t index) const + { + const Node* current = root_.get(); + for (size_t at = depth(); at > level && current != nullptr; --at) { + current = current->children[bit(index, at - 1 - level)].get(); + } + return hash_of(current, level); + } + + FF leaf_hash(uint64_t index) const { return node(0, index); } + + /** The payload stored at `index`, or null when none was set. */ + const Payload* payload(uint64_t index) const + { + const Node* current = root_.get(); + for (size_t at = depth(); at > 0 && current != nullptr; --at) { + current = current->children[bit(index, at - 1)].get(); + } + return current != nullptr ? current->payload.get() : nullptr; + } + + crypto::merkle_tree::fr_sibling_path sibling_path(uint64_t index) const { return subtree_sibling_path(index, 0); } + + /** The siblings from `level` up to the root of the subtree holding `index` at `level`. */ + crypto::merkle_tree::fr_sibling_path subtree_sibling_path(uint64_t index, size_t level) const + { + crypto::merkle_tree::fr_sibling_path path(depth() - level); + std::vector siblings(depth() + 1, nullptr); + const Node* current = root_.get(); + for (size_t at = depth(); at > level; --at) { + size_t direction = bit(index, at - 1 - level); + siblings[at - 1] = current != nullptr ? current->children[direction ^ 1].get() : nullptr; + current = current != nullptr ? current->children[direction].get() : nullptr; + } + for (size_t at = level; at < depth(); ++at) { + path[at - level] = hash_of(siblings[at], at); + } + return path; + } + + /** A copy of this tree with the leaf at `index` set. */ + PersistentMerkleTree with_leaf(uint64_t index, Leaf leaf) const + { + PersistentMerkleTree out = *this; + out.root_ = set(root_, depth(), 0, index, std::span(&leaf, 1)); + return out; + } + + /** A copy of this tree with `leaves` written at consecutive indices from `start`, hashing each node once. */ + PersistentMerkleTree with_leaves(uint64_t start, std::span leaves) const + { + if (leaves.empty()) { + return *this; + } + PersistentMerkleTree out = *this; + out.root_ = set(root_, depth(), 0, start, leaves); + return out; + } + + private: + struct Node { + FF hash; + std::shared_ptr children[2]; + std::shared_ptr payload; + }; + using NodePtr = std::shared_ptr; + + static std::shared_ptr> make_zero_hashes(size_t depth) + { + BB_ASSERT_GTE(depth, static_cast(1)); + auto zero_hashes = std::make_shared>(depth + 1); + (*zero_hashes)[0] = FF::zero(); + for (size_t level = 1; level <= depth; ++level) { + (*zero_hashes)[level] = HashingPolicy::hash_pair((*zero_hashes)[level - 1], (*zero_hashes)[level - 1]); + } + return zero_hashes; + } + + static size_t bit(uint64_t index, size_t position) { return static_cast((index >> position) & 1ULL); } + + FF hash_of(const Node* node, size_t level) const { return node != nullptr ? node->hash : (*zero_hashes_)[level]; } + FF hash_of(const NodePtr& node, size_t level) const { return hash_of(node.get(), level); } + + /** + * Writes `leaves` (from leaf index `start`) into the subtree at `level` whose first leaf is `first`, + * returning the new subtree. A subtree left with nothing but zero leaves and no payloads is null. + */ + NodePtr set(const NodePtr& node, size_t level, uint64_t first, uint64_t start, std::span leaves) const + { + if (level == 0) { + const Leaf& leaf = leaves[static_cast(first - start)]; + if (leaf.hash.is_zero() && !leaf.payload) { + return nullptr; + } + return std::make_shared(Node{ .hash = leaf.hash, .children = {}, .payload = leaf.payload }); + } + uint64_t half = uint64_t(1) << (level - 1); + uint64_t end = start + leaves.size(); + NodePtr children[2] = { node ? node->children[0] : nullptr, node ? node->children[1] : nullptr }; + for (size_t side = 0; side < 2; ++side) { + uint64_t child_first = first + side * half; + uint64_t child_end = child_first + half; + if (child_first < end && start < child_end) { + children[side] = set(children[side], level - 1, child_first, start, leaves); + } + } + if (!children[0] && !children[1]) { + return nullptr; + } + FF hash = HashingPolicy::hash_pair(hash_of(children[0], level - 1), hash_of(children[1], level - 1)); + return std::make_shared( + Node{ .hash = hash, .children = { std::move(children[0]), std::move(children[1]) }, .payload = nullptr }); + } + + std::shared_ptr> zero_hashes_; + NodePtr root_; +}; + +} // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp index 53560b93937..4e2fd4e901d 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp @@ -1,7 +1,11 @@ #include "barretenberg/world_state_reference/reference_world_state.hpp" +#include +#include +#include +#include + #include "barretenberg/aztec/aztec_constants.hpp" -#include "barretenberg/common/assert.hpp" #include "barretenberg/crypto/merkle_tree/hash.hpp" #include "barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp" @@ -18,21 +22,10 @@ constexpr MerkleTreeId STATE_TREES[] = { MerkleTreeId::L1_TO_L2_MESSAGE_TREE, }; -TreeSnapshot snapshot_of(const TreeRoots& roots, MerkleTreeId tree_id) -{ - switch (tree_id) { - case MerkleTreeId::NULLIFIER_TREE: - return roots.nullifier_tree; - case MerkleTreeId::NOTE_HASH_TREE: - return roots.note_hash_tree; - case MerkleTreeId::PUBLIC_DATA_TREE: - return roots.public_data_tree; - case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: - return roots.l1_to_l2_message_tree; - default: - throw_or_abort("snapshot_of: not a state-reference tree"); - } -} +constexpr MerkleTreeId ALL_TREES[] = { + MerkleTreeId::NULLIFIER_TREE, MerkleTreeId::NOTE_HASH_TREE, MerkleTreeId::PUBLIC_DATA_TREE, + MerkleTreeId::L1_TO_L2_MESSAGE_TREE, MerkleTreeId::ARCHIVE, +}; bool is_append_only(MerkleTreeId tree_id) { @@ -40,6 +33,23 @@ bool is_append_only(MerkleTreeId tree_id) tree_id == MerkleTreeId::ARCHIVE; } +template [[noreturn]] void fail(const Parts&... parts) +{ + std::ostringstream message; + (message << ... << parts); + throw std::runtime_error(message.str()); +} + +/** An indexed-tree leaf as the tree stores it: an empty leaf hashes to zero and carries no preimage. */ +template typename Tree::Leaf leaf_of(const Payload& indexed_leaf) +{ + if (indexed_leaf.is_empty()) { + return { .hash = FF::zero(), .payload = nullptr }; + } + return { .hash = Tree::Hashing::hash(indexed_leaf.get_hash_inputs()), + .payload = std::make_shared(indexed_leaf) }; +} + } // namespace GenesisConfig GenesisConfig::canonical() @@ -52,50 +62,50 @@ GenesisConfig GenesisConfig::canonical() } ReferenceWorldState::ReferenceWorldState(const GenesisConfig& genesis) - : genesis_{ .trees = MemoryMerkleDB(INDEXED_TREE_INITIAL_SIZE, - INDEXED_TREE_INITIAL_SIZE, - genesis.prefilled_nullifiers, - genesis.prefilled_public_data), - .archive = ArchiveTree(ARCHIVE_HEIGHT) } - , current_(genesis_) { - genesis_block_header_hash_ = - compute_genesis_block_header_hash(genesis_.trees.get_tree_roots(), genesis.genesis_timestamp); - genesis_.archive.append_leaves(std::span(&genesis_block_header_hash_, 1)); - current_ = genesis_; + reset(genesis); } std::optional ReferenceWorldState::check_genesis(const GenesisConfig& genesis) { - if (auto problem = - MemoryMerkleDB::NullifierTree::check_prefill(INDEXED_TREE_INITIAL_SIZE, genesis.prefilled_nullifiers)) { + auto check = [](const auto& prefilled) -> std::optional { + if (prefilled.size() > INDEXED_TREE_INITIAL_SIZE) { + return "Number of prefilled values can't be more than initial size"; + } + size_t num_padding = INDEXED_TREE_INITIAL_SIZE - prefilled.size(); + using LeafType = std::remove_cvref_t; + uint256_t previous = num_padding > 0 ? uint256_t(LeafType::padding(num_padding - 1).get_key()) : uint256_t(0); + for (size_t i = 0; i < prefilled.size(); ++i) { + uint256_t key = uint256_t(prefilled[i].get_key()); + if ((i > 0 || num_padding > 0) && key <= previous) { + return i == 0 ? "Prefilled values must not be the same as the default values" + : "Prefilled values must be unique and sorted"; + } + previous = key; + } + return std::nullopt; + }; + if (auto problem = check(genesis.prefilled_nullifiers)) { return "prefilled nullifiers: " + *problem; } - if (auto problem = - MemoryMerkleDB::PublicDataTree::check_prefill(INDEXED_TREE_INITIAL_SIZE, genesis.prefilled_public_data)) { + if (auto problem = check(genesis.prefilled_public_data)) { return "prefilled public data: " + *problem; } return std::nullopt; } -std::optional ReferenceWorldState::check_revision(const WorldStateRevision& revision) +void ReferenceWorldState::reset(const GenesisConfig& genesis) { - if (auto problem = check_fork(revision.forkId)) { - return problem; + if (auto problem = check_genesis(genesis)) { + fail(*problem); } - if (revision.blockNumber != 0 && revision.blockNumber != WorldStateRevision::LATEST) { - return "the reference world state keeps no block history: read block 0 or the latest block (" + - std::to_string(WorldStateRevision::LATEST) + "), not " + std::to_string(revision.blockNumber); - } - return std::nullopt; -} - -std::optional ReferenceWorldState::check_fork(uint64_t fork_id) -{ - if (fork_id != 0) { - return "the reference world state has one fork (0), not " + std::to_string(fork_id); - } - return std::nullopt; + blocks_.clear(); + forks_.clear(); + blocks_.push_back(build_genesis(genesis)); + forks_.emplace(0, Fork{ .block_number = 0, .base = blocks_[0], .current = blocks_[0], .checkpoints = {} }); + next_fork_id_ = 1; + finalized_block_number_ = 0; + oldest_historical_block_ = 0; } uint32_t ReferenceWorldState::tree_height(MerkleTreeId tree_id) @@ -112,217 +122,838 @@ uint32_t ReferenceWorldState::tree_height(MerkleTreeId tree_id) case MerkleTreeId::ARCHIVE: return ARCHIVE_HEIGHT; } - throw_or_abort("tree_height: unknown tree id"); + fail("Unknown tree id ", static_cast(tree_id)); } -const ReferenceWorldState::State& ReferenceWorldState::view(const WorldStateRevision& revision) const +ReferenceWorldState::Trees ReferenceWorldState::build_genesis(const GenesisConfig& genesis) { - BB_ASSERT(!check_revision(revision).has_value()); - // Block 0 is the genesis. The latest block is the genesis too until the first commit, which the - // base contract has no command for; only uncommitted reads see the current state. - if (revision.blockNumber == 0 || !revision.includeUncommitted) { - return genesis_; + // The indexed trees start as on a node: LeafType::padding(i) for the leaves the prefill leaves + // free, then the prefill, linked as one ascending chain. + auto indexed = [](auto tree, const auto& prefilled) { + using Tree = decltype(tree); + using LeafType = std::remove_cvref_t; + std::vector> leaves; + size_t num_padding = INDEXED_TREE_INITIAL_SIZE - prefilled.size(); + for (size_t i = 0; i < num_padding; ++i) { + leaves.push_back(IndexedLeaf(LeafType::padding(i), 0, 0)); + } + for (const auto& value : prefilled) { + leaves.push_back(IndexedLeaf(value, 0, 0)); + } + std::vector hashed; + for (size_t i = 0; i < leaves.size(); ++i) { + size_t next = i + 1 == leaves.size() ? 0 : i + 1; + leaves[i].nextIndex = next; + leaves[i].nextKey = leaves[next].leaf.get_key(); + hashed.push_back(leaf_of(leaves[i])); + tree.indices = tree.indices.with(uint256_t(leaves[i].leaf.get_key()), i); + } + tree.tree = tree.tree.with_leaves(0, hashed); + tree.size = leaves.size(); + return tree; + }; + auto append_only = [](MerkleTreeId tree_id) { + return AppendOnlyTree{ .tree = decltype(AppendOnlyTree::tree)(tree_height(tree_id)), .indices = {}, .size = 0 }; + }; + + Trees trees{ + .nullifier = indexed(NullifierTree{ .tree = decltype(NullifierTree::tree)(NULLIFIER_TREE_HEIGHT) }, + genesis.prefilled_nullifiers), + .note_hash = append_only(MerkleTreeId::NOTE_HASH_TREE), + .public_data = indexed(PublicDataTree{ .tree = decltype(PublicDataTree::tree)(PUBLIC_DATA_TREE_HEIGHT) }, + genesis.prefilled_public_data), + .l1_to_l2 = append_only(MerkleTreeId::L1_TO_L2_MESSAGE_TREE), + .archive = append_only(MerkleTreeId::ARCHIVE), + }; + FF header_hash = compute_genesis_block_header_hash( + state_reference(trees), genesis.genesis_timestamp, genesis.header_generator_point); + append(trees.archive, std::span(&header_hash, 1)); + return trees; +} + +// --------------------------------------------------------------------------- +// Revisions +// --------------------------------------------------------------------------- + +const ReferenceWorldState::Fork& ReferenceWorldState::fork(uint64_t fork_id) const +{ + auto it = forks_.find(fork_id); + if (it == forks_.end()) { + fail("Fork not found"); } - return current_; + return it->second; } -std::vector ReferenceWorldState::get_state_reference(const WorldStateRevision& revision) const +ReferenceWorldState::Fork& ReferenceWorldState::fork(uint64_t fork_id) +{ + return const_cast(std::as_const(*this).fork(fork_id)); +} + +const ReferenceWorldState::Trees& ReferenceWorldState::committed_block(block_number_t block_number) const +{ + if (block_number > latest_block_number()) { + fail("Unable to get state for block ", block_number, ", latest block is ", latest_block_number()); + } + if (block_number != 0 && block_number < oldest_historical_block_) { + fail("Unable to get state for block ", block_number, ", oldest historical block is ", oldest_historical_block_); + } + return blocks_[block_number]; +} + +const ReferenceWorldState::Trees& ReferenceWorldState::view(const WorldStateRevision& revision) const +{ + const Fork& f = fork(revision.forkId); + if (revision.is_historical()) { + return committed_block(revision.blockNumber); + } + return revision.includeUncommitted ? f.current : f.base; +} + +TreeState ReferenceWorldState::tree_state(const Trees& trees, MerkleTreeId tree_id) +{ + switch (tree_id) { + case MerkleTreeId::NULLIFIER_TREE: + return { tree_id, trees.nullifier.tree.root(), trees.nullifier.size }; + case MerkleTreeId::NOTE_HASH_TREE: + return { tree_id, trees.note_hash.tree.root(), trees.note_hash.size }; + case MerkleTreeId::PUBLIC_DATA_TREE: + return { tree_id, trees.public_data.tree.root(), trees.public_data.size }; + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return { tree_id, trees.l1_to_l2.tree.root(), trees.l1_to_l2.size }; + case MerkleTreeId::ARCHIVE: + return { tree_id, trees.archive.tree.root(), trees.archive.size }; + } + fail("Unknown tree id ", static_cast(tree_id)); +} + +uint64_t ReferenceWorldState::tree_size(const Trees& trees, MerkleTreeId tree_id) +{ + return tree_state(trees, tree_id).size; +} + +std::vector ReferenceWorldState::state_reference(const Trees& trees) { - const auto roots = view(revision).trees.get_tree_roots(); std::vector state; for (auto tree_id : STATE_TREES) { - const auto snapshot = snapshot_of(roots, tree_id); - state.push_back({ tree_id, snapshot.root, snapshot.next_available_leaf_index }); + state.push_back(tree_state(trees, tree_id)); } return state; } +bool ReferenceWorldState::state_matches(const Trees& trees, std::span state) +{ + // The node compares maps keyed by tree id: the same four trees, each with the same root and size. + const auto current = state_reference(trees); + if (state.size() != current.size()) { + return false; + } + return std::all_of(current.begin(), current.end(), [&](const TreeState& expected) { + return std::any_of(state.begin(), state.end(), [&](const TreeState& given) { return given == expected; }); + }); +} + +// --------------------------------------------------------------------------- +// Reads +// --------------------------------------------------------------------------- + +std::vector ReferenceWorldState::get_state_reference(const WorldStateRevision& revision) const +{ + return state_reference(view(revision)); +} + std::vector ReferenceWorldState::get_initial_state_reference() const { - return get_state_reference(WorldStateRevision{ .blockNumber = 0 }); + return state_reference(blocks_[0]); } TreeState ReferenceWorldState::get_tree_info(const WorldStateRevision& revision, MerkleTreeId tree_id) const { - const State& state = view(revision); - const TreeSnapshot snapshot = tree_id == MerkleTreeId::ARCHIVE ? state.archive.get_snapshot() - : snapshot_of(state.trees.get_tree_roots(), tree_id); - return { tree_id, snapshot.root, snapshot.next_available_leaf_index }; + return tree_state(view(revision), tree_id); } std::optional ReferenceWorldState::get_leaf_value(const WorldStateRevision& revision, MerkleTreeId tree_id, uint64_t index) const { - BB_ASSERT(is_append_only(tree_id)); - if (index >= get_tree_info(revision, tree_id).size) { + if (!is_append_only(tree_id)) { + fail("Unsupported tree type for get_leaf_value"); + } + // As on a node: any leaf inside the tree's capacity reads (zero if unwritten), except that a historical + // read past that block's size, or of a block it does not have, reads as no leaf. + const Fork& f = fork(revision.forkId); + const Trees* trees = &(revision.includeUncommitted ? f.current : f.base); + if (revision.is_historical()) { + if (revision.blockNumber > latest_block_number() || + (revision.blockNumber != 0 && revision.blockNumber < oldest_historical_block_)) { + return std::nullopt; + } + trees = &blocks_[revision.blockNumber]; + } + const AppendOnlyTree& tree = tree_id == MerkleTreeId::NOTE_HASH_TREE ? trees->note_hash + : tree_id == MerkleTreeId::L1_TO_L2_MESSAGE_TREE ? trees->l1_to_l2 + : trees->archive; + if (index >= (uint64_t(1) << tree.tree.depth()) || (revision.is_historical() && index >= tree.size)) { return std::nullopt; } - const State& state = view(revision); - return tree_id == MerkleTreeId::ARCHIVE ? state.archive.get_leaf_value(index) - : state.trees.get_leaf_value(tree_id, index); + return tree.tree.leaf_hash(index); } std::optional> ReferenceWorldState::get_nullifier_preimage( const WorldStateRevision& revision, uint64_t index) const { - if (index >= get_tree_info(revision, MerkleTreeId::NULLIFIER_TREE).size) { + // As on a node: a slot a batch left empty reads as the empty leaf; past the tree's size there is none. + const auto& tree = view(revision).nullifier; + if (index >= tree.size) { return std::nullopt; } - return view(revision).trees.get_leaf_preimage_nullifier_tree(index); + const auto* leaf = tree.tree.payload(index); + return leaf != nullptr ? *leaf : IndexedLeaf::empty(); } std::optional> ReferenceWorldState::get_public_data_preimage( const WorldStateRevision& revision, uint64_t index) const { - if (index >= get_tree_info(revision, MerkleTreeId::PUBLIC_DATA_TREE).size) { + // As on a node: a slot a batch left empty reads as the empty leaf; past the tree's size there is none. + const auto& tree = view(revision).public_data; + if (index >= tree.size) { return std::nullopt; } - return view(revision).trees.get_leaf_preimage_public_data_tree(index); + const auto* leaf = tree.tree.payload(index); + return leaf != nullptr ? *leaf : IndexedLeaf::empty(); } SiblingPath ReferenceWorldState::get_sibling_path(const WorldStateRevision& revision, MerkleTreeId tree_id, uint64_t index) const { - const State& state = view(revision); - return tree_id == MerkleTreeId::ARCHIVE ? state.archive.get_sibling_path(index) - : state.trees.get_sibling_path(tree_id, index); + const Trees& trees = view(revision); + switch (tree_id) { + case MerkleTreeId::NULLIFIER_TREE: + return trees.nullifier.tree.sibling_path(index); + case MerkleTreeId::NOTE_HASH_TREE: + return trees.note_hash.tree.sibling_path(index); + case MerkleTreeId::PUBLIC_DATA_TREE: + return trees.public_data.tree.sibling_path(index); + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return trees.l1_to_l2.tree.sibling_path(index); + case MerkleTreeId::ARCHIVE: + return trees.archive.tree.sibling_path(index); + } + fail("Unknown tree id ", static_cast(tree_id)); } -std::optional ReferenceWorldState::find_leaf_index(const WorldStateRevision& revision, - MerkleTreeId tree_id, - const FF& leaf, - uint64_t start) const +std::vector> ReferenceWorldState::get_block_numbers_for_leaf_indices( + const WorldStateRevision& revision, MerkleTreeId tree_id, std::span indices) const { - const uint64_t size = get_tree_info(revision, tree_id).size; - for (uint64_t index = start; index < size; ++index) { - if (get_leaf_value(revision, tree_id, index) == leaf) { - return index; + // Only committed blocks number leaves: up to the revision's block, or the block the fork started from. + const Fork& f = fork(revision.forkId); + block_number_t last = revision.is_historical() ? revision.blockNumber : f.block_number; + committed_block(last); + std::vector> out; + out.reserve(indices.size()); + for (uint64_t index : indices) { + if (index >= tree_size(blocks_[last], tree_id)) { + out.emplace_back(std::nullopt); + continue; + } + // The first block whose tree holds the index. + block_number_t low = 0; + block_number_t high = last; + while (low < high) { + block_number_t mid = low + (high - low) / 2; + if (tree_size(blocks_[mid], tree_id) > index) { + high = mid; + } else { + low = mid + 1; + } } + out.emplace_back(low); } - return std::nullopt; + return out; +} + +const PersistentKeyIndex& ReferenceWorldState::key_index(const Trees& trees, MerkleTreeId tree_id) +{ + switch (tree_id) { + case MerkleTreeId::NULLIFIER_TREE: + return trees.nullifier.indices; + case MerkleTreeId::PUBLIC_DATA_TREE: + return trees.public_data.indices; + case MerkleTreeId::NOTE_HASH_TREE: + return trees.note_hash.indices; + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return trees.l1_to_l2.indices; + case MerkleTreeId::ARCHIVE: + return trees.archive.indices; + } + fail("Unknown tree id ", static_cast(tree_id)); } -std::optional ReferenceWorldState::find_indexed_leaf_index(const WorldStateRevision& revision, - MerkleTreeId tree_id, - const FF& key, - uint64_t start) const +std::optional ReferenceWorldState::find_leaf_index(const WorldStateRevision& revision, + MerkleTreeId tree_id, + const FF& key, + uint64_t start) const { - const uint64_t size = get_tree_info(revision, tree_id).size; - for (uint64_t index = start; index < size; ++index) { - const FF leaf_key = tree_id == MerkleTreeId::NULLIFIER_TREE - ? get_nullifier_preimage(revision, index)->leaf.get_key() - : get_public_data_preimage(revision, index)->leaf.get_key(); - if (leaf_key == key) { - return index; + const uint256_t k = uint256_t(key); + const Trees& trees = view(revision); + std::optional index; + if (revision.includeUncommitted && revision.is_historical()) { + // As on a node: a key the fork wrote since its base is found first, whatever block the read names. + const Fork& f = fork(revision.forkId); + auto written = key_index(f.current, tree_id).find(k); + if (written.has_value() && !key_index(f.base, tree_id).find(k).has_value()) { + index = written; } } - return std::nullopt; + if (!index.has_value()) { + index = key_index(trees, tree_id).find(k); + } + if (!index.has_value() || *index < start) { + return std::nullopt; + } + return index; } GetLowIndexedLeafResponse ReferenceWorldState::find_low_leaf(const WorldStateRevision& revision, MerkleTreeId tree_id, const FF& key) const { - return view(revision).trees.get_low_indexed_leaf(tree_id, key); + if (tree_id != MerkleTreeId::NULLIFIER_TREE && tree_id != MerkleTreeId::PUBLIC_DATA_TREE) { + fail("Unsupported tree type for find_low_leaf"); + } + if (auto index = find_leaf_index(revision, tree_id, key, 0)) { + return GetLowIndexedLeafResponse(true, *index); + } + const uint256_t k = uint256_t(key); + auto low = key_index(view(revision), tree_id).predecessor(k); + if (revision.includeUncommitted && revision.is_historical()) { + // As on a node: keys the fork wrote since its base count too, whatever block the read names. + const Fork& f = fork(revision.forkId); + const auto& current = key_index(f.current, tree_id); + const auto& base = key_index(f.base, tree_id); + for (auto written = current.predecessor(k); written.has_value() && (!low || written->first > low->first); + written = current.predecessor(written->first)) { + if (!base.find(written->first).has_value()) { + low = written; + break; + } + } + } + return GetLowIndexedLeafResponse(false, low.has_value() ? low->second : 0); +} + +// --------------------------------------------------------------------------- +// Tree writes +// --------------------------------------------------------------------------- + +void ReferenceWorldState::append(AppendOnlyTree& tree, std::span leaves) +{ + uint64_t max = uint64_t(1) << tree.tree.depth(); + if (tree.size + leaves.size() > max) { + fail("Unable to append leaves to tree, new size: ", tree.size + leaves.size(), " max size: ", max); + } + std::vector hashed; + hashed.reserve(leaves.size()); + for (size_t i = 0; i < leaves.size(); ++i) { + hashed.push_back({ .hash = leaves[i], .payload = nullptr }); + // A node does not index zero leaves. + if (!leaves[i].is_zero()) { + tree.indices = tree.indices.with(uint256_t(leaves[i]), tree.size + i); + } + } + tree.tree = tree.tree.with_leaves(tree.size, hashed); + tree.size += leaves.size(); } -void ReferenceWorldState::append_leaves(MerkleTreeId tree_id, std::span leaves) +template +BatchInsertionResult ReferenceWorldState::batch_insert(Tree& target, + std::span leaves, + uint32_t subtree_depth, + bool capture_witness) { - BB_ASSERT(is_append_only(tree_id)); - if (tree_id == MerkleTreeId::ARCHIVE) { - current_.archive.append_leaves(leaves); - } else { - current_.trees.append_leaves(tree_id, leaves); + // Mirrors ContentAddressedIndexedTree::add_or_update_values: sort the batch by descending key, update each + // value's low leaf in that order, then append one slot per value at the batch's own position, empty for a + // zero value or a value that updated its low leaf in place. Works on a copy, so a failure changes nothing. + using Leaf = IndexedLeaf; + Tree tree = target; + const uint32_t depth = static_cast(tree.tree.depth()); + const uint64_t start = tree.size; + if (start + leaves.size() > (uint64_t(1) << depth)) { + fail("Unable to insert values into tree ", + LeafValueType::name(), + " new size: ", + start + leaves.size(), + " max size: ", + uint64_t(1) << depth); } + + BatchInsertionResult result; + result.sorted_leaves.reserve(leaves.size()); + for (size_t i = 0; i < leaves.size(); ++i) { + result.sorted_leaves.emplace_back(leaves[i], i); + } + std::sort(result.sorted_leaves.begin(), result.sorted_leaves.end(), [](const auto& a, const auto& b) { + uint256_t a_key = uint256_t(a.first.get_key()); + uint256_t b_key = uint256_t(b.first.get_key()); + return a_key == b_key ? a.second < b.second : a_key > b_key; + }); + + std::vector appended(leaves.size(), Leaf::empty()); + std::vector> witnesses; + std::set unique; + for (const auto& [value, position] : result.sorted_leaves) { + if (value.is_empty()) { + continue; + } + uint256_t key = uint256_t(value.get_key()); + if (!unique.insert(key).second) { + fail("Duplicate key not allowed in same batch, key value: ", value.get_key(), ", tree: ", Leaf::name()); + } + uint64_t low_index = 0; + bool present = false; + if (auto exact = tree.indices.find(key)) { + present = true; + low_index = *exact; + } else if (auto low = tree.indices.predecessor(key)) { + low_index = low->second; + } + const Leaf* stored = tree.tree.payload(low_index); + if (stored == nullptr) { + fail("Unable to insert values into tree ", Leaf::name(), ", failed to find low leaf at index ", low_index); + } + Leaf low_leaf = *stored; + if (capture_witness) { + witnesses.emplace_back(low_leaf, low_index, tree.tree.sibling_path(low_index)); + } + Leaf updated = low_leaf; + if (!present) { + uint64_t new_index = start + position; + appended[static_cast(position)] = Leaf(value, low_leaf.nextIndex, low_leaf.nextKey); + updated.nextIndex = new_index; + updated.nextKey = value.get_key(); + tree.indices = tree.indices.with(key, new_index); + } else if (Leaf::is_updateable()) { + updated = Leaf(value, low_leaf.nextIndex, low_leaf.nextKey); + } else { + fail("Unable to insert values into tree ", + Leaf::name(), + " leaf type ", + Leaf::name(), + " is not updateable and ", + value.get_key(), + " is already present"); + } + tree.tree = tree.tree.with_leaf(low_index, leaf_of(updated)); + } + + if (capture_witness) { + // One witness slot per value: the low-leaf updates in the order they were applied, then empty ones. + witnesses.resize(leaves.size(), + LeafUpdateWitnessData(Leaf::empty(), 0, SiblingPath(depth, FF::zero()))); + result.low_leaf_witness_data = std::move(witnesses); + result.subtree_path = subtree_depth >= depth + ? SiblingPath{} + : tree.tree.subtree_sibling_path(start >> subtree_depth, subtree_depth); + } + + std::vector hashed; + hashed.reserve(appended.size()); + for (const auto& leaf : appended) { + hashed.push_back(leaf_of(leaf)); + } + tree.tree = tree.tree.with_leaves(start, hashed); + tree.size = start + leaves.size(); + target = std::move(tree); + if (!capture_witness) { + result.sorted_leaves.clear(); + } + return result; } -SequentialInsertionResult ReferenceWorldState::insert_nullifier(const NullifierLeafValue& leaf) +template +SequentialInsertionResult ReferenceWorldState::sequential_insert(Tree& target, + std::span leaves) { - return current_.trees.insert_indexed_leaves_nullifier_tree(leaf); + // Mirrors ContentAddressedIndexedTree::add_or_update_values_sequentially: each value in order updates its low + // leaf, then (if new) is written at the next index. Zero values are skipped. Works on a copy. + using Leaf = IndexedLeaf; + Tree tree = target; + const uint32_t depth = static_cast(tree.tree.depth()); + SequentialInsertionResult result; + for (const auto& value : leaves) { + if (value.is_empty()) { + continue; + } + uint256_t key = uint256_t(value.get_key()); + uint64_t low_index = 0; + bool present = false; + if (auto exact = tree.indices.find(key)) { + present = true; + low_index = *exact; + } else if (auto low = tree.indices.predecessor(key)) { + low_index = low->second; + } + const Leaf* stored = tree.tree.payload(low_index); + if (stored == nullptr) { + fail("Unable to insert values into tree ", Leaf::name(), ", failed to find low leaf at index ", low_index); + } + Leaf low_leaf = *stored; + result.low_leaf_witness_data.emplace_back(low_leaf, low_index, tree.tree.sibling_path(low_index)); + if (!present) { + uint64_t new_index = tree.size; + Leaf fresh(value, low_leaf.nextIndex, low_leaf.nextKey); + Leaf updated = low_leaf; + updated.nextIndex = new_index; + updated.nextKey = value.get_key(); + tree.tree = tree.tree.with_leaf(low_index, leaf_of(updated)); + result.insertion_witness_data.emplace_back(Leaf::empty(), new_index, tree.tree.sibling_path(new_index)); + tree.tree = tree.tree.with_leaf(new_index, leaf_of(fresh)); + tree.indices = tree.indices.with(key, new_index); + tree.size++; + } else if (Leaf::is_updateable()) { + tree.tree = + tree.tree.with_leaf(low_index, leaf_of(Leaf(value, low_leaf.nextIndex, low_leaf.nextKey))); + result.insertion_witness_data.emplace_back(Leaf::empty(), 0, SiblingPath(depth, FF::zero())); + } else { + fail("Unable to insert values into tree ", + Leaf::name(), + " leaf type ", + Leaf::name(), + " is not updateable and ", + value.get_key(), + " is already present"); + } + } + if (tree.size > (uint64_t(1) << depth)) { + fail("Unable to insert values into tree ", Leaf::name(), " new size: ", tree.size); + } + target = std::move(tree); + return result; } -SequentialInsertionResult ReferenceWorldState::insert_public_data(const PublicDataLeafValue& leaf) +void ReferenceWorldState::append_leaves(uint64_t fork_id, MerkleTreeId tree_id, std::span leaves) { - return current_.trees.insert_indexed_leaves_public_data_tree(leaf); + Trees& trees = fork(fork_id).current; + switch (tree_id) { + case MerkleTreeId::NOTE_HASH_TREE: + return append(trees.note_hash, leaves); + case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: + return append(trees.l1_to_l2, leaves); + case MerkleTreeId::ARCHIVE: + return append(trees.archive, leaves); + default: + fail("Unsupported tree type for append_leaves"); + } } -bool ReferenceWorldState::has_nullifier(const FF& nullifier) const +BatchInsertionResult ReferenceWorldState::batch_insert_nullifiers( + uint64_t fork_id, std::span leaves, uint32_t subtree_depth) { - return current_.trees.get_low_indexed_leaf(MerkleTreeId::NULLIFIER_TREE, nullifier).is_already_present; + return batch_insert(fork(fork_id).current.nullifier, leaves, subtree_depth, true); } -bool ReferenceWorldState::state_matches(std::span state) const +BatchInsertionResult ReferenceWorldState::batch_insert_public_data( + uint64_t fork_id, std::span leaves, uint32_t subtree_depth) { - const auto current = get_state_reference(WorldStateRevision{ .includeUncommitted = true }); - if (state.size() != current.size()) { - return false; + return batch_insert(fork(fork_id).current.public_data, leaves, subtree_depth, true); +} + +SequentialInsertionResult ReferenceWorldState::sequential_insert_nullifiers( + uint64_t fork_id, std::span leaves) +{ + return sequential_insert(fork(fork_id).current.nullifier, leaves); +} + +SequentialInsertionResult ReferenceWorldState::sequential_insert_public_data( + uint64_t fork_id, std::span leaves) +{ + return sequential_insert(fork(fork_id).current.public_data, leaves); +} + +void ReferenceWorldState::update_archive(uint64_t fork_id, + std::span block_state_ref, + const FF& block_header_hash) +{ + Trees& trees = fork(fork_id).current; + if (!state_matches(trees, block_state_ref)) { + fail("Can't update archive tree: Block state does not match world state"); } - for (const auto& expected : current) { - bool found = false; - for (const auto& given : state) { - found = found || given == expected; - } - if (!found) { - return false; + append(trees.archive, std::span(&block_header_hash, 1)); +} + +// --------------------------------------------------------------------------- +// Checkpoints +// --------------------------------------------------------------------------- + +void ReferenceWorldState::create_checkpoint(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + f.checkpoints.push_back(f.current); +} + +void ReferenceWorldState::commit_checkpoint(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + if (f.checkpoints.empty()) { + fail("Cannot commit without a checkpoint"); + } + f.checkpoints.pop_back(); +} + +void ReferenceWorldState::revert_checkpoint(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + if (f.checkpoints.empty()) { + fail("Cannot revert without a checkpoint"); + } + f.current = std::move(f.checkpoints.back()); + f.checkpoints.pop_back(); +} + +void ReferenceWorldState::commit_all_checkpoints(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + if (f.checkpoints.empty()) { + fail("Invalid depth for commit_to_depth"); + } + f.checkpoints.clear(); +} + +void ReferenceWorldState::revert_all_checkpoints(uint64_t fork_id) +{ + Fork& f = fork(fork_id); + if (f.checkpoints.empty()) { + fail("Invalid depth for revert_to_depth"); + } + f.current = std::move(f.checkpoints.front()); + f.checkpoints.clear(); +} + +// --------------------------------------------------------------------------- +// Forks +// --------------------------------------------------------------------------- + +uint64_t ReferenceWorldState::create_fork(std::optional block_number) +{ + block_number_t block = block_number.value_or(latest_block_number()); + const Trees& trees = committed_block(block); + uint64_t fork_id = next_fork_id_++; + forks_.emplace(fork_id, Fork{ .block_number = block, .base = trees, .current = trees, .checkpoints = {} }); + return fork_id; +} + +void ReferenceWorldState::delete_fork(uint64_t fork_id) +{ + if (fork_id == 0) { + fail("Unable to delete canonical fork"); + } + fork(fork_id); + forks_.erase(fork_id); +} + +// --------------------------------------------------------------------------- +// The canonical chain +// --------------------------------------------------------------------------- + +void ReferenceWorldState::commit() +{ + Fork& canonical = fork(0); + blocks_.push_back(canonical.current); + // As on a node, the oldest historical block is 1 from the first commit on, unwinding included; block 0 + // stays readable regardless. + oldest_historical_block_ = std::max(oldest_historical_block_, 1); + canonical.block_number = latest_block_number(); + canonical.base = canonical.current; + canonical.checkpoints.clear(); +} + +void ReferenceWorldState::rollback() +{ + Fork& canonical = fork(0); + canonical.current = canonical.base; + canonical.checkpoints.clear(); +} + +void ReferenceWorldState::sync_block(std::span block_state_ref, + const FF& block_header_hash, + std::span padded_note_hashes, + std::span padded_l1_to_l2_messages, + std::span padded_nullifiers, + std::span public_data_writes, + const std::optional& expected_archive_root, + const std::optional& expected_previous_archive_root) +{ + rollback(); + Fork& canonical = fork(0); + if (expected_previous_archive_root.has_value()) { + FF actual = canonical.base.archive.tree.root(); + if (actual != *expected_previous_archive_root) { + fail("Can't sync block: local archive root ", + actual, + " does not match the block's previous archive root ", + *expected_previous_archive_root, + "; world state has diverged from the canonical chain and must be resynced"); } } - return true; + + Trees trees = canonical.base; + try { + batch_insert(trees.nullifier, padded_nullifiers, 0, false); + append(trees.note_hash, padded_note_hashes); + append(trees.l1_to_l2, padded_l1_to_l2_messages); + append(trees.archive, std::span(&block_header_hash, 1)); + sequential_insert(trees.public_data, public_data_writes); + } catch (const std::exception& e) { + fail("Failed to sync block: ", e.what()); + } + + auto tip = trees.archive.indices.find(uint256_t(block_header_hash)); + if (!tip.has_value() || *tip + 1 != trees.archive.size) { + fail("Can't synch block: block header hash is not the tip of the archive tree"); + } + if (!state_matches(trees, block_state_ref)) { + fail("Can't synch block: block state does not match world state"); + } + if (expected_archive_root.has_value() && trees.archive.tree.root() != *expected_archive_root) { + fail("Can't sync block: resulting archive root ", + trees.archive.tree.root(), + " does not match the block's archive root ", + *expected_archive_root, + "; world state has diverged from the canonical chain and must be resynced"); + } + canonical.current = std::move(trees); + commit(); } -void ReferenceWorldState::update_archive(const FF& block_header_hash) +void ReferenceWorldState::finalize_blocks(block_number_t to_block_number) { - current_.archive.append_leaves(std::span(&block_header_hash, 1)); + if (to_block_number == 0) { + fail("Unable to finalize block 0"); + } + if (to_block_number <= finalized_block_number_) { + return; + } + if (to_block_number > latest_block_number()) { + fail("Unable to finalize block ", + to_block_number, + " currently unfinalized block height ", + latest_block_number()); + } + finalized_block_number_ = to_block_number; } -void ReferenceWorldState::create_checkpoint() +void ReferenceWorldState::drop_forks_of(block_number_t block_number) { - checkpoints_.push(current_); + for (auto it = forks_.begin(); it != forks_.end();) { + it = it->first != 0 && it->second.block_number == block_number ? forks_.erase(it) : std::next(it); + } } -void ReferenceWorldState::commit_checkpoint() +void ReferenceWorldState::unwind_blocks(block_number_t to_block_number) { - BB_ASSERT(has_checkpoint()); - checkpoints_.pop(); + // As on a node: one block at a time from the tip, so a request that reaches a finalized block fails there, + // keeping the blocks it already unwound. + rollback(); + block_number_t latest = latest_block_number(); + if (to_block_number >= latest) { + fail("Unable to unwind blocks to block number ", to_block_number, ", current pending block ", latest); + } + Fork& canonical = fork(0); + for (block_number_t block = latest; block > to_block_number; --block) { + if (block <= finalized_block_number_) { + fail("Unable to unwind block: ", block, " finalizedBlockHeight: ", finalized_block_number_); + } + blocks_.pop_back(); + drop_forks_of(block); + canonical.block_number = latest_block_number(); + canonical.base = blocks_.back(); + canonical.current = blocks_.back(); + } } -void ReferenceWorldState::revert_checkpoint() +void ReferenceWorldState::remove_historical_blocks(block_number_t to_block_number) { - BB_ASSERT(has_checkpoint()); - current_ = checkpoints_.top(); - checkpoints_.pop(); + // As on a node: one block at a time from the oldest, never a block that is not yet finalized. The genesis + // block is never removed. States stay in memory (they share almost all of their nodes with their + // successors); only reads of removed blocks stop. + block_number_t oldest = status().oldest_historical_block; + if (to_block_number <= oldest) { + fail("Unable to remove historical blocks to block number ", + to_block_number, + ", blocks not found. Current oldest block: ", + oldest); + } + for (block_number_t block = oldest; block < to_block_number; ++block) { + if (block < 1) { + fail("Unable to remove historical block: ", block); + } + if (block >= finalized_block_number_) { + fail("Unable to remove historical block: ", block, " finalizedBlockHeight: ", finalized_block_number_); + } + oldest_historical_block_ = block + 1; + drop_forks_of(block); + } } -void ReferenceWorldState::commit_all_checkpoints() +StatusSummary ReferenceWorldState::status() const { - checkpoints_ = {}; + return StatusSummary{ .unfinalized_block_number = latest_block_number(), + .finalized_block_number = finalized_block_number_, + .oldest_historical_block = oldest_historical_block_, + .trees_are_synched = true }; } -void ReferenceWorldState::revert_all_checkpoints() +std::array ReferenceWorldState::tree_meta() const { - while (has_checkpoint()) { - revert_checkpoint(); + const Fork& canonical = fork(0); + const StatusSummary summary = status(); + std::array meta{}; + for (auto tree_id : ALL_TREES) { + const TreeState current = tree_state(canonical.current, tree_id); + const TreeState initial = tree_state(blocks_[0], tree_id); + meta[static_cast(tree_id)] = TreeMetaInfo{ + .tree_id = tree_id, + .depth = tree_height(tree_id), + .root = current.root, + .size = current.size, + .committed_size = tree_size(canonical.base, tree_id), + .initial_root = initial.root, + .initial_size = initial.size, + .oldest_historic_block = summary.oldest_historical_block, + .unfinalized_block_height = summary.unfinalized_block_number, + .finalized_block_height = summary.finalized_block_number, + }; } + return meta; } -FF ReferenceWorldState::compute_genesis_block_header_hash(const TreeRoots& trees, uint64_t genesis_timestamp) +FF ReferenceWorldState::compute_genesis_block_header_hash(std::span state, + uint64_t genesis_timestamp, + const FF& header_generator_point) { + auto tree = [&](MerkleTreeId tree_id) { + for (const auto& entry : state) { + if (entry.tree_id == tree_id) { + return entry; + } + } + fail("compute_genesis_block_header_hash: missing tree ", static_cast(tree_id)); + }; // BlockHeader::serialize (block_header.nr) with every field zero except the state: the last // archive is empty, and the sponge blob hash, tx effects root, global variables (other than the // timestamp), total fees and mana used are all zero at genesis. return crypto::merkle_tree::Poseidon2HashPolicy::hash({ - FF(DOM_SEP__BLOCK_HEADER_HASH), + header_generator_point, FF(0), // last archive root FF(0), // last archive next_available_leaf_index - trees.l1_to_l2_message_tree.root, - FF(trees.l1_to_l2_message_tree.next_available_leaf_index), - trees.note_hash_tree.root, - FF(trees.note_hash_tree.next_available_leaf_index), - trees.nullifier_tree.root, - FF(trees.nullifier_tree.next_available_leaf_index), - trees.public_data_tree.root, - FF(trees.public_data_tree.next_available_leaf_index), + tree(MerkleTreeId::L1_TO_L2_MESSAGE_TREE).root, + FF(tree(MerkleTreeId::L1_TO_L2_MESSAGE_TREE).size), + tree(MerkleTreeId::NOTE_HASH_TREE).root, + FF(tree(MerkleTreeId::NOTE_HASH_TREE).size), + tree(MerkleTreeId::NULLIFIER_TREE).root, + FF(tree(MerkleTreeId::NULLIFIER_TREE).size), + tree(MerkleTreeId::PUBLIC_DATA_TREE).root, + FF(tree(MerkleTreeId::PUBLIC_DATA_TREE).size), FF(0), // sponge_blob_hash FF(0), // tx_effects_tree_root FF(0), // chain_id diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp index 957cc7a921d..ecc9ea31058 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp @@ -1,27 +1,46 @@ #pragma once +#include #include #include +#include #include #include -#include #include +#include #include -#include "barretenberg/world_state_reference/memory_merkle_db.hpp" +#include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/aztec/aztec_hash_policy.hpp" +#include "barretenberg/crypto/merkle_tree/indexed_leaf.hpp" +#include "barretenberg/crypto/merkle_tree/response.hpp" #include "barretenberg/world_state_reference/merkle_tree_id.hpp" +#include "barretenberg/world_state_reference/persistent_key_index.hpp" +#include "barretenberg/world_state_reference/persistent_merkle_tree.hpp" namespace bb::world_state { +using FF = bb::fr; +using crypto::merkle_tree::GetLowIndexedLeafResponse; +using crypto::merkle_tree::IndexedLeaf; +using crypto::merkle_tree::LeafUpdateWitnessData; +using crypto::merkle_tree::NullifierLeafValue; +using crypto::merkle_tree::PublicDataLeafValue; +using crypto::merkle_tree::SequentialInsertionResult; +using SiblingPath = crypto::merkle_tree::fr_sibling_path; +using numeric::uint256_t; + /** - * @brief The genesis a world state starts from: indexed-tree prefill and the genesis block's timestamp. + * @brief The genesis a world state starts from, as a node world state is configured with it. * @details canonical() is the protocol's: the protocol contracts' registration nullifiers, no public - * data, timestamp 0. Anything else is for tests of the reference itself. + * data, timestamp 0. */ struct GenesisConfig { std::vector prefilled_nullifiers; std::vector prefilled_public_data; uint64_t genesis_timestamp = 0; + /** The first field of the genesis block header's hash preimage (its domain separator). */ + FF header_generator_point = FF(DOM_SEP__BLOCK_HEADER_HASH); static GenesisConfig canonical(); }; @@ -34,28 +53,54 @@ struct TreeState { bool operator==(const TreeState& other) const = default; }; +/** @brief What a node world state reports about one tree of the canonical chain. */ +struct TreeMetaInfo { + MerkleTreeId tree_id; + uint32_t depth; + FF root; + uint64_t size; + uint64_t committed_size; + FF initial_root; + uint64_t initial_size; + block_number_t oldest_historic_block; + block_number_t unfinalized_block_height; + block_number_t finalized_block_height; +}; + +struct StatusSummary { + block_number_t unfinalized_block_number; + block_number_t finalized_block_number; + block_number_t oldest_historical_block; + bool trees_are_synched; +}; + +template struct BatchInsertionResult { + std::vector> low_leaf_witness_data; + /** The batch sorted by descending key, each with its position in the batch. */ + std::vector> sorted_leaves; + SiblingPath subtree_path; +}; + /** - * @brief The in-memory reference world state behind the wsdb base contract (wsdb_base_schema.jsonc). - * - * All five trees at protocol height: the four MemoryMerkleDB trees plus the archive. It is one fork - * (fork 0) with no block history, which is all the protocol's own flows need: build a genesis, read - * witnesses, append and insert leaves, and advance the archive. + * @brief The in-memory reference world state: the node world state's contract (wsdb_schema.jsonc) + * over trees whose versions share structure. * - * The genesis snapshot never changes (the base contract has no Commit), so reads resolve against one - * of two states: the genesis, for block 0 or for committed reads of the latest block, and the current - * state, for uncommitted reads of the latest block. That is what a node world state answers on fork 0 - * before its first commit. Checkpoints copy the current state onto a stack. + * Holds the canonical chain (one state per committed block, from genesis), its uncommitted state, and + * any number of forks. A fork starts from the state at a committed block and is written to on its own, + * with a stack of checkpoints. Every one of these is a copy of the persistent trees, which costs one + * pointer per tree, so keeping every block, forking and checkpointing are all free. * - * Genesis matches the node world state: the indexed trees hold their prefill, and the archive holds - * one leaf, the hash of the genesis block header built from the four trees' genesis roots. + * Reads take a revision, resolved as a node world state resolves it: + * - a block number other than WorldStateRevision::LATEST reads the committed state at that block; + * - otherwise includeUncommitted reads the fork's current state, and a committed read sees the state + * the fork started from (for the canonical fork, the latest committed block). * - * Methods take already validated input; the check_* helpers return why a request cannot be served, - * so a caller can answer with an error instead of aborting (wasm builds have no exceptions). + * Roots, sibling paths, insertion witnesses and indices follow the node world state (the + * content-addressed trees in native-packages/wsdb) for the same requests. Every failure is a + * std::runtime_error whose message a caller can return as the request's error. */ class ReferenceWorldState { public: - using ArchiveTree = MemoryAppendOnlyTree; - /** Leaves each indexed tree starts with, prefill included, as on a node. */ static constexpr size_t INDEXED_TREE_INITIAL_SIZE = 128; @@ -64,83 +109,174 @@ class ReferenceWorldState { /** Why `genesis` cannot seed a world state, or nullopt when it can. */ static std::optional check_genesis(const GenesisConfig& genesis); - /** - * @brief Why a read at this revision cannot be served, or nullopt when it can. - * @details Fork 0 only; the block must be 0 (the genesis) or WorldStateRevision::LATEST. - */ - static std::optional check_revision(const WorldStateRevision& revision); - - /** Why a write to this fork cannot be served, or nullopt when it can. */ - static std::optional check_fork(uint64_t fork_id); + /** Restarts from `genesis`: every block, fork and uncommitted write is dropped. */ + void reset(const GenesisConfig& genesis); /** The tree height the protocol fixes for `tree_id`. */ static uint32_t tree_height(MerkleTreeId tree_id); - // Reads. `revision` must have passed check_revision. + // Reads. std::vector get_state_reference(const WorldStateRevision& revision) const; std::vector get_initial_state_reference() const; TreeState get_tree_info(const WorldStateRevision& revision, MerkleTreeId tree_id) const; - /** Leaf of an append-only tree (note hash, L1-to-L2 message, archive); nullopt past its size. */ + /** + * @brief Leaf of an append-only tree (note hash, L1-to-L2 message, archive), read as a node reads it: zero if + * unwritten, nullopt past the tree's capacity, and for a historical read nullopt past that block's size or + * for a block the chain does not have. + */ std::optional get_leaf_value(const WorldStateRevision& revision, MerkleTreeId tree_id, uint64_t index) const; + /** Preimage of an indexed-tree leaf: the empty leaf for a slot a batch left empty, nullopt past the tree's size. */ std::optional> get_nullifier_preimage(const WorldStateRevision& revision, uint64_t index) const; std::optional> get_public_data_preimage(const WorldStateRevision& revision, uint64_t index) const; SiblingPath get_sibling_path(const WorldStateRevision& revision, MerkleTreeId tree_id, uint64_t index) const; - /** First index at or after `start` holding `leaf` in an append-only tree. */ + /** The block that added each leaf index; nullopt for an index the revision's committed tree does not hold. */ + std::vector> get_block_numbers_for_leaf_indices( + const WorldStateRevision& revision, MerkleTreeId tree_id, std::span indices) const; + /** + * @brief The index holding `key` (an append-only leaf, a nullifier or a public-data slot), if it is at or + * after `start`. Each key has one index, the first it was written at; zero leaves are not indexed. + */ std::optional find_leaf_index(const WorldStateRevision& revision, MerkleTreeId tree_id, - const FF& leaf, + const FF& key, uint64_t start) const; - /** First index at or after `start` of the indexed-tree leaf with `key` (a nullifier, or a slot). */ - std::optional find_indexed_leaf_index(const WorldStateRevision& revision, - MerkleTreeId tree_id, - const FF& key, - uint64_t start) const; GetLowIndexedLeafResponse find_low_leaf(const WorldStateRevision& revision, MerkleTreeId tree_id, const FF& key) const; - // Writes, all to the current state. - /** Append to an append-only tree (note hash, L1-to-L2 message, archive). */ - void append_leaves(MerkleTreeId tree_id, std::span leaves); - SequentialInsertionResult insert_nullifier(const NullifierLeafValue& leaf); - SequentialInsertionResult insert_public_data(const PublicDataLeafValue& leaf); - /** Whether `nullifier` is already in the current nullifier tree (inserting it again is an error). */ - bool has_nullifier(const FF& nullifier) const; - /** Whether `state` equals the current state of the four non-archive trees, as UpdateArchive requires. */ - bool state_matches(std::span state) const; - void update_archive(const FF& block_header_hash); - - void create_checkpoint(); - void commit_checkpoint(); - void revert_checkpoint(); - void commit_all_checkpoints(); - void revert_all_checkpoints(); - bool has_checkpoint() const { return !checkpoints_.empty(); } - - /** The genesis block header hash: the archive's first leaf. */ - const FF& genesis_block_header_hash() const { return genesis_block_header_hash_; } + // Writes to a fork's current state (fork 0 is the canonical chain's uncommitted state). + void append_leaves(uint64_t fork_id, MerkleTreeId tree_id, std::span leaves); + BatchInsertionResult batch_insert_nullifiers(uint64_t fork_id, + std::span leaves, + uint32_t subtree_depth); + BatchInsertionResult batch_insert_public_data(uint64_t fork_id, + std::span leaves, + uint32_t subtree_depth); + SequentialInsertionResult sequential_insert_nullifiers( + uint64_t fork_id, std::span leaves); + SequentialInsertionResult sequential_insert_public_data( + uint64_t fork_id, std::span leaves); + /** Appends `block_header_hash` to the archive, if `block_state_ref` is the fork's current state. */ + void update_archive(uint64_t fork_id, std::span block_state_ref, const FF& block_header_hash); + + void create_checkpoint(uint64_t fork_id); + void commit_checkpoint(uint64_t fork_id); + void revert_checkpoint(uint64_t fork_id); + void commit_all_checkpoints(uint64_t fork_id); + void revert_all_checkpoints(uint64_t fork_id); + + /** A fork from the committed state at `block_number`, or at the latest block. */ + uint64_t create_fork(std::optional block_number); + void delete_fork(uint64_t fork_id); + // The canonical chain. + /** Commits the uncommitted state as the next block. */ + void commit(); + void rollback(); /** - * @brief Hash of the genesis block header over `trees` (nullifier, note hash, public data, L1-to-L2). + * @brief Applies a block's effects to the canonical chain and commits it, as a node syncing a block does. + * @details Rolls back first. Fails, committing nothing, unless the result is `block_state_ref` with + * `block_header_hash` at the archive's tip, and the archive roots before and after match the expected + * ones when given. + */ + void sync_block(std::span block_state_ref, + const FF& block_header_hash, + std::span padded_note_hashes, + std::span padded_l1_to_l2_messages, + std::span padded_nullifiers, + std::span public_data_writes, + const std::optional& expected_archive_root, + const std::optional& expected_previous_archive_root); + void finalize_blocks(block_number_t to_block_number); + void unwind_blocks(block_number_t to_block_number); + void remove_historical_blocks(block_number_t to_block_number); + + StatusSummary status() const; + /** Per-tree meta for a full status, in MerkleTreeId order. */ + std::array tree_meta() const; + + block_number_t latest_block_number() const { return static_cast(blocks_.size() - 1); } + bool has_fork(uint64_t fork_id) const { return forks_.contains(fork_id); } + + /** + * @brief Hash of the genesis block header over the four state trees' genesis state. * @details Must match BlockHeader::hash in noir-protocol-circuits' block_header.nr for a header whose * only non-zero fields are the four trees' state and the timestamp. */ - static FF compute_genesis_block_header_hash(const TreeRoots& trees, uint64_t genesis_timestamp); + static FF compute_genesis_block_header_hash(std::span state, + uint64_t genesis_timestamp, + const FF& header_generator_point); private: - struct State { - MemoryMerkleDB trees; - ArchiveTree archive; + struct NoPayload {}; + + /** A note hash, L1-to-L2 message or archive tree: leaves are values. */ + struct AppendOnlyTree { + PersistentMerkleTree tree; + PersistentKeyIndex indices; + uint64_t size = 0; }; - const State& view(const WorldStateRevision& revision) const; + /** A nullifier or public-data tree: leaves are hashes of the preimages they carry. */ + template struct IndexedTree { + using Hashing = HashingPolicy; + using Leaf = typename PersistentMerkleTree>::Leaf; + PersistentMerkleTree> tree; + PersistentKeyIndex indices; + uint64_t size = 0; + }; + using NullifierTree = IndexedTree; + using PublicDataTree = IndexedTree; + + /** All five trees. Copying one is copying ten pointers. */ + struct Trees { + NullifierTree nullifier; + AppendOnlyTree note_hash; + PublicDataTree public_data; + AppendOnlyTree l1_to_l2; + AppendOnlyTree archive; + }; + + struct Fork { + /** The committed block the fork started from (for the canonical fork, the latest block). */ + block_number_t block_number; + Trees base; + Trees current; + std::vector checkpoints; + }; + + static Trees build_genesis(const GenesisConfig& genesis); + + const Fork& fork(uint64_t fork_id) const; + Fork& fork(uint64_t fork_id); + const Trees& committed_block(block_number_t block_number) const; + const Trees& view(const WorldStateRevision& revision) const; + /** Drops the forks of `block_number`, as unwinding or removing that block does. */ + void drop_forks_of(block_number_t block_number); + + static TreeState tree_state(const Trees& trees, MerkleTreeId tree_id); + static const PersistentKeyIndex& key_index(const Trees& trees, MerkleTreeId tree_id); + static std::vector state_reference(const Trees& trees); + static bool state_matches(const Trees& trees, std::span state); + static uint64_t tree_size(const Trees& trees, MerkleTreeId tree_id); + + static void append(AppendOnlyTree& tree, std::span leaves); + template + static BatchInsertionResult batch_insert(Tree& tree, + std::span leaves, + uint32_t subtree_depth, + bool capture_witness); + template + static SequentialInsertionResult sequential_insert(Tree& tree, + std::span leaves); - State genesis_; - State current_; - std::stack checkpoints_; - FF genesis_block_header_hash_; + std::vector blocks_; + std::map forks_; + uint64_t next_fork_id_ = 1; + block_number_t finalized_block_number_ = 0; + block_number_t oldest_historical_block_ = 0; }; } // namespace bb::world_state diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp index 7d49999bd66..cc6ad40f44b 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.test.cpp @@ -5,15 +5,26 @@ #include #include +#include using namespace bb; using namespace bb::world_state; namespace { +constexpr uint64_t CANONICAL = 0; const WorldStateRevision LATEST_UNCOMMITTED{ .includeUncommitted = true }; const WorldStateRevision LATEST_COMMITTED{ .includeUncommitted = false }; -const WorldStateRevision GENESIS_BLOCK{ .blockNumber = 0 }; + +WorldStateRevision at_block(block_number_t block) +{ + return WorldStateRevision{ .blockNumber = block }; +} + +WorldStateRevision fork_current(uint64_t fork_id) +{ + return WorldStateRevision{ .forkId = fork_id, .includeUncommitted = true }; +} // Recompute a tree root from a leaf hash and its sibling path. template FF root_from_path(FF node, uint64_t index, const SiblingPath& path) @@ -25,6 +36,25 @@ template FF root_from_path(FF node, uint64_t index, const return node; } +uint64_t size_of(const ReferenceWorldState& ws, const WorldStateRevision& revision, MerkleTreeId tree_id) +{ + return ws.get_tree_info(revision, tree_id).size; +} + +/** Commits a block that appends `note_hash` and nullifies `nullifier`, the way a node syncs one. */ +void sync_block(ReferenceWorldState& ws, FF note_hash, FF nullifier) +{ + uint64_t fork_id = ws.create_fork(std::nullopt); + std::vector notes{ note_hash }; + std::vector nullifiers{ NullifierLeafValue(nullifier) }; + ws.append_leaves(fork_id, MerkleTreeId::NOTE_HASH_TREE, notes); + ws.batch_insert_nullifiers(fork_id, nullifiers, 0); + auto state = ws.get_state_reference(fork_current(fork_id)); + FF header_hash = FF(1000) + FF(ws.latest_block_number()); + ws.delete_fork(fork_id); + ws.sync_block(state, header_hash, notes, {}, nullifiers, {}, std::nullopt, std::nullopt); +} + } // namespace TEST(ReferenceWorldState, CanonicalGenesisMatchesProtocolConstants) @@ -35,11 +65,11 @@ TEST(ReferenceWorldState, CanonicalGenesisMatchesProtocolConstants) EXPECT_EQ(nullifiers.root, FF(GENESIS_NULLIFIER_TREE_ROOT)); EXPECT_EQ(nullifiers.size, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); - EXPECT_EQ(ws.genesis_block_header_hash(), FF(GENESIS_BLOCK_HEADER_HASH)); auto archive = ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::ARCHIVE); EXPECT_EQ(archive.root, FF(GENESIS_ARCHIVE_ROOT)); EXPECT_EQ(archive.size, 1); EXPECT_EQ(ws.get_leaf_value(LATEST_COMMITTED, MerkleTreeId::ARCHIVE, 0), FF(GENESIS_BLOCK_HEADER_HASH)); + EXPECT_EQ(ws.latest_block_number(), 0); } TEST(ReferenceWorldState, EmptyGenesisMatchesEmptyWorldStateRoots) @@ -100,30 +130,21 @@ TEST(ReferenceWorldState, GenesisPrefillValidation) public_data.prefilled_public_data = { PublicDataLeafValue(FF(2000), FF(1)), PublicDataLeafValue(FF(1000), FF(2)) }; EXPECT_EQ(ReferenceWorldState::check_genesis(public_data), "prefilled public data: Prefilled values must be unique and sorted"); + EXPECT_THROW(ReferenceWorldState{ public_data }, std::runtime_error); } -TEST(ReferenceWorldState, RevisionAndForkChecks) -{ - EXPECT_FALSE(ReferenceWorldState::check_revision(LATEST_UNCOMMITTED).has_value()); - EXPECT_FALSE(ReferenceWorldState::check_revision(GENESIS_BLOCK).has_value()); - EXPECT_TRUE(ReferenceWorldState::check_revision(WorldStateRevision{ .blockNumber = 3 }).has_value()); - EXPECT_TRUE(ReferenceWorldState::check_revision(WorldStateRevision{ .forkId = 1 }).has_value()); - EXPECT_FALSE(ReferenceWorldState::check_fork(0).has_value()); - EXPECT_EQ(ReferenceWorldState::check_fork(2), "the reference world state has one fork (0), not 2"); -} - -TEST(ReferenceWorldState, CommittedReadsSeeGenesisUncommittedSeeCurrent) +TEST(ReferenceWorldState, CommittedReadsSeeTheLastBlockUncommittedSeeCurrent) { ReferenceWorldState ws; const std::vector note_hashes{ FF(11), FF(22), FF(33) }; - ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, note_hashes); - EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 3); - EXPECT_EQ(ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 0); - EXPECT_EQ(ws.get_tree_info(GENESIS_BLOCK, MerkleTreeId::NOTE_HASH_TREE).size, 0); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_EQ(size_of(ws, LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE), 0); + EXPECT_EQ(size_of(ws, at_block(0), MerkleTreeId::NOTE_HASH_TREE), 0); EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 1), FF(22)); - EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 3), std::nullopt); + EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, 3), FF(0)); EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(33), 0), 2); EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(11), 1), std::nullopt); EXPECT_EQ(ws.find_leaf_index(LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(33), 0), std::nullopt); @@ -132,29 +153,128 @@ TEST(ReferenceWorldState, CommittedReadsSeeGenesisUncommittedSeeCurrent) EXPECT_EQ(path.size(), NOTE_HASH_TREE_HEIGHT); EXPECT_EQ(root_from_path(FF(22), 1, path), ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).root); + + ws.commit(); + EXPECT_EQ(ws.latest_block_number(), 1); + EXPECT_EQ(size_of(ws, LATEST_COMMITTED, MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_EQ(size_of(ws, at_block(0), MerkleTreeId::NOTE_HASH_TREE), 0); + EXPECT_EQ(size_of(ws, at_block(1), MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_THROW(ws.get_tree_info(at_block(2), MerkleTreeId::NOTE_HASH_TREE), std::runtime_error); +} + +TEST(ReferenceWorldState, ForksAreIsolatedAndCommittedReadsSeeTheirBlock) +{ + ReferenceWorldState ws; + sync_block(ws, FF(1), FF(0x1001)); + sync_block(ws, FF(2), FF(0x1002)); + ASSERT_EQ(ws.latest_block_number(), 2); + + uint64_t at_latest = ws.create_fork(std::nullopt); + uint64_t at_one = ws.create_fork(1); + ws.append_leaves(at_latest, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(3) }); + + EXPECT_EQ(size_of(ws, fork_current(at_latest), MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_EQ(size_of(ws, WorldStateRevision{ .forkId = at_latest }, MerkleTreeId::NOTE_HASH_TREE), 2); + EXPECT_EQ(size_of(ws, fork_current(at_one), MerkleTreeId::NOTE_HASH_TREE), 1); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 2); + EXPECT_EQ(ws.find_leaf_index(fork_current(at_latest), MerkleTreeId::NOTE_HASH_TREE, FF(3), 0), 2); + EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE, FF(3), 0), std::nullopt); + + // Leaves are numbered by the committed block that added them; a fork's uncommitted ones have none. + std::vector indices{ 0, 1, 2 }; + auto blocks = ws.get_block_numbers_for_leaf_indices(fork_current(at_latest), MerkleTreeId::NOTE_HASH_TREE, indices); + EXPECT_EQ(blocks, (std::vector>{ 1, 2, std::nullopt })); + blocks = ws.get_block_numbers_for_leaf_indices(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE, indices); + EXPECT_EQ(blocks, (std::vector>{ 0, 0, 0 })); + + EXPECT_THROW(ws.delete_fork(CANONICAL), std::runtime_error); + ws.delete_fork(at_one); + EXPECT_THROW(ws.get_tree_info(fork_current(at_one), MerkleTreeId::NOTE_HASH_TREE), std::runtime_error); + EXPECT_THROW(ws.create_fork(3), std::runtime_error); +} + +TEST(ReferenceWorldState, SyncBlockChecksTheStateAndCommitsNothingOnFailure) +{ + ReferenceWorldState ws; + std::vector notes{ FF(5) }; + auto initial = ws.get_state_reference(LATEST_COMMITTED); + // The block claims the state before its own effects, so the result does not match it. + EXPECT_THROW(ws.sync_block(initial, FF(77), notes, {}, {}, {}, std::nullopt, std::nullopt), std::runtime_error); + EXPECT_EQ(ws.latest_block_number(), 0); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 0); + + FF genesis_archive = ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::ARCHIVE).root; + EXPECT_THROW(ws.sync_block(initial, FF(77), {}, {}, {}, {}, std::nullopt, FF(123)), std::runtime_error); + ws.sync_block(initial, FF(77), {}, {}, {}, {}, std::nullopt, genesis_archive); + EXPECT_EQ(ws.latest_block_number(), 1); + EXPECT_EQ(ws.get_leaf_value(LATEST_COMMITTED, MerkleTreeId::ARCHIVE, 1), FF(77)); + // A header hash already in the archive is not its tip. + EXPECT_THROW(ws.sync_block(initial, FF(77), {}, {}, {}, {}, std::nullopt, std::nullopt), std::runtime_error); +} + +TEST(ReferenceWorldState, UnwindDropsBlocksAndTheirForks) +{ + ReferenceWorldState ws; + sync_block(ws, FF(1), FF(0x1001)); + sync_block(ws, FF(2), FF(0x1002)); + sync_block(ws, FF(3), FF(0x1003)); + uint64_t at_three = ws.create_fork(std::nullopt); + uint64_t at_one = ws.create_fork(1); + FF root_at_one = ws.get_tree_info(at_block(1), MerkleTreeId::NULLIFIER_TREE).root; + + EXPECT_THROW(ws.unwind_blocks(3), std::runtime_error); + ws.unwind_blocks(1); + EXPECT_EQ(ws.latest_block_number(), 1); + EXPECT_EQ(ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE).root, root_at_one); + EXPECT_THROW(ws.get_tree_info(fork_current(at_three), MerkleTreeId::NULLIFIER_TREE), std::runtime_error); + EXPECT_EQ(ws.get_tree_info(fork_current(at_one), MerkleTreeId::NULLIFIER_TREE).root, root_at_one); + + // The chain carries on from block 1, and a finalized block cannot be unwound. + sync_block(ws, FF(4), FF(0x1004)); + EXPECT_EQ(ws.latest_block_number(), 2); + ws.finalize_blocks(2); + EXPECT_EQ(ws.status().finalized_block_number, 2); + sync_block(ws, FF(5), FF(0x1005)); + EXPECT_THROW(ws.unwind_blocks(1), std::runtime_error); + EXPECT_THROW(ws.finalize_blocks(9), std::runtime_error); } -TEST(ReferenceWorldState, NullifierInsertionWitnessesAreConsistent) +TEST(ReferenceWorldState, RemovingHistoryStopsReadsOfOldBlocks) +{ + ReferenceWorldState ws; + EXPECT_EQ(ws.status().oldest_historical_block, 0); + for (int i = 1; i <= 4; ++i) { + sync_block(ws, FF(i), FF(0x1000 + i)); + } + EXPECT_EQ(ws.status().oldest_historical_block, 1); + EXPECT_THROW(ws.remove_historical_blocks(3), std::runtime_error); + ws.finalize_blocks(3); + ws.remove_historical_blocks(3); + EXPECT_EQ(ws.status().oldest_historical_block, 3); + EXPECT_THROW(ws.get_tree_info(at_block(2), MerkleTreeId::NOTE_HASH_TREE), std::runtime_error); + EXPECT_EQ(size_of(ws, at_block(3), MerkleTreeId::NOTE_HASH_TREE), 3); + EXPECT_THROW(ws.remove_historical_blocks(3), std::runtime_error); +} + +TEST(ReferenceWorldState, SequentialNullifierWitnessesAreConsistent) { ReferenceWorldState ws; const FF nullifier(0x1234); - ASSERT_FALSE(ws.has_nullifier(nullifier)); auto low = ws.find_low_leaf(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, nullifier); EXPECT_FALSE(low.is_already_present); - auto result = ws.insert_nullifier(NullifierLeafValue(nullifier)); + std::vector leaves{ NullifierLeafValue(nullifier) }; + auto result = ws.sequential_insert_nullifiers(CANONICAL, leaves); ASSERT_EQ(result.low_leaf_witness_data.size(), 1); ASSERT_EQ(result.insertion_witness_data.size(), 1); EXPECT_EQ(result.low_leaf_witness_data[0].index, low.index); EXPECT_EQ(result.insertion_witness_data[0].index, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); - EXPECT_TRUE(ws.has_nullifier(nullifier)); - auto index = ws.find_indexed_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, nullifier, 0); + auto index = ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, nullifier, 0); ASSERT_EQ(index, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE); auto preimage = ws.get_nullifier_preimage(LATEST_UNCOMMITTED, *index); ASSERT_TRUE(preimage.has_value()); EXPECT_EQ(preimage->leaf.nullifier, nullifier); - // The low leaf now points at the new leaf. EXPECT_EQ(ws.get_nullifier_preimage(LATEST_UNCOMMITTED, low.index)->nextKey, nullifier); // A membership witness read back from the tree reproduces its root. @@ -163,18 +283,66 @@ TEST(ReferenceWorldState, NullifierInsertionWitnessesAreConsistent) EXPECT_EQ(root_from_path(leaf_hash, *index, path), ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE).root); - // The genesis view is untouched. + // Inserting it again fails and changes nothing; the committed view is untouched throughout. + FF root = ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE).root; + EXPECT_THROW(ws.sequential_insert_nullifiers(CANONICAL, leaves), std::runtime_error); + EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE).root, root); EXPECT_EQ(ws.get_tree_info(LATEST_COMMITTED, MerkleTreeId::NULLIFIER_TREE).root, FF(GENESIS_NULLIFIER_TREE_ROOT)); } +TEST(ReferenceWorldState, BatchInsertionTakesOneSlotPerValue) +{ + // A batch takes one slot per value at the value's own position, a zero value's left empty, and links + // the values in key order, updating low leaves in descending key order. + ReferenceWorldState batch; + std::vector leaves{ NullifierLeafValue(FF(0x3000)), + NullifierLeafValue(FF(0)), + NullifierLeafValue(FF(0x1000)), + NullifierLeafValue(FF(0x2000)) }; + auto result = batch.batch_insert_nullifiers(CANONICAL, leaves, 2); + EXPECT_EQ(size_of(batch, LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE), + ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + 4); + // The zero value's slot is empty: it reads as the empty leaf, as on a node. + EXPECT_EQ(batch.get_nullifier_preimage(LATEST_UNCOMMITTED, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + 1), + IndexedLeaf::empty()); + + ASSERT_EQ(result.sorted_leaves.size(), 4); + EXPECT_EQ(result.sorted_leaves[0].second, 0); + EXPECT_EQ(result.sorted_leaves[1].second, 3); + EXPECT_EQ(result.sorted_leaves[2].second, 2); + EXPECT_EQ(result.sorted_leaves[3].second, 1); + ASSERT_EQ(result.low_leaf_witness_data.size(), 4); + EXPECT_EQ(result.low_leaf_witness_data[3].index, 0); + EXPECT_EQ(result.subtree_path.size(), NULLIFIER_TREE_HEIGHT - 2); + + // Every value is linked in key order. + for (size_t i = 0; i < leaves.size(); ++i) { + if (leaves[i].is_empty()) { + continue; + } + auto index = batch.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, leaves[i].nullifier, 0); + EXPECT_EQ(index, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + i); + } + auto low_of_2000 = batch.find_low_leaf(LATEST_UNCOMMITTED, MerkleTreeId::NULLIFIER_TREE, FF(0x2000)); + EXPECT_TRUE(low_of_2000.is_already_present); + EXPECT_EQ( + batch.get_nullifier_preimage(LATEST_UNCOMMITTED, ReferenceWorldState::INDEXED_TREE_INITIAL_SIZE + 2)->nextKey, + FF(0x2000)); + + std::vector duplicate{ NullifierLeafValue(FF(9)), NullifierLeafValue(FF(9)) }; + EXPECT_THROW(batch.batch_insert_nullifiers(CANONICAL, duplicate, 1), std::runtime_error); +} + TEST(ReferenceWorldState, PublicDataUpdatesInPlace) { ReferenceWorldState ws; - ws.insert_public_data(PublicDataLeafValue(FF(5000), FF(1))); - auto size_after_insert = ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE).size; - ws.insert_public_data(PublicDataLeafValue(FF(5000), FF(2))); - EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE).size, size_after_insert); - auto index = ws.find_indexed_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE, FF(5000), 0); + ws.sequential_insert_public_data(CANONICAL, std::vector{ PublicDataLeafValue(FF(5000), FF(1)) }); + auto size_after_insert = size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE); + auto update = ws.sequential_insert_public_data(CANONICAL, std::vector{ PublicDataLeafValue(FF(5000), FF(2)) }); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE), size_after_insert); + ASSERT_EQ(update.insertion_witness_data.size(), 1); + EXPECT_EQ(update.insertion_witness_data[0].path, SiblingPath(PUBLIC_DATA_TREE_HEIGHT, FF::zero())); + auto index = ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::PUBLIC_DATA_TREE, FF(5000), 0); ASSERT_TRUE(index.has_value()); EXPECT_EQ(ws.get_public_data_preimage(LATEST_UNCOMMITTED, *index)->leaf.value, FF(2)); } @@ -182,37 +350,46 @@ TEST(ReferenceWorldState, PublicDataUpdatesInPlace) TEST(ReferenceWorldState, CheckpointsRevertAndCommit) { ReferenceWorldState ws; - ws.create_checkpoint(); - ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(1) }); - ws.create_checkpoint(); - ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(2) }); - ws.revert_checkpoint(); - EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 1); - ws.commit_checkpoint(); - EXPECT_FALSE(ws.has_checkpoint()); - EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 1); - - ws.create_checkpoint(); - ws.create_checkpoint(); - ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(3) }); - ws.revert_all_checkpoints(); - EXPECT_FALSE(ws.has_checkpoint()); - EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE).size, 1); + ws.create_checkpoint(CANONICAL); + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(1) }); + ws.create_checkpoint(CANONICAL); + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(2) }); + ws.revert_checkpoint(CANONICAL); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 1); + ws.commit_checkpoint(CANONICAL); + EXPECT_THROW(ws.commit_checkpoint(CANONICAL), std::runtime_error); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 1); + + ws.create_checkpoint(CANONICAL); + ws.create_checkpoint(CANONICAL); + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(3) }); + ws.revert_all_checkpoints(CANONICAL); + EXPECT_THROW(ws.revert_checkpoint(CANONICAL), std::runtime_error); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 1); + + ws.rollback(); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::NOTE_HASH_TREE), 0); } TEST(ReferenceWorldState, UpdateArchiveRequiresTheCurrentState) { ReferenceWorldState ws; - ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(7) }); + ws.append_leaves(CANONICAL, MerkleTreeId::NOTE_HASH_TREE, std::vector{ FF(7) }); auto current = ws.get_state_reference(LATEST_UNCOMMITTED); - EXPECT_TRUE(ws.state_matches(current)); - EXPECT_FALSE(ws.state_matches(ws.get_initial_state_reference())); + EXPECT_THROW(ws.update_archive(CANONICAL, ws.get_initial_state_reference(), FF(99)), std::runtime_error); // Order is not part of the contract. std::reverse(current.begin(), current.end()); - EXPECT_TRUE(ws.state_matches(current)); - - ws.update_archive(FF(99)); - EXPECT_EQ(ws.get_tree_info(LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE).size, 2); + ws.update_archive(CANONICAL, current, FF(99)); + EXPECT_EQ(size_of(ws, LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE), 2); EXPECT_EQ(ws.get_leaf_value(LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE, 1), FF(99)); EXPECT_EQ(ws.find_leaf_index(LATEST_UNCOMMITTED, MerkleTreeId::ARCHIVE, FF(99), 0), 1); } + +// wasm does not find main() in gtest_main (see ecc_tests' secp256r1.test.cpp). +#ifdef __wasm__ +GTEST_API_ int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +#endif diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc b/barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc deleted file mode 100644 index aed3f5c62cc..00000000000 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc +++ /dev/null @@ -1,270 +0,0 @@ -// The world-state commands the protocol itself needs: reading tree state, membership and low-leaf -// witnesses, appending and inserting leaves, and advancing the archive. Code written against this -// contract runs against any world state that implements it: the in-memory reference -// (@aztec-foundation/wsdb-ref, generated from this file) and a node world state, whose schema -// extends this one with forks, block history and storage. -// -// Every definition here is the one the node world state already uses, so an extension can add to -// it without redefining anything. Revisions and fork ids keep their full shape; an implementation -// may answer anything but fork 0 at block 0 or the latest block (0xffffffff) with an error. -// -// A world state starts at the canonical genesis: empty trees, except the indexed trees' prefill, -// whose last nullifiers are the protocol contracts' registration nullifiers, and an archive holding -// the genesis block header hash (GENESIS_* in constants.nr). How it gets there is not part of this -// contract. -// -// Tree ids: 0 nullifier, 1 note hash, 2 public data, 3 L1-to-L2 message, 4 archive. -{ - "service": "Wsdb", - "aliases": { - "MerkleTreeId": "u32", - "ForkId": "u64", - "BlockNumber": "u32", - "Fr": "bin32", - "LeafIndex": "u64", - "PublicDataSlot": "bin32", - "PublicDataValue": "bin32", - "Nullifier": "bin32", - "BlockHeaderHash": "bin32" - }, - "types": { - "WorldStateRevision": { - "forkId": "ForkId", - "blockNumber": "BlockNumber", - "includeUncommitted": "bool" - }, - "TreeStateReference": { - "treeId": "MerkleTreeId", - "root": "Fr", - "size": "LeafIndex" - }, - "PublicDataLeafValue": { - "slot": "PublicDataSlot", - "value": "PublicDataValue" - }, - "NullifierLeafValue": { - "nullifier": "Nullifier" - }, - "IndexedPublicDataLeafValue": { - "leaf": "PublicDataLeafValue", - "nextIndex": "LeafIndex", - "nextKey": "Fr" - }, - "IndexedNullifierLeafValue": { - "leaf": "NullifierLeafValue", - "nextIndex": "LeafIndex", - "nextKey": "Fr" - }, - "PublicDataLeafUpdateWitnessData": { - "leaf": "IndexedPublicDataLeafValue", - "index": "LeafIndex", - "path": "Fr[]" - }, - "NullifierLeafUpdateWitnessData": { - "leaf": "IndexedNullifierLeafValue", - "index": "LeafIndex", - "path": "Fr[]" - }, - "SequentialInsertionResultPublicData": { - "lowLeafWitnessData": "PublicDataLeafUpdateWitnessData[]", - "insertionWitnessData": "PublicDataLeafUpdateWitnessData[]" - }, - "SequentialInsertionResultNullifier": { - "lowLeafWitnessData": "NullifierLeafUpdateWitnessData[]", - "insertionWitnessData": "NullifierLeafUpdateWitnessData[]" - } - }, - "error": { - "message": "string" - }, - "commands": { - "GetTreeInfo": { - "request": { - "treeId": "MerkleTreeId", - "revision": "WorldStateRevision" - }, - "response": { - "treeId": "MerkleTreeId", - "root": "Fr", - "size": "LeafIndex", - "depth": "u32" - } - }, - "GetStateReference": { - "request": { - "revision": "WorldStateRevision" - }, - "response": { - "state": "TreeStateReference[]" - } - }, - "GetInitialStateReference": { - "request": {}, - "response": { - "state": "TreeStateReference[]" - } - }, - "GetLeafValue": { - "request": { - "treeId": "MerkleTreeId", - "revision": "WorldStateRevision", - "leafIndex": "LeafIndex" - }, - "response": { - "value": "Fr?" - } - }, - "GetPublicDataLeafValue": { - "request": { - "revision": "WorldStateRevision", - "leafIndex": "LeafIndex" - }, - "response": { - "value": "PublicDataLeafValue?" - } - }, - "GetNullifierLeafValue": { - "request": { - "revision": "WorldStateRevision", - "leafIndex": "LeafIndex" - }, - "response": { - "value": "NullifierLeafValue?" - } - }, - "GetPublicDataLeafPreimage": { - "request": { - "revision": "WorldStateRevision", - "leafIndex": "LeafIndex" - }, - "response": { - "preimage": "IndexedPublicDataLeafValue?" - } - }, - "GetNullifierLeafPreimage": { - "request": { - "revision": "WorldStateRevision", - "leafIndex": "LeafIndex" - }, - "response": { - "preimage": "IndexedNullifierLeafValue?" - } - }, - "GetSiblingPath": { - "request": { - "treeId": "MerkleTreeId", - "revision": "WorldStateRevision", - "leafIndex": "LeafIndex" - }, - "response": { - "path": "Fr[]" - } - }, - "FindLeafIndices": { - "request": { - "treeId": "MerkleTreeId", - "revision": "WorldStateRevision", - "leaves": "Fr[]", - "startIndex": "LeafIndex" - }, - "response": { - "indices": "LeafIndex?[]" - } - }, - "FindPublicDataLeafIndices": { - "request": { - "revision": "WorldStateRevision", - "leaves": "PublicDataLeafValue[]", - "startIndex": "LeafIndex" - }, - "response": { - "indices": "LeafIndex?[]" - } - }, - "FindNullifierLeafIndices": { - "request": { - "revision": "WorldStateRevision", - "leaves": "NullifierLeafValue[]", - "startIndex": "LeafIndex" - }, - "response": { - "indices": "LeafIndex?[]" - } - }, - "FindLowLeaf": { - "request": { - "treeId": "MerkleTreeId", - "revision": "WorldStateRevision", - "key": "Fr" - }, - "response": { - "alreadyPresent": "bool", - "index": "LeafIndex" - } - }, - "AppendLeaves": { - "request": { - "treeId": "MerkleTreeId", - "leaves": "Fr[]", - "forkId": "ForkId" - }, - "response": {} - }, - "SequentialInsertPublicData": { - "request": { - "leaves": "PublicDataLeafValue[]", - "forkId": "ForkId" - }, - "response": { - "result": "SequentialInsertionResultPublicData" - } - }, - "SequentialInsertNullifier": { - "request": { - "leaves": "NullifierLeafValue[]", - "forkId": "ForkId" - }, - "response": { - "result": "SequentialInsertionResultNullifier" - } - }, - "UpdateArchive": { - "request": { - "blockStateRef": "TreeStateReference[]", - "blockHeaderHash": "BlockHeaderHash", - "forkId": "ForkId" - }, - "response": {} - }, - "CreateCheckpoint": { - "request": { - "forkId": "ForkId" - }, - "response": {} - }, - "CommitCheckpoint": { - "request": { - "forkId": "ForkId" - }, - "response": {} - }, - "RevertCheckpoint": { - "request": { - "forkId": "ForkId" - }, - "response": {} - }, - "CommitAllCheckpoints": { - "request": { - "forkId": "ForkId" - }, - "response": {} - }, - "RevertAllCheckpoints": { - "request": { - "forkId": "ForkId" - }, - "response": {} - } - } -} diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt index 844160891aa..5ce22249897 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt @@ -1,10 +1,11 @@ -# wsdb-ref: the in-memory reference world state (world_state_reference) as a service over the wsdb -# base contract, ../world_state_reference/wsdb_base_schema.jsonc. The generated dispatch runs the -# handlers here; the generated FFI entry (wsdb_ipc_ffi_entry) runs the dispatch in-process. Two -# artifacts carry that entry to @aztec-foundation/wsdb-ref (barretenberg/ts/wsdb-ref): +# wsdb-ref: the in-memory reference world state (world_state_reference) as a service over the node +# world state's contract, native-packages/wsdb/wsdb_schema.jsonc (the schema the AVM's wsdb client in +# vm2_wsdb is generated from too). The generated dispatch runs the handlers here; the generated FFI +# entry (wsdb_ipc_ffi_entry) runs the dispatch in-process. Two artifacts carry that entry to +# @aztec-foundation/wsdb-ref (barretenberg/ts/wsdb-ref): # - wsdb-ref.wasm, a WASI reactor (wasm builds), for the package's wasm transport; # - wsdb_ref_napi.node, a Node-API addon over the entry (native builds), for its napi transport. -set(WSDB_REF_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/../world_state_reference/wsdb_base_schema.jsonc) +set(WSDB_REF_SCHEMA ${CMAKE_SOURCE_DIR}/../../native-packages/wsdb/wsdb_schema.jsonc) set(WSDB_REF_GEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/generated) set(WSDB_REF_GEN_OUTPUTS ${WSDB_REF_GEN_DIR}/wsdb_dispatch.hpp @@ -32,7 +33,7 @@ add_custom_command( --cpp-namespace bb::wsdb_ref --cpp-include-dir barretenberg/wsdb_ref/generated DEPENDS ${WSDB_REF_SCHEMA} ${IPC_CODEGEN_SRC} - COMMENT "Generating the wsdb-ref dispatch, FFI entry and Node-API addon from wsdb_base_schema.jsonc" + COMMENT "Generating the wsdb-ref dispatch, FFI entry and Node-API addon from native-packages/wsdb/wsdb_schema.jsonc" VERBATIM ) add_custom_target(wsdb_ref_generated DEPENDS ${WSDB_REF_GEN_OUTPUTS}) @@ -40,6 +41,10 @@ add_custom_target(wsdb_ref_generated DEPENDS ${WSDB_REF_GEN_OUTPUTS}) # Explicit sources: the generated Node-API addon must stay out of the module (it needs napi.h), and # the generated FFI entry does not exist yet when a clean tree is configured. file(GLOB WSDB_REF_TESTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.test.cpp) +if(WASM) + # The comparison with aztec-wsdb spawns it and talks over its socket. + list(FILTER WSDB_REF_TESTS EXCLUDE REGEX "wsdb_ref_native_diff\\.test\\.cpp$") +endif() barretenberg_module_with_sources( wsdb_ref SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_ref_handlers.cpp ${WSDB_REF_GEN_DIR}/wsdb_ffi.cpp @@ -51,6 +56,11 @@ add_dependencies(wsdb_ref_objects wsdb_ref_generated) if(TARGET wsdb_ref_test_objects) add_dependencies(wsdb_ref_test_objects wsdb_ref_generated) endif() +if(TARGET wsdb_ref_tests AND NOT WASM) + # wsdb_ref_native_diff.test.cpp reaches aztec-wsdb through ipc-runtime's socket client. + target_link_libraries(wsdb_ref_test_objects PRIVATE ipc_runtime) + target_link_libraries(wsdb_ref_tests PRIVATE ipc_runtime) +endif() set_target_properties(wsdb_ref_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) if(WASM) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp index 76e973dffa9..97e5da06494 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp @@ -141,46 +141,103 @@ TEST(WsdbRef, FfiEntryAnswersBadRequestsWithErrors) msgpack::packer pk(buf); pk.pack_array(1); pk.pack_array(2); - pk.pack(std::string("WsdbSyncBlock")); + pk.pack(std::string("WsdbNoSuchCommand")); pk.pack_map(0); uint8_t* output = nullptr; size_t output_len = 0; wsdb_ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &output, &output_len); std::vector frame(output, output + output_len); wsdb_ipc_ffi_free(output); - EXPECT_EQ(error_of(decode_response(frame, "")), "unknown command: WsdbSyncBlock"); + EXPECT_EQ(error_of(decode_response(frame, "")), + "unknown command: WsdbNoSuchCommand"); EXPECT_EQ(error_of(call_ffi( "WsdbGetTreeInfo", wire::WsdbGetTreeInfo{ .treeId = 9, .revision = LATEST_COMMITTED }, "WsdbGetTreeInfoResponse")), - "unknown tree id 9"); + "Unknown tree id 9"); } -TEST(WsdbRef, RejectsWhatTheReferenceDoesNotModel) +TEST(WsdbRef, ForksAndHistoryOverTheWire) { Service service; + auto fork = ok(service.call( + "WsdbCreateFork", wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }, "WsdbCreateForkResponse")); + EXPECT_EQ(fork.forkId, 1); + ok(service.call( + "WsdbAppendLeaves", + wire::WsdbAppendLeaves{ .treeId = 1, .leaves = { bytes_of(fr(42)) }, .forkId = fork.forkId }, + "WsdbAppendLeavesResponse")); + auto on_fork = ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 1, .revision = { .forkId = fork.forkId, .blockNumber = LATEST, .includeUncommitted = true } }, + "WsdbGetTreeInfoResponse")); + EXPECT_EQ(on_fork.size, 1); + auto canonical = ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 1, .revision = LATEST_UNCOMMITTED }, + "WsdbGetTreeInfoResponse")); + EXPECT_EQ(canonical.size, 0); + EXPECT_EQ(error_of(service.call( "WsdbGetTreeInfo", wire::WsdbGetTreeInfo{ .treeId = 1, .revision = { .forkId = 3, .blockNumber = LATEST, .includeUncommitted = true } }, "WsdbGetTreeInfoResponse")), - "the reference world state has one fork (0), not 3"); - EXPECT_NE(error_of(service.call( - "WsdbGetTreeInfo", - wire::WsdbGetTreeInfo{ - .treeId = 1, .revision = { .forkId = 0, .blockNumber = 5, .includeUncommitted = true } }, - "WsdbGetTreeInfoResponse")) - .find("keeps no block history"), - std::string::npos); + "Fork not found"); + EXPECT_NE( + error_of(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 1, .revision = { .forkId = 0, .blockNumber = 5, .includeUncommitted = false } }, + "WsdbGetTreeInfoResponse")) + .find("latest block is 0"), + std::string::npos); EXPECT_EQ(error_of(service.call( "WsdbGetLeafValue", wire::WsdbGetLeafValue{ .treeId = 0, .revision = LATEST_UNCOMMITTED, .leafIndex = 0 }, "WsdbGetLeafValueResponse")), - "get_leaf_value is not supported for tree 0"); + "Unsupported tree type for get_leaf_value"); EXPECT_EQ(error_of(service.call( "WsdbRevertCheckpoint", wire::WsdbRevertCheckpoint{ .forkId = 0 }, "WsdbRevertCheckpointResponse")), - "no checkpoint to revert"); + "Cannot revert without a checkpoint"); + EXPECT_NE(error_of(service.call( + "WsdbCopyStores", + wire::WsdbCopyStores{ .dstPath = "/tmp/x", .compact = std::nullopt }, + "WsdbCopyStoresResponse")) + .find("not supported"), + std::string::npos); +} + +TEST(WsdbRef, InitGenesisRestartsAtTheGivenGenesis) +{ + Service service; + auto nullifier_root = [&]() { + auto info = ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = 0, .revision = LATEST_COMMITTED }, + "WsdbGetTreeInfoResponse")); + return fr_of(info.root); + }; + EXPECT_EQ(nullifier_root(), fr(GENESIS_NULLIFIER_TREE_ROOT)); + ok(service.call( + "WsdbInitGenesis", + wire::WsdbInitGenesis{ .prefilledPublicData = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(5)) } }, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }, + "WsdbInitGenesisResponse")); + EXPECT_NE(nullifier_root(), fr(GENESIS_NULLIFIER_TREE_ROOT)); + auto slot = ok(service.call( + "WsdbFindPublicDataLeafIndices", + wire::WsdbFindPublicDataLeafIndices{ .revision = LATEST_COMMITTED, + .leaves = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(0)) } }, + .startIndex = 0 }, + "WsdbFindPublicDataLeafIndicesResponse")); + EXPECT_EQ(slot.indices, (std::vector>{ 127 })); } TEST(WsdbRef, InsertsNullifiersWithWitnessesAndRejectsDuplicates) @@ -213,7 +270,7 @@ TEST(WsdbRef, InsertsNullifiersWithWitnessesAndRejectsDuplicates) { .nullifier = bytes_of(nullifier) } }, .forkId = 0 }, "WsdbSequentialInsertNullifierResponse"); - EXPECT_NE(error_of(duplicate).find("already exists"), std::string::npos); + EXPECT_NE(error_of(duplicate).find("is already present"), std::string::npos); auto low = ok(service.call( "WsdbFindLowLeaf", wire::WsdbFindLowLeaf{ .treeId = 0, .revision = LATEST_UNCOMMITTED, .key = bytes_of(fresh) }, @@ -254,3 +311,12 @@ TEST(WsdbRef, UpdateArchiveChecksTheBlockState) "WsdbGetTreeInfoResponse")); EXPECT_EQ(archive.size, 2); } + +// wasm does not find main() in gtest_main (see ecc_tests' secp256r1.test.cpp). +#ifdef __wasm__ +GTEST_API_ int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} +#endif diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp index b0e80fb99a3..eef30178b1c 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp @@ -1,13 +1,15 @@ #include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" -#include "barretenberg/common/log.hpp" #include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" +#include #include #include #include namespace bb::wsdb_ref { +using world_state::BatchInsertionResult; +using world_state::GenesisConfig; using world_state::MerkleTreeId; using world_state::ReferenceWorldState; using world_state::TreeState; @@ -17,9 +19,23 @@ using crypto::merkle_tree::IndexedLeaf; using crypto::merkle_tree::LeafUpdateWitnessData; using crypto::merkle_tree::NullifierLeafValue; using crypto::merkle_tree::PublicDataLeafValue; +using crypto::merkle_tree::SequentialInsertionResult; namespace { +/** Answers with what `work` returns, or with the error it throws. */ +template void serve(const Responder& respond, Work&& work) +{ + std::optional response; + try { + response = work(); + } catch (const std::exception& e) { + respond.error(e.what()); + return; + } + respond.ok(*response); +} + // Field elements cross the wire as 32 canonical bytes under whichever nominal alias the field declares. template FF to_fr(const W& w) { @@ -43,10 +59,10 @@ std::vector path_to_wire(const std::vector& path) return out; } -std::optional tree_id_of(uint32_t id) +MerkleTreeId tree_id_of(uint32_t id) { if (id > static_cast(MerkleTreeId::ARCHIVE)) { - return std::nullopt; + throw std::runtime_error("Unknown tree id " + std::to_string(id)); } return static_cast(id); } @@ -57,9 +73,11 @@ bool is_append_only(MerkleTreeId id) id == MerkleTreeId::ARCHIVE; } -bool is_indexed(MerkleTreeId id) +void require_append_only(MerkleTreeId id, const char* command) { - return id == MerkleTreeId::NULLIFIER_TREE || id == MerkleTreeId::PUBLIC_DATA_TREE; + if (!is_append_only(id)) { + throw std::runtime_error(std::string("Unsupported tree type for ") + command); + } } WorldStateRevision revision_of(const wire::WorldStateRevision& w) @@ -80,6 +98,46 @@ std::vector state_to_wire(const std::vector return out; } +std::vector state_from_wire(const std::vector& state) +{ + std::vector out; + out.reserve(state.size()); + for (const auto& tree : state) { + out.push_back({ .tree_id = tree_id_of(tree.treeId), .root = to_fr(tree.root), .size = tree.size }); + } + return out; +} + +NullifierLeafValue from_wire(const wire::NullifierLeafValue& leaf) +{ + return NullifierLeafValue(to_fr(leaf.nullifier)); +} + +PublicDataLeafValue from_wire(const wire::PublicDataLeafValue& leaf) +{ + return PublicDataLeafValue(to_fr(leaf.slot), to_fr(leaf.value)); +} + +template std::vector all_from_wire(const std::vector& in) +{ + std::vector out; + out.reserve(in.size()); + for (const auto& item : in) { + out.push_back(from_wire(item)); + } + return out; +} + +std::vector frs_from_wire(const std::vector& in) +{ + std::vector out; + out.reserve(in.size()); + for (const auto& item : in) { + out.push_back(to_fr(item)); + } + return out; +} + wire::NullifierLeafValue to_wire(const NullifierLeafValue& leaf) { return { .nullifier = from_fr(leaf.nullifier) }; @@ -120,54 +178,137 @@ template std::vector all_to_wire(const std::vec return out; } -std::vector> to_wire_indices(const std::vector>& indices) +template +WireResult batch_to_wire(const BatchInsertionResult& result) +{ + std::vector sorted; + sorted.reserve(result.sorted_leaves.size()); + for (const auto& [leaf, index] : result.sorted_leaves) { + sorted.push_back({ .leaf = to_wire(leaf), .index = index }); + } + return { .lowLeafWitnessData = all_to_wire(result.low_leaf_witness_data), + .sortedLeaves = std::move(sorted), + .subtreePath = path_to_wire(result.subtree_path) }; +} + +template +WireResult sequential_to_wire(const SequentialInsertionResult& result) +{ + return { .lowLeafWitnessData = all_to_wire(result.low_leaf_witness_data), + .insertionWitnessData = all_to_wire(result.insertion_witness_data) }; +} + +wire::WorldStateStatusSummary summary_to_wire(const world_state::StatusSummary& status) +{ + return { .unfinalizedBlockNumber = status.unfinalized_block_number, + .finalizedBlockNumber = status.finalized_block_number, + .oldestHistoricalBlock = status.oldest_historical_block, + .treesAreSynched = status.trees_are_synched }; +} + +/** A node's full status: the summary, each tree's meta, and empty store statistics (there are no stores). */ +wire::WorldStateStatusFull full_status(const ReferenceWorldState& world_state) +{ + const auto meta = world_state.tree_meta(); + auto tree_meta = [&](MerkleTreeId id) { + const auto& m = meta[static_cast(id)]; + return wire::TreeMeta{ .name = world_state::getMerkleTreeName(id), + .depth = m.depth, + .size = m.size, + .committedSize = m.committed_size, + .root = from_fr(m.root), + .initialSize = m.initial_size, + .initialRoot = from_fr(m.initial_root), + .oldestHistoricBlock = m.oldest_historic_block, + .unfinalizedBlockHeight = m.unfinalized_block_height, + .finalizedBlockHeight = m.finalized_block_height }; + }; + auto tree_stats = [](MerkleTreeId id) { + auto db = [&](const char* suffix) { + return wire::DBStats{ .name = world_state::getMerkleTreeName(id) + suffix, + .numDataItems = 0, + .totalUsedSize = 0 }; + }; + return wire::TreeDBStats{ .mapSize = 0, + .physicalFileSize = 0, + .blocksDBStats = db(" blocks"), + .nodesDBStats = db(" nodes"), + .leafPreimagesDBStats = db(" leaf preimages"), + .leafIndicesDBStats = db(" leaf indices"), + .blockIndicesDBStats = db(" block indices") }; + }; + return { .summary = summary_to_wire(world_state.status()), + .dbStats = { .noteHashTreeStats = tree_stats(MerkleTreeId::NOTE_HASH_TREE), + .messageTreeStats = tree_stats(MerkleTreeId::L1_TO_L2_MESSAGE_TREE), + .archiveTreeStats = tree_stats(MerkleTreeId::ARCHIVE), + .publicDataTreeStats = tree_stats(MerkleTreeId::PUBLIC_DATA_TREE), + .nullifierTreeStats = tree_stats(MerkleTreeId::NULLIFIER_TREE) }, + .meta = { .noteHashTreeMeta = tree_meta(MerkleTreeId::NOTE_HASH_TREE), + .messageTreeMeta = tree_meta(MerkleTreeId::L1_TO_L2_MESSAGE_TREE), + .archiveTreeMeta = tree_meta(MerkleTreeId::ARCHIVE), + .publicDataTreeMeta = tree_meta(MerkleTreeId::PUBLIC_DATA_TREE), + .nullifierTreeMeta = tree_meta(MerkleTreeId::NULLIFIER_TREE) } }; +} + +template +std::vector> find_paths(const ReferenceWorldState& world_state, + const WorldStateRevision& revision, + MerkleTreeId tree_id, + const std::vector& keys) +{ + std::vector> paths; + paths.reserve(keys.size()); + for (const auto& key : keys) { + auto index = world_state.find_leaf_index(revision, tree_id, key, 0); + if (!index.has_value()) { + paths.emplace_back(std::nullopt); + continue; + } + paths.emplace_back(wire::SiblingPathAndIndex{ + .index = *index, .path = path_to_wire(world_state.get_sibling_path(revision, tree_id, *index)) }); + } + return paths; +} + +template +std::vector> find_indices(const ReferenceWorldState& world_state, + const WorldStateRevision& revision, + MerkleTreeId tree_id, + const std::vector& keys, + uint64_t start) { + std::vector> indices; + indices.reserve(keys.size()); + for (const auto& key : keys) { + indices.push_back(world_state.find_leaf_index(revision, tree_id, key, start)); + } return indices; } /** - * Resolve the tree id and revision of a read, or answer the request with why it cannot be served. - * `allowed` says which trees the command reads. + * An indexed leaf for a leaf-value read, which a node answers with null on any failure (an unknown block + * included) except an unknown fork. */ -template -std::optional check_read(uint32_t tree_id, - const WorldStateRevision& revision, - Allowed allowed, - const char* what, - const Responder& respond) -{ - auto id = tree_id_of(tree_id); - if (!id.has_value()) { - respond.error("unknown tree id " + std::to_string(tree_id)); - return std::nullopt; - } - if (!allowed(*id)) { - respond.error(std::string(what) + " is not supported for tree " + std::to_string(tree_id)); - return std::nullopt; - } - if (auto problem = ReferenceWorldState::check_revision(revision)) { - respond.error(*problem); - return std::nullopt; - } - return id; -} - -template bool check_revision(const WorldStateRevision& revision, const Responder& respond) +template auto leaf_or_null(const ReferenceWorldState& world_state, uint64_t fork_id, Read read) { - if (auto problem = ReferenceWorldState::check_revision(revision)) { - respond.error(*problem); - return false; + if (!world_state.has_fork(fork_id)) { + throw std::runtime_error("Fork not found"); + } + try { + return read(); + } catch (const std::exception&) { + return decltype(read())(std::nullopt); } - return true; } -template bool check_fork(uint64_t fork_id, const Responder& respond) +template std::vector keys_of(const std::vector& leaves) { - if (auto problem = ReferenceWorldState::check_fork(fork_id)) { - respond.error(*problem); - return false; + std::vector keys; + keys.reserve(leaves.size()); + for (const auto& leaf : leaves) { + keys.push_back(leaf.get_key()); } - return true; + return keys; } } // namespace @@ -180,181 +321,202 @@ void handle_get_tree_info(WsdbRefContext& ctx, wire::WsdbGetTreeInfo&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - auto id = check_read(cmd.treeId, revision, [](MerkleTreeId) { return true; }, "get_tree_info", respond); - if (!id) { - return; - } - auto info = ctx.world_state.get_tree_info(revision, *id); - respond.ok({ .treeId = cmd.treeId, + serve(respond, [&]() -> wire::WsdbGetTreeInfoResponse { + auto id = tree_id_of(cmd.treeId); + auto info = ctx.world_state.get_tree_info(revision_of(cmd.revision), id); + return { .treeId = cmd.treeId, .root = from_fr(info.root), .size = info.size, - .depth = ReferenceWorldState::tree_height(*id) }); + .depth = ReferenceWorldState::tree_height(id) }; + }); } void handle_get_state_reference(WsdbRefContext& ctx, wire::WsdbGetStateReference&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - if (!check_revision(revision, respond)) { - return; - } - respond.ok({ .state = state_to_wire(ctx.world_state.get_state_reference(revision)) }); + serve(respond, [&]() -> wire::WsdbGetStateReferenceResponse { + return { .state = state_to_wire(ctx.world_state.get_state_reference(revision_of(cmd.revision))) }; + }); } void handle_get_initial_state_reference(WsdbRefContext& ctx, wire::WsdbGetInitialStateReference&& /*cmd*/, Responder respond) { - respond.ok({ .state = state_to_wire(ctx.world_state.get_initial_state_reference()) }); + serve(respond, [&]() -> wire::WsdbGetInitialStateReferenceResponse { + return { .state = state_to_wire(ctx.world_state.get_initial_state_reference()) }; + }); } // --------------------------------------------------------------------------- -// Leaf reads +// Leaves // --------------------------------------------------------------------------- void handle_get_leaf_value(WsdbRefContext& ctx, wire::WsdbGetLeafValue&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - auto id = check_read(cmd.treeId, revision, is_append_only, "get_leaf_value", respond); - if (!id) { - return; - } - auto leaf = ctx.world_state.get_leaf_value(revision, *id, cmd.leafIndex); - respond.ok({ .value = leaf ? std::optional(from_fr(*leaf)) : std::nullopt }); + serve(respond, [&]() -> wire::WsdbGetLeafValueResponse { + auto id = tree_id_of(cmd.treeId); + require_append_only(id, "get_leaf_value"); + auto value = ctx.world_state.get_leaf_value(revision_of(cmd.revision), id, cmd.leafIndex); + return { .value = value.has_value() ? std::optional(from_fr(*value)) : std::nullopt }; + }); } void handle_get_public_data_leaf_value(WsdbRefContext& ctx, wire::WsdbGetPublicDataLeafValue&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - if (!check_revision(revision, respond)) { - return; - } - auto preimage = ctx.world_state.get_public_data_preimage(revision, cmd.leafIndex); - respond.ok({ .value = preimage ? std::optional(to_wire(preimage->leaf)) : std::nullopt }); + serve(respond, [&]() -> wire::WsdbGetPublicDataLeafValueResponse { + auto leaf = leaf_or_null(ctx.world_state, cmd.revision.forkId, [&] { + return ctx.world_state.get_public_data_preimage(revision_of(cmd.revision), cmd.leafIndex); + }); + return { .value = leaf.has_value() ? std::optional(to_wire(leaf->leaf)) : std::nullopt }; + }); } void handle_get_nullifier_leaf_value(WsdbRefContext& ctx, wire::WsdbGetNullifierLeafValue&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - if (!check_revision(revision, respond)) { - return; - } - auto preimage = ctx.world_state.get_nullifier_preimage(revision, cmd.leafIndex); - respond.ok({ .value = preimage ? std::optional(to_wire(preimage->leaf)) : std::nullopt }); + serve(respond, [&]() -> wire::WsdbGetNullifierLeafValueResponse { + auto leaf = leaf_or_null(ctx.world_state, cmd.revision.forkId, [&] { + return ctx.world_state.get_nullifier_preimage(revision_of(cmd.revision), cmd.leafIndex); + }); + return { .value = leaf.has_value() ? std::optional(to_wire(leaf->leaf)) : std::nullopt }; + }); } void handle_get_public_data_leaf_preimage(WsdbRefContext& ctx, wire::WsdbGetPublicDataLeafPreimage&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - if (!check_revision(revision, respond)) { - return; - } - auto preimage = ctx.world_state.get_public_data_preimage(revision, cmd.leafIndex); - respond.ok({ .preimage = preimage ? std::optional(to_wire(*preimage)) : std::nullopt }); + serve(respond, [&]() -> wire::WsdbGetPublicDataLeafPreimageResponse { + // As on a node, a slot past the tree's size is an error here, while the leaf-value read of it answers null. + auto leaf = ctx.world_state.get_public_data_preimage(revision_of(cmd.revision), cmd.leafIndex); + if (!leaf.has_value()) { + throw std::runtime_error("Failed to find indexed leaf: no leaf at index " + std::to_string(cmd.leafIndex)); + } + return { .preimage = to_wire(*leaf) }; + }); } void handle_get_nullifier_leaf_preimage(WsdbRefContext& ctx, wire::WsdbGetNullifierLeafPreimage&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - if (!check_revision(revision, respond)) { - return; - } - auto preimage = ctx.world_state.get_nullifier_preimage(revision, cmd.leafIndex); - respond.ok({ .preimage = preimage ? std::optional(to_wire(*preimage)) : std::nullopt }); + serve(respond, [&]() -> wire::WsdbGetNullifierLeafPreimageResponse { + // As on a node, a slot past the tree's size is an error here, while the leaf-value read of it answers null. + auto leaf = ctx.world_state.get_nullifier_preimage(revision_of(cmd.revision), cmd.leafIndex); + if (!leaf.has_value()) { + throw std::runtime_error("Failed to find indexed leaf: no leaf at index " + std::to_string(cmd.leafIndex)); + } + return { .preimage = to_wire(*leaf) }; + }); } void handle_get_sibling_path(WsdbRefContext& ctx, wire::WsdbGetSiblingPath&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - auto id = check_read(cmd.treeId, revision, [](MerkleTreeId) { return true; }, "get_sibling_path", respond); - if (!id) { - return; - } - if (cmd.leafIndex >= (uint64_t(1) << ReferenceWorldState::tree_height(*id))) { - respond.error("leaf index " + std::to_string(cmd.leafIndex) + " is outside tree " + std::to_string(cmd.treeId)); - return; - } - respond.ok({ .path = path_to_wire(ctx.world_state.get_sibling_path(revision, *id, cmd.leafIndex)) }); + serve(respond, [&]() -> wire::WsdbGetSiblingPathResponse { + return { .path = path_to_wire(ctx.world_state.get_sibling_path( + revision_of(cmd.revision), tree_id_of(cmd.treeId), cmd.leafIndex)) }; + }); +} + +void handle_get_block_numbers_for_leaf_indices(WsdbRefContext& ctx, + wire::WsdbGetBlockNumbersForLeafIndices&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbGetBlockNumbersForLeafIndicesResponse { + return { .blockNumbers = ctx.world_state.get_block_numbers_for_leaf_indices( + revision_of(cmd.revision), tree_id_of(cmd.treeId), cmd.leafIndices) }; + }); } // --------------------------------------------------------------------------- -// Lookups +// Searches // --------------------------------------------------------------------------- void handle_find_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindLeafIndices&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - auto id = check_read(cmd.treeId, revision, is_append_only, "find_leaf_indices", respond); - if (!id) { - return; - } - std::vector> indices; - for (const auto& leaf : cmd.leaves) { - indices.push_back(ctx.world_state.find_leaf_index(revision, *id, to_fr(leaf), cmd.startIndex)); - } - respond.ok({ .indices = to_wire_indices(indices) }); + serve(respond, [&]() -> wire::WsdbFindLeafIndicesResponse { + auto id = tree_id_of(cmd.treeId); + require_append_only(id, "find_leaf_indices"); + return { .indices = find_indices( + ctx.world_state, revision_of(cmd.revision), id, frs_from_wire(cmd.leaves), cmd.startIndex) }; + }); } void handle_find_public_data_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindPublicDataLeafIndices&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - if (!check_revision(revision, respond)) { - return; - } - // Public data is keyed by slot: the leaf at a slot is found whatever value it now holds. - std::vector> indices; - for (const auto& leaf : cmd.leaves) { - indices.push_back(ctx.world_state.find_indexed_leaf_index( - revision, MerkleTreeId::PUBLIC_DATA_TREE, to_fr(leaf.slot), cmd.startIndex)); - } - respond.ok({ .indices = to_wire_indices(indices) }); + serve(respond, [&]() -> wire::WsdbFindPublicDataLeafIndicesResponse { + auto keys = keys_of(all_from_wire(cmd.leaves)); + return { + .indices = find_indices( + ctx.world_state, revision_of(cmd.revision), MerkleTreeId::PUBLIC_DATA_TREE, keys, cmd.startIndex) + }; + }); } void handle_find_nullifier_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindNullifierLeafIndices&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - if (!check_revision(revision, respond)) { - return; - } - std::vector> indices; - for (const auto& leaf : cmd.leaves) { - indices.push_back(ctx.world_state.find_indexed_leaf_index( - revision, MerkleTreeId::NULLIFIER_TREE, to_fr(leaf.nullifier), cmd.startIndex)); - } - respond.ok({ .indices = to_wire_indices(indices) }); + serve(respond, [&]() -> wire::WsdbFindNullifierLeafIndicesResponse { + auto keys = keys_of(all_from_wire(cmd.leaves)); + return { .indices = find_indices( + ctx.world_state, revision_of(cmd.revision), MerkleTreeId::NULLIFIER_TREE, keys, cmd.startIndex) }; + }); } void handle_find_low_leaf(WsdbRefContext& ctx, wire::WsdbFindLowLeaf&& cmd, Responder respond) { - auto revision = revision_of(cmd.revision); - auto id = check_read(cmd.treeId, revision, is_indexed, "find_low_leaf", respond); - if (!id) { - return; - } - auto low = ctx.world_state.find_low_leaf(revision, *id, to_fr(cmd.key)); - respond.ok({ .alreadyPresent = low.is_already_present, .index = low.index }); + serve(respond, [&]() -> wire::WsdbFindLowLeafResponse { + auto low = ctx.world_state.find_low_leaf(revision_of(cmd.revision), tree_id_of(cmd.treeId), to_fr(cmd.key)); + return { .alreadyPresent = low.is_already_present, .index = low.index }; + }); +} + +void handle_find_sibling_paths(WsdbRefContext& ctx, + wire::WsdbFindSiblingPaths&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindSiblingPathsResponse { + auto id = tree_id_of(cmd.treeId); + require_append_only(id, "find_sibling_paths"); + return { .paths = find_paths(ctx.world_state, revision_of(cmd.revision), id, frs_from_wire(cmd.leaves)) }; + }); +} + +void handle_find_public_data_sibling_paths(WsdbRefContext& ctx, + wire::WsdbFindPublicDataSiblingPaths&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindPublicDataSiblingPathsResponse { + auto keys = keys_of(all_from_wire(cmd.leaves)); + return { .paths = + find_paths(ctx.world_state, revision_of(cmd.revision), MerkleTreeId::PUBLIC_DATA_TREE, keys) }; + }); +} + +void handle_find_nullifier_sibling_paths(WsdbRefContext& ctx, + wire::WsdbFindNullifierSiblingPaths&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFindNullifierSiblingPathsResponse { + auto keys = keys_of(all_from_wire(cmd.leaves)); + return { .paths = find_paths(ctx.world_state, revision_of(cmd.revision), MerkleTreeId::NULLIFIER_TREE, keys) }; + }); } // --------------------------------------------------------------------------- @@ -365,99 +527,198 @@ void handle_append_leaves(WsdbRefContext& ctx, wire::WsdbAppendLeaves&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - auto id = tree_id_of(cmd.treeId); - if (!id || !is_append_only(*id)) { - respond.error("append_leaves is not supported for tree " + std::to_string(cmd.treeId)); - return; - } - std::vector leaves; - leaves.reserve(cmd.leaves.size()); - for (const auto& leaf : cmd.leaves) { - leaves.push_back(to_fr(leaf)); - } - ctx.world_state.append_leaves(*id, leaves); - respond.ok({}); + serve(respond, [&]() -> wire::WsdbAppendLeavesResponse { + auto leaves = frs_from_wire(cmd.leaves); + ctx.world_state.append_leaves(cmd.forkId, tree_id_of(cmd.treeId), leaves); + return {}; + }); +} + +void handle_append_public_data_leaves(WsdbRefContext& ctx, + wire::WsdbAppendPublicDataLeaves&& cmd, + Responder respond) +{ + // As on a node: a batch insertion without its witnesses. + serve(respond, [&]() -> wire::WsdbAppendPublicDataLeavesResponse { + auto leaves = all_from_wire(cmd.leaves); + ctx.world_state.batch_insert_public_data(cmd.forkId, leaves, 0); + return {}; + }); +} + +void handle_append_nullifier_leaves(WsdbRefContext& ctx, + wire::WsdbAppendNullifierLeaves&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbAppendNullifierLeavesResponse { + auto leaves = all_from_wire(cmd.leaves); + ctx.world_state.batch_insert_nullifiers(cmd.forkId, leaves, 0); + return {}; + }); +} + +void handle_batch_insert_public_data(WsdbRefContext& ctx, + wire::WsdbBatchInsertPublicData&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbBatchInsertPublicDataResponse { + auto leaves = all_from_wire(cmd.leaves); + auto result = ctx.world_state.batch_insert_public_data(cmd.forkId, leaves, cmd.subtreeDepth); + return { .result = batch_to_wire(result) }; + }); +} + +void handle_batch_insert_nullifier(WsdbRefContext& ctx, + wire::WsdbBatchInsertNullifier&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbBatchInsertNullifierResponse { + auto leaves = all_from_wire(cmd.leaves); + auto result = ctx.world_state.batch_insert_nullifiers(cmd.forkId, leaves, cmd.subtreeDepth); + return { .result = batch_to_wire(result) }; + }); } void handle_sequential_insert_public_data(WsdbRefContext& ctx, wire::WsdbSequentialInsertPublicData&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - wire::SequentialInsertionResultPublicData result; - for (const auto& leaf : cmd.leaves) { - auto inserted = ctx.world_state.insert_public_data(PublicDataLeafValue(to_fr(leaf.slot), to_fr(leaf.value))); - for (const auto& w : inserted.low_leaf_witness_data) { - result.lowLeafWitnessData.push_back(to_wire(w)); - } - for (const auto& w : inserted.insertion_witness_data) { - result.insertionWitnessData.push_back(to_wire(w)); - } - } - respond.ok({ .result = std::move(result) }); + serve(respond, [&]() -> wire::WsdbSequentialInsertPublicDataResponse { + auto leaves = all_from_wire(cmd.leaves); + auto result = ctx.world_state.sequential_insert_public_data(cmd.forkId, leaves); + return { + .result = + sequential_to_wire( + result) + }; + }); } void handle_sequential_insert_nullifier(WsdbRefContext& ctx, wire::WsdbSequentialInsertNullifier&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - // Validate the whole batch first, so a rejected request changes nothing. - std::vector nullifiers; - for (const auto& leaf : cmd.leaves) { - FF nullifier = to_fr(leaf.nullifier); - bool repeated = ctx.world_state.has_nullifier(nullifier); - for (const auto& earlier : nullifiers) { - repeated = repeated || earlier == nullifier; - } - if (repeated) { - respond.error("nullifier " + format(nullifier) + " already exists"); - return; - } - nullifiers.push_back(nullifier); - } - wire::SequentialInsertionResultNullifier result; - for (const auto& nullifier : nullifiers) { - auto inserted = ctx.world_state.insert_nullifier(NullifierLeafValue(nullifier)); - for (const auto& w : inserted.low_leaf_witness_data) { - result.lowLeafWitnessData.push_back(to_wire(w)); - } - for (const auto& w : inserted.insertion_witness_data) { - result.insertionWitnessData.push_back(to_wire(w)); - } - } - respond.ok({ .result = std::move(result) }); + serve(respond, [&]() -> wire::WsdbSequentialInsertNullifierResponse { + auto leaves = all_from_wire(cmd.leaves); + auto result = ctx.world_state.sequential_insert_nullifiers(cmd.forkId, leaves); + return { .result = + sequential_to_wire( + result) }; + }); } void handle_update_archive(WsdbRefContext& ctx, wire::WsdbUpdateArchive&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - std::vector state; - for (const auto& tree : cmd.blockStateRef) { - auto id = tree_id_of(tree.treeId); - if (!id) { - respond.error("unknown tree id " + std::to_string(tree.treeId)); - return; - } - state.push_back({ *id, to_fr(tree.root), tree.size }); - } - if (!ctx.world_state.state_matches(state)) { - respond.error("Can't update archive tree: Block state does not match world state"); - return; - } - ctx.world_state.update_archive(to_fr(cmd.blockHeaderHash)); - respond.ok({}); + serve(respond, [&]() -> wire::WsdbUpdateArchiveResponse { + ctx.world_state.update_archive(cmd.forkId, state_from_wire(cmd.blockStateRef), to_fr(cmd.blockHeaderHash)); + return {}; + }); +} + +// --------------------------------------------------------------------------- +// The canonical chain +// --------------------------------------------------------------------------- + +void handle_commit(WsdbRefContext& ctx, wire::WsdbCommit&& /*cmd*/, Responder respond) +{ + serve(respond, [&]() -> wire::WsdbCommitResponse { + ctx.world_state.commit(); + return { .status = full_status(ctx.world_state) }; + }); +} + +void handle_rollback(WsdbRefContext& ctx, wire::WsdbRollback&& /*cmd*/, Responder respond) +{ + serve(respond, [&]() -> wire::WsdbRollbackResponse { + ctx.world_state.rollback(); + return {}; + }); +} + +void handle_sync_block(WsdbRefContext& ctx, wire::WsdbSyncBlock&& cmd, Responder respond) +{ + serve(respond, [&]() -> wire::WsdbSyncBlockResponse { + auto note_hashes = frs_from_wire(cmd.paddedNoteHashes); + auto messages = frs_from_wire(cmd.paddedL1ToL2Messages); + auto nullifiers = all_from_wire(cmd.paddedNullifiers); + auto public_data = all_from_wire(cmd.publicDataWrites); + auto optional_fr = [](const std::optional& w) { + return w.has_value() ? std::optional(to_fr(*w)) : std::nullopt; + }; + ctx.world_state.sync_block(state_from_wire(cmd.blockStateRef), + to_fr(cmd.blockHeaderHash), + note_hashes, + messages, + nullifiers, + public_data, + optional_fr(cmd.expectedArchiveRoot), + optional_fr(cmd.expectedPreviousArchiveRoot)); + return { .status = full_status(ctx.world_state) }; + }); +} + +void handle_create_fork(WsdbRefContext& ctx, + wire::WsdbCreateFork&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbCreateForkResponse { + auto block = cmd.latest ? std::nullopt : std::optional(cmd.blockNumber); + return { .forkId = ctx.world_state.create_fork(block) }; + }); +} + +void handle_delete_fork(WsdbRefContext& ctx, + wire::WsdbDeleteFork&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbDeleteForkResponse { + ctx.world_state.delete_fork(cmd.forkId); + return {}; + }); +} + +void handle_finalize_blocks(WsdbRefContext& ctx, + wire::WsdbFinalizeBlocks&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbFinalizeBlocksResponse { + ctx.world_state.finalize_blocks(cmd.toBlockNumber); + return { .status = summary_to_wire(ctx.world_state.status()) }; + }); +} + +void handle_unwind_blocks(WsdbRefContext& ctx, + wire::WsdbUnwindBlocks&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbUnwindBlocksResponse { + ctx.world_state.unwind_blocks(cmd.toBlockNumber); + return { .status = full_status(ctx.world_state) }; + }); +} + +void handle_remove_historical_blocks(WsdbRefContext& ctx, + wire::WsdbRemoveHistoricalBlocks&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbRemoveHistoricalBlocksResponse { + ctx.world_state.remove_historical_blocks(cmd.toBlockNumber); + return { .status = full_status(ctx.world_state) }; + }); +} + +void handle_get_status(WsdbRefContext& ctx, + wire::WsdbGetStatus&& /*cmd*/, + Responder respond) +{ + serve(respond, + [&]() -> wire::WsdbGetStatusResponse { return { .status = summary_to_wire(ctx.world_state.status()) }; }); } // --------------------------------------------------------------------------- @@ -468,67 +729,82 @@ void handle_create_checkpoint(WsdbRefContext& ctx, wire::WsdbCreateCheckpoint&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - ctx.world_state.create_checkpoint(); - respond.ok({}); + serve(respond, [&]() -> wire::WsdbCreateCheckpointResponse { + ctx.world_state.create_checkpoint(cmd.forkId); + return {}; + }); } void handle_commit_checkpoint(WsdbRefContext& ctx, wire::WsdbCommitCheckpoint&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - if (!ctx.world_state.has_checkpoint()) { - respond.error("no checkpoint to commit"); - return; - } - ctx.world_state.commit_checkpoint(); - respond.ok({}); + serve(respond, [&]() -> wire::WsdbCommitCheckpointResponse { + ctx.world_state.commit_checkpoint(cmd.forkId); + return {}; + }); } void handle_revert_checkpoint(WsdbRefContext& ctx, wire::WsdbRevertCheckpoint&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - if (!ctx.world_state.has_checkpoint()) { - respond.error("no checkpoint to revert"); - return; - } - ctx.world_state.revert_checkpoint(); - respond.ok({}); + serve(respond, [&]() -> wire::WsdbRevertCheckpointResponse { + ctx.world_state.revert_checkpoint(cmd.forkId); + return {}; + }); } void handle_commit_all_checkpoints(WsdbRefContext& ctx, wire::WsdbCommitAllCheckpoints&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - ctx.world_state.commit_all_checkpoints(); - respond.ok({}); + serve(respond, [&]() -> wire::WsdbCommitAllCheckpointsResponse { + ctx.world_state.commit_all_checkpoints(cmd.forkId); + return {}; + }); } void handle_revert_all_checkpoints(WsdbRefContext& ctx, wire::WsdbRevertAllCheckpoints&& cmd, Responder respond) { - if (!check_fork(cmd.forkId, respond)) { - return; - } - ctx.world_state.revert_all_checkpoints(); - respond.ok({}); + serve(respond, [&]() -> wire::WsdbRevertAllCheckpointsResponse { + ctx.world_state.revert_all_checkpoints(cmd.forkId); + return {}; + }); +} + +// --------------------------------------------------------------------------- +// Stores and genesis +// --------------------------------------------------------------------------- + +void handle_copy_stores(WsdbRefContext& /*ctx*/, + wire::WsdbCopyStores&& /*cmd*/, + Responder respond) +{ + respond.error("CopyStores is not supported: wsdb-ref keeps its state in memory, with no stores to copy"); +} + +void handle_init_genesis(WsdbRefContext& ctx, + wire::WsdbInitGenesis&& cmd, + Responder respond) +{ + serve(respond, [&]() -> wire::WsdbInitGenesisResponse { + GenesisConfig genesis; + genesis.prefilled_public_data = all_from_wire(cmd.prefilledPublicData); + for (const auto& nullifier : cmd.prefilledNullifiers) { + genesis.prefilled_nullifiers.emplace_back(to_fr(nullifier)); + } + genesis.genesis_timestamp = cmd.genesisTimestamp; + genesis.header_generator_point = FF(cmd.initialHeaderGeneratorPoint); + ctx.world_state.reset(genesis); + return {}; + }); } // The in-process FFI entry's context (generated/wsdb_ffi.hpp): one reference world state per loaded -// module, at the canonical genesis, shared by every call as over one transport connection. +// module, at the canonical genesis until InitGenesis, shared by every call as over one transport connection. AsyncDispatchHandler& ipc_ffi_dispatcher() { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp index 0e72637a103..91b36d4a2a8 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp @@ -1,13 +1,14 @@ #pragma once /** * @file wsdb_ref_handlers.hpp - * @brief The wsdb base contract (wsdb_base_schema.jsonc) served by the in-memory reference world state. + * @brief The node world state's contract (native-packages/wsdb/wsdb_schema.jsonc) served by the in-memory + * reference world state. * * The generated dispatch (generated/wsdb_dispatch.hpp) declares template handle_; the * overloads below, for Ctx = WsdbRefContext, are preferred at its instantiation in - * make_wsdb_handler. Every handler answers synchronously. A request the reference cannot - * serve (another fork, a historical block, an unsupported tree, a duplicate nullifier) is answered with - * an error, never an exception, so a wasm build (no exceptions) keeps running after it. + * make_wsdb_handler. Every handler answers synchronously, and a request that fails is + * answered with its error. CopyStores, which copies a node's LMDB stores, is the one command answered + * with an error unconditionally. */ #include "barretenberg/world_state_reference/reference_world_state.hpp" #include "barretenberg/wsdb_ref/generated/wsdb_dispatch.hpp" @@ -15,76 +16,54 @@ namespace bb::wsdb_ref { -/** @brief The state a wsdb-ref service runs over: one reference world state at the canonical genesis. */ +/** @brief The state a wsdb-ref service runs over: one reference world state, at the canonical genesis until + * InitGenesis. */ struct WsdbRefContext { world_state::ReferenceWorldState world_state; }; -void handle_get_tree_info(WsdbRefContext& ctx, - wire::WsdbGetTreeInfo&& cmd, - Responder respond); -void handle_get_state_reference(WsdbRefContext& ctx, - wire::WsdbGetStateReference&& cmd, - Responder respond); -void handle_get_initial_state_reference(WsdbRefContext& ctx, - wire::WsdbGetInitialStateReference&& cmd, - Responder respond); -void handle_get_leaf_value(WsdbRefContext& ctx, - wire::WsdbGetLeafValue&& cmd, - Responder respond); -void handle_get_public_data_leaf_value(WsdbRefContext& ctx, - wire::WsdbGetPublicDataLeafValue&& cmd, - Responder respond); -void handle_get_nullifier_leaf_value(WsdbRefContext& ctx, - wire::WsdbGetNullifierLeafValue&& cmd, - Responder respond); -void handle_get_public_data_leaf_preimage(WsdbRefContext& ctx, - wire::WsdbGetPublicDataLeafPreimage&& cmd, - Responder respond); -void handle_get_nullifier_leaf_preimage(WsdbRefContext& ctx, - wire::WsdbGetNullifierLeafPreimage&& cmd, - Responder respond); -void handle_get_sibling_path(WsdbRefContext& ctx, - wire::WsdbGetSiblingPath&& cmd, - Responder respond); -void handle_find_leaf_indices(WsdbRefContext& ctx, - wire::WsdbFindLeafIndices&& cmd, - Responder respond); -void handle_find_public_data_leaf_indices(WsdbRefContext& ctx, - wire::WsdbFindPublicDataLeafIndices&& cmd, - Responder respond); -void handle_find_nullifier_leaf_indices(WsdbRefContext& ctx, - wire::WsdbFindNullifierLeafIndices&& cmd, - Responder respond); -void handle_find_low_leaf(WsdbRefContext& ctx, - wire::WsdbFindLowLeaf&& cmd, - Responder respond); -void handle_append_leaves(WsdbRefContext& ctx, - wire::WsdbAppendLeaves&& cmd, - Responder respond); -void handle_sequential_insert_public_data(WsdbRefContext& ctx, - wire::WsdbSequentialInsertPublicData&& cmd, - Responder respond); -void handle_sequential_insert_nullifier(WsdbRefContext& ctx, - wire::WsdbSequentialInsertNullifier&& cmd, - Responder respond); -void handle_update_archive(WsdbRefContext& ctx, - wire::WsdbUpdateArchive&& cmd, - Responder respond); -void handle_create_checkpoint(WsdbRefContext& ctx, - wire::WsdbCreateCheckpoint&& cmd, - Responder respond); -void handle_commit_checkpoint(WsdbRefContext& ctx, - wire::WsdbCommitCheckpoint&& cmd, - Responder respond); -void handle_revert_checkpoint(WsdbRefContext& ctx, - wire::WsdbRevertCheckpoint&& cmd, - Responder respond); -void handle_commit_all_checkpoints(WsdbRefContext& ctx, - wire::WsdbCommitAllCheckpoints&& cmd, - Responder respond); -void handle_revert_all_checkpoints(WsdbRefContext& ctx, - wire::WsdbRevertAllCheckpoints&& cmd, - Responder respond); +// clang-format off +void handle_get_tree_info(WsdbRefContext& ctx, wire::WsdbGetTreeInfo&& cmd, Responder respond); +void handle_get_state_reference(WsdbRefContext& ctx, wire::WsdbGetStateReference&& cmd, Responder respond); +void handle_get_initial_state_reference(WsdbRefContext& ctx, wire::WsdbGetInitialStateReference&& cmd, Responder respond); +void handle_get_leaf_value(WsdbRefContext& ctx, wire::WsdbGetLeafValue&& cmd, Responder respond); +void handle_get_public_data_leaf_value(WsdbRefContext& ctx, wire::WsdbGetPublicDataLeafValue&& cmd, Responder respond); +void handle_get_nullifier_leaf_value(WsdbRefContext& ctx, wire::WsdbGetNullifierLeafValue&& cmd, Responder respond); +void handle_get_public_data_leaf_preimage(WsdbRefContext& ctx, wire::WsdbGetPublicDataLeafPreimage&& cmd, Responder respond); +void handle_get_nullifier_leaf_preimage(WsdbRefContext& ctx, wire::WsdbGetNullifierLeafPreimage&& cmd, Responder respond); +void handle_get_sibling_path(WsdbRefContext& ctx, wire::WsdbGetSiblingPath&& cmd, Responder respond); +void handle_get_block_numbers_for_leaf_indices(WsdbRefContext& ctx, wire::WsdbGetBlockNumbersForLeafIndices&& cmd, Responder respond); +void handle_find_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindLeafIndices&& cmd, Responder respond); +void handle_find_public_data_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindPublicDataLeafIndices&& cmd, Responder respond); +void handle_find_nullifier_leaf_indices(WsdbRefContext& ctx, wire::WsdbFindNullifierLeafIndices&& cmd, Responder respond); +void handle_find_low_leaf(WsdbRefContext& ctx, wire::WsdbFindLowLeaf&& cmd, Responder respond); +void handle_find_sibling_paths(WsdbRefContext& ctx, wire::WsdbFindSiblingPaths&& cmd, Responder respond); +void handle_find_public_data_sibling_paths(WsdbRefContext& ctx, wire::WsdbFindPublicDataSiblingPaths&& cmd, Responder respond); +void handle_find_nullifier_sibling_paths(WsdbRefContext& ctx, wire::WsdbFindNullifierSiblingPaths&& cmd, Responder respond); +void handle_append_leaves(WsdbRefContext& ctx, wire::WsdbAppendLeaves&& cmd, Responder respond); +void handle_append_public_data_leaves(WsdbRefContext& ctx, wire::WsdbAppendPublicDataLeaves&& cmd, Responder respond); +void handle_append_nullifier_leaves(WsdbRefContext& ctx, wire::WsdbAppendNullifierLeaves&& cmd, Responder respond); +void handle_batch_insert_public_data(WsdbRefContext& ctx, wire::WsdbBatchInsertPublicData&& cmd, Responder respond); +void handle_batch_insert_nullifier(WsdbRefContext& ctx, wire::WsdbBatchInsertNullifier&& cmd, Responder respond); +void handle_sequential_insert_public_data(WsdbRefContext& ctx, wire::WsdbSequentialInsertPublicData&& cmd, Responder respond); +void handle_sequential_insert_nullifier(WsdbRefContext& ctx, wire::WsdbSequentialInsertNullifier&& cmd, Responder respond); +void handle_update_archive(WsdbRefContext& ctx, wire::WsdbUpdateArchive&& cmd, Responder respond); +void handle_commit(WsdbRefContext& ctx, wire::WsdbCommit&& cmd, Responder respond); +void handle_rollback(WsdbRefContext& ctx, wire::WsdbRollback&& cmd, Responder respond); +void handle_sync_block(WsdbRefContext& ctx, wire::WsdbSyncBlock&& cmd, Responder respond); +void handle_create_fork(WsdbRefContext& ctx, wire::WsdbCreateFork&& cmd, Responder respond); +void handle_delete_fork(WsdbRefContext& ctx, wire::WsdbDeleteFork&& cmd, Responder respond); +void handle_finalize_blocks(WsdbRefContext& ctx, wire::WsdbFinalizeBlocks&& cmd, Responder respond); +void handle_unwind_blocks(WsdbRefContext& ctx, wire::WsdbUnwindBlocks&& cmd, Responder respond); +void handle_remove_historical_blocks(WsdbRefContext& ctx, wire::WsdbRemoveHistoricalBlocks&& cmd, Responder respond); +void handle_get_status(WsdbRefContext& ctx, wire::WsdbGetStatus&& cmd, Responder respond); +void handle_create_checkpoint(WsdbRefContext& ctx, wire::WsdbCreateCheckpoint&& cmd, Responder respond); +void handle_commit_checkpoint(WsdbRefContext& ctx, wire::WsdbCommitCheckpoint&& cmd, Responder respond); +void handle_revert_checkpoint(WsdbRefContext& ctx, wire::WsdbRevertCheckpoint&& cmd, Responder respond); +void handle_commit_all_checkpoints(WsdbRefContext& ctx, wire::WsdbCommitAllCheckpoints&& cmd, Responder respond); +void handle_revert_all_checkpoints(WsdbRefContext& ctx, wire::WsdbRevertAllCheckpoints&& cmd, Responder respond); +void handle_copy_stores(WsdbRefContext& ctx, wire::WsdbCopyStores&& cmd, Responder respond); +void handle_init_genesis(WsdbRefContext& ctx, wire::WsdbInitGenesis&& cmd, Responder respond); +// clang-format on } // namespace bb::wsdb_ref diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp new file mode 100644 index 00000000000..5da7c4f98ab --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp @@ -0,0 +1,875 @@ +/** + * @file wsdb_ref_native_diff.test.cpp + * @brief wsdb-ref against the node world state (native-packages/wsdb's aztec-wsdb), request for request. + * + * Both serve wsdb_schema.jsonc, so one encoded request goes to both: wsdb-ref through its in-process FFI + * entry, aztec-wsdb through its socket. A seeded random run builds blocks the way a node does (effects on a + * fork, then SyncBlock), interleaved with reads at every revision, writes and checkpoints on forks, unwinds, + * finalization and history removal. Every response must be byte-identical, apart from the store statistics a + * full status carries; a request must fail on both or on neither. + * + * Opt-in: set WSDB_NATIVE_BIN to an aztec-wsdb binary built from the same schema. WSDB_DIFF_SEEDS (default 4) + * and WSDB_DIFF_STEPS (default 400) size the run. + */ +#include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp" +#include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" +#include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" +#include "ipc_runtime/ipc_client.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char** environ; + +using namespace bb; +using namespace bb::wsdb_ref; + +namespace { + +constexpr uint32_t LATEST = std::numeric_limits::max(); + +template W bytes_of(const fr& value) +{ + W w{}; + fr::serialize_to_buffer(value, w.data()); + return w; +} + +fr fr_of(const auto& w) +{ + return fr::serialize_from_buffer(w.data()); +} + +std::vector encode_request(const std::string& name, const auto& cmd) +{ + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack_array(1); + pk.pack_array(2); + pk.pack(name); + pk.pack(cmd); + return { buf.data(), buf.data() + buf.size() }; +} + +/** A response frame: its tag, and the payload decoded when it is not an error. */ +struct Frame { + std::vector bytes; + std::string tag; + std::string error; + bool is_error() const { return tag == "WsdbErrorResponse"; } +}; + +Frame parse(std::vector bytes) +{ + Frame frame{ .bytes = std::move(bytes), .tag = {}, .error = {} }; + auto unpacked = msgpack::unpack(reinterpret_cast(frame.bytes.data()), frame.bytes.size()); + auto obj = unpacked.get(); + frame.tag.assign(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size); + if (frame.is_error()) { + wire::WsdbErrorResponse error; + obj.via.array.ptr[1].convert(error); + frame.error = error.message; + } + return frame; +} + +template Resp decode(const Frame& frame) +{ + auto unpacked = msgpack::unpack(reinterpret_cast(frame.bytes.data()), frame.bytes.size()); + Resp resp; + unpacked.get().via.array.ptr[1].convert(resp); + return resp; +} + +/** aztec-wsdb in a child process, at the same genesis wsdb-ref is given. */ +class NativeWsdb { + public: + NativeWsdb(const std::string& binary, const std::filesystem::path& dir) + : socket_path_((dir / "wsdb.sock").string()) + { + std::filesystem::create_directories(dir / "data"); + std::ostringstream nullifiers; + nullifiers << "["; + auto seeds = world_state::genesis_protocol_nullifiers(); + for (size_t i = 0; i < seeds.size(); ++i) { + std::ostringstream hex; + hex << seeds[i]; + nullifiers << (i > 0 ? "," : "") << "\"" << hex.str().substr(2) << "\""; + } + nullifiers << "]"; + std::ostringstream heights; + heights << "{0:" << NULLIFIER_TREE_HEIGHT << ",1:" << NOTE_HASH_TREE_HEIGHT << ",2:" << PUBLIC_DATA_TREE_HEIGHT + << ",3:" << L1_TO_L2_MSG_TREE_HEIGHT << ",4:" << ARCHIVE_HEIGHT << "}"; + std::vector args{ + binary, + "msgpack", + "run", + "-i", + socket_path_, + "-d", + (dir / "data").string(), + "-t", + "4", + "--tree-heights", + heights.str(), + "--tree-prefill", + "{0:128,2:128}", + "--initial-header-generator-point", + std::to_string(DOM_SEP__BLOCK_HEADER_HASH), + "--prefilled-public-data", + R"([["00000000000000000000000000000000000000000000000000000000000003e8","0000000000000000000000000000000000000000000000000000000000000007"]])", + "--prefilled-nullifiers", + nullifiers.str() + }; + std::vector argv; + for (auto& arg : args) { + argv.push_back(arg.data()); + } + argv.push_back(nullptr); + if (posix_spawn(&pid_, binary.c_str(), nullptr, nullptr, argv.data(), environ) != 0) { + throw std::runtime_error("failed to spawn " + binary); + } + client_ = ipc::IpcClient::create_socket(socket_path_); + for (int attempt = 0; attempt < 200 && !client_->connect(); ++attempt) { + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + } + } + + ~NativeWsdb() + { + client_->close(); + kill(pid_, SIGTERM); + int status = 0; + waitpid(pid_, &status, 0); + } + + std::vector call(const std::vector& request) + { + if (!client_->send(request.data(), request.size(), 0)) { + throw std::runtime_error("send to aztec-wsdb failed"); + } + auto response = client_->receive(0); + if (response.data() == nullptr) { + throw std::runtime_error("receive from aztec-wsdb failed"); + } + std::vector out(response.begin(), response.end()); + client_->release(response.size()); + return out; + } + + private: + std::string socket_path_; + pid_t pid_ = 0; + std::unique_ptr client_; +}; + +std::vector call_ref(const std::vector& request) +{ + uint8_t* output = nullptr; + size_t output_len = 0; + wsdb_ipc_ffi_entry(request.data(), request.size(), &output, &output_len); + std::vector frame(output, output + output_len); + wsdb_ipc_ffi_free(output); + return frame; +} + +/** One differential run: a random but node-shaped workload, every request answered by both. */ +class DiffRun { + public: + DiffRun(NativeWsdb& native, uint64_t seed) + : native_(native) + , rng_(seed) + {} + + size_t requests = 0; + size_t errors = 0; + size_t empty_slot_divergences = 0; + std::vector message_mismatches; + + /** Sends `cmd` to both; returns the reference's frame. */ + template Frame both(const std::string& name, const Cmd& cmd) + { + auto request = encode_request(name, cmd); + Frame ref = parse(call_ref(request)); + Frame nat = parse(native_.call(request)); + ++requests; + if (std::getenv("WSDB_DIFF_TRACE") != nullptr) { + std::cout << "step " << step_ << " " << dump_request(request).substr(0, 600) + << (ref.is_error() + ? " -> error: " + ref.error + : (name == "WsdbCreateFork" || name == "WsdbGetStatus" ? " -> " + dump(ref) : "")) + << "\n"; + } + if (empty_slot_divergence(name, ref, nat)) { + ++empty_slot_divergences; + return ref; + } + if (ref.is_error() != nat.is_error()) { + ADD_FAILURE() << name << " step " << step_ << " " << dump_request(request) << ": wsdb-ref " + << (ref.is_error() ? "failed: " + ref.error : "succeeded") << ", aztec-wsdb " + << (nat.is_error() ? "failed: " + nat.error : "succeeded"); + return ref; + } + if (ref.is_error()) { + ++errors; + if (ref.error != nat.error) { + message_mismatches.push_back(name + ": '" + ref.error + "' vs '" + nat.error + "'"); + } + return ref; + } + if (has_full_status(name)) { + compare_status(name, ref, nat); + } else if (name == "WsdbGetStateReference" || name == "WsdbGetInitialStateReference") { + // A node builds a state reference from an unordered map: the entries match, in any order. + EXPECT_EQ(sorted_state(ref), sorted_state(nat)) << name << " step " << step_ << ": state references differ"; + } else if (ref.bytes != nat.bytes) { + ADD_FAILURE() << name << " step " << step_ << ": responses differ\n request: " << dump_request(request) + << "\n wsdb-ref: " << dump(ref) << "\n aztec-wsdb: " << dump(nat); + } + return ref; + } + + void run(size_t steps) + { + init(); + for (step_ = 0; step_ < steps && !::testing::Test::HasFailure(); ++step_) { + std::uniform_int_distribution pick(0, 99); + int choice = pick(rng_); + if (choice < 12) { + build_block(); + } else if (choice < 40) { + read(); + } else if (choice < 65) { + write_on_fork(); + } else if (choice < 72) { + checkpoint_op(); + } else if (choice < 78) { + fork_op(); + } else if (choice < 83) { + canonical_op(); + } else if (choice < 90) { + chain_op(); + } else { + read(); + } + } + } + + private: + NativeWsdb& native_; + std::mt19937_64 rng_; + size_t step_ = 0; + std::vector forks_{ 0 }; + std::vector known_keys_; + std::vector known_note_hashes_; + std::vector slots_; + + static std::string dump(const Frame& frame) + { + auto unpacked = msgpack::unpack(reinterpret_cast(frame.bytes.data()), frame.bytes.size()); + std::ostringstream out; + out << unpacked.get(); + std::string text = out.str(); + return text.size() > 1500 ? text.substr(0, 1500) + "..." : text; + } + + /** + * A read of an indexed-tree slot a batch left empty. aztec-wsdb answers it with the empty leaf or fails (null + * for a leaf-value read) depending on how it stored the nodes above it: under a committed all-zero subtree the + * path is not found. wsdb-ref always answers the empty leaf. Clients only read slots they found by key. + */ + static bool empty_slot_divergence(const std::string& name, const Frame& ref, const Frame& nat) + { + auto empty_or_missing = [&](const Frame& frame) { + if (frame.is_error()) { + return true; + } + if (name == "WsdbGetNullifierLeafPreimage") { + auto p = decode(frame).preimage; + return p.has_value() && fr_of(p->leaf.nullifier).is_zero() && p->nextIndex == 0 && + fr_of(p->nextKey).is_zero(); + } + if (name == "WsdbGetPublicDataLeafPreimage") { + auto p = decode(frame).preimage; + return p.has_value() && fr_of(p->leaf.slot).is_zero() && fr_of(p->leaf.value).is_zero() && + p->nextIndex == 0 && fr_of(p->nextKey).is_zero(); + } + if (name == "WsdbGetNullifierLeafValue") { + auto v = decode(frame).value; + return !v.has_value() || fr_of(v->nullifier).is_zero(); + } + if (name == "WsdbGetPublicDataLeafValue") { + auto v = decode(frame).value; + return !v.has_value() || (fr_of(v->slot).is_zero() && fr_of(v->value).is_zero()); + } + return false; + }; + return ref.bytes != nat.bytes && empty_or_missing(ref) && empty_or_missing(nat); + } + + static std::string dump_request(const std::vector& request) + { + auto unpacked = msgpack::unpack(reinterpret_cast(request.data()), request.size()); + std::ostringstream out; + out << unpacked.get(); + std::string text = out.str(); + return text.size() > 1500 ? text.substr(0, 1500) + "..." : text; + } + + static std::vector> sorted_state(const Frame& frame) + { + // Both responses carry `state` as their only field. + auto state = decode(frame).state; + std::vector> out; + for (const auto& tree : state) { + out.emplace_back(tree.treeId, fr_of(tree.root), tree.size); + } + std::sort(out.begin(), out.end(), [](const auto& a, const auto& b) { return std::get<0>(a) < std::get<0>(b); }); + return out; + } + + static bool has_full_status(const std::string& name) + { + return name == "WsdbCommit" || name == "WsdbSyncBlock" || name == "WsdbUnwindBlocks" || + name == "WsdbRemoveHistoricalBlocks"; + } + + void compare_status(const std::string& name, const Frame& ref, const Frame& nat) + { + // Store statistics are a node's LMDB sizes; the summary and every tree's meta must match. + auto summary = [](const wire::WorldStateStatusFull& s) { + return std::make_tuple(s.summary.unfinalizedBlockNumber, + s.summary.finalizedBlockNumber, + s.summary.oldestHistoricalBlock, + s.summary.treesAreSynched); + }; + auto meta = [](const wire::TreeMeta& m) { + return std::make_tuple(m.depth, + m.size, + m.committedSize, + fr_of(m.root), + m.initialSize, + fr_of(m.initialRoot), + m.oldestHistoricBlock, + m.unfinalizedBlockHeight, + m.finalizedBlockHeight); + }; + auto statuses = [&]() -> std::pair { + if (name == "WsdbCommit") { + return { decode(ref).status, decode(nat).status }; + } + if (name == "WsdbSyncBlock") { + return { decode(ref).status, + decode(nat).status }; + } + if (name == "WsdbUnwindBlocks") { + return { decode(ref).status, + decode(nat).status }; + } + return { decode(ref).status, + decode(nat).status }; + }(); + const auto& [r, n] = statuses; + // aztec-wsdb's Commit leaves its summary unset (WorldState::commit does not populate it), so only its + // meta is compared. + if (name != "WsdbCommit") { + EXPECT_EQ(summary(r), summary(n)) << name << " step " << step_ << ": status summaries differ"; + } + EXPECT_EQ(meta(r.meta.nullifierTreeMeta), meta(n.meta.nullifierTreeMeta)) << name << " nullifier meta"; + EXPECT_EQ(meta(r.meta.noteHashTreeMeta), meta(n.meta.noteHashTreeMeta)) << name << " note hash meta"; + EXPECT_EQ(meta(r.meta.publicDataTreeMeta), meta(n.meta.publicDataTreeMeta)) << name << " public data meta"; + EXPECT_EQ(meta(r.meta.messageTreeMeta), meta(n.meta.messageTreeMeta)) << name << " message meta"; + EXPECT_EQ(meta(r.meta.archiveTreeMeta), meta(n.meta.archiveTreeMeta)) << name << " archive meta"; + } + + uint64_t uniform(uint64_t lo, uint64_t hi) { return std::uniform_int_distribution(lo, hi)(rng_); } + bool coin(int percent) { return uniform(0, 99) < static_cast(percent); } + + fr fresh_key() + { + fr key(uniform(1000, 1ULL << 62)); + known_keys_.push_back(key); + return key; + } + fr some_key() + { + return known_keys_.empty() || coin(30) ? fresh_key() : known_keys_[uniform(0, known_keys_.size() - 1)]; + } + fr some_slot() + { + if (slots_.empty() || coin(40)) { + slots_.push_back(fr(uniform(129, 5000))); + } + return slots_[uniform(0, slots_.size() - 1)]; + } + fr some_note_hash() + { + if (known_note_hashes_.empty() || coin(40)) { + return fr(uniform(1, 1ULL << 62)); + } + return known_note_hashes_[uniform(0, known_note_hashes_.size() - 1)]; + } + uint64_t some_fork() { return forks_[uniform(0, forks_.size() - 1)]; } + + uint32_t latest_block() + { + auto status = decode(both("WsdbGetStatus", wire::WsdbGetStatus{})); + return static_cast(status.status.unfinalizedBlockNumber); + } + + wire::WorldStateRevision some_revision() + { + uint32_t latest = latest_block(); + if (coin(30)) { + // Historical reads go to the canonical fork, as a node's clients send them. (Through another fork, + // aztec-wsdb caps index searches at that fork's own block: a combination nothing sends.) + return { .forkId = coin(10) ? 99ULL : 0ULL, + .blockNumber = static_cast(uniform(0, latest + 1)), + .includeUncommitted = coin(60) }; + } + return { .forkId = coin(10) ? 99 : some_fork(), .blockNumber = LATEST, .includeUncommitted = coin(60) }; + } + + void init() + { + // The genesis aztec-wsdb was started with: the protocol nullifiers and one public-data slot. + wire::WsdbInitGenesis genesis{ .prefilledPublicData = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(7)) } }, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = + static_cast(DOM_SEP__BLOCK_HEADER_HASH) }; + for (const auto& seed : world_state::genesis_protocol_nullifiers()) { + genesis.prefilledNullifiers.push_back(bytes_of(seed)); + } + auto request = encode_request("WsdbInitGenesis", genesis); + ASSERT_FALSE(parse(call_ref(request)).is_error()); + slots_.push_back(fr(1000)); + both("WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}); + } + + void read() + { + auto revision = some_revision(); + uint32_t tree = static_cast(uniform(0, 4)); + switch (uniform(0, 11)) { + case 0: + both("WsdbGetTreeInfo", wire::WsdbGetTreeInfo{ .treeId = tree, .revision = revision }); + break; + case 1: + both("WsdbGetStateReference", wire::WsdbGetStateReference{ .revision = revision }); + break; + case 2: + both("WsdbGetLeafValue", + wire::WsdbGetLeafValue{ + .treeId = coin(80) ? 1u : 4u, .revision = revision, .leafIndex = uniform(0, 300) }); + break; + case 3: + both("WsdbGetNullifierLeafPreimage", + wire::WsdbGetNullifierLeafPreimage{ .revision = revision, .leafIndex = uniform(0, 400) }); + both("WsdbGetNullifierLeafValue", + wire::WsdbGetNullifierLeafValue{ .revision = revision, .leafIndex = uniform(0, 400) }); + break; + case 4: + both("WsdbGetPublicDataLeafPreimage", + wire::WsdbGetPublicDataLeafPreimage{ .revision = revision, .leafIndex = uniform(0, 300) }); + both("WsdbGetPublicDataLeafValue", + wire::WsdbGetPublicDataLeafValue{ .revision = revision, .leafIndex = uniform(0, 300) }); + break; + case 5: + both("WsdbGetSiblingPath", + wire::WsdbGetSiblingPath{ .treeId = tree, .revision = revision, .leafIndex = uniform(0, 400) }); + break; + case 6: + both("WsdbFindLeafIndices", + wire::WsdbFindLeafIndices{ + .treeId = 1, + .revision = revision, + .leaves = { bytes_of(some_note_hash()), bytes_of(some_note_hash()) }, + .startIndex = uniform(0, 20) }); + break; + case 7: + both("WsdbFindNullifierLeafIndices", + wire::WsdbFindNullifierLeafIndices{ + .revision = revision, + .leaves = { { .nullifier = bytes_of(some_key()) }, + { .nullifier = bytes_of(fr(uniform(1, 127))) } }, + .startIndex = uniform(0, 140) }); + both("WsdbFindPublicDataLeafIndices", + wire::WsdbFindPublicDataLeafIndices{ + .revision = revision, + .leaves = { { .slot = bytes_of(some_slot()), + .value = bytes_of(fr(uniform(0, 3))) } }, + .startIndex = 0 }); + break; + case 8: + both("WsdbFindLowLeaf", + wire::WsdbFindLowLeaf{ .treeId = coin(50) ? 0u : 2u, + .revision = revision, + .key = bytes_of(coin(50) ? some_key() : some_slot()) }); + break; + case 9: + both("WsdbFindSiblingPaths", + wire::WsdbFindSiblingPaths{ + .treeId = 1, .revision = revision, .leaves = { bytes_of(some_note_hash()) } }); + both("WsdbFindNullifierSiblingPaths", + wire::WsdbFindNullifierSiblingPaths{ + .revision = revision, .leaves = { { .nullifier = bytes_of(some_key()) } } }); + both("WsdbFindPublicDataSiblingPaths", + wire::WsdbFindPublicDataSiblingPaths{ + .revision = revision, + .leaves = { { .slot = bytes_of(some_slot()), + .value = bytes_of(fr(0)) } } }); + break; + case 10: + both("WsdbGetBlockNumbersForLeafIndices", + wire::WsdbGetBlockNumbersForLeafIndices{ + .treeId = tree, .revision = revision, .leafIndices = { 0, uniform(0, 140), uniform(0, 400) } }); + break; + default: + both("WsdbGetStatus", wire::WsdbGetStatus{}); + both("WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}); + } + } + + /** Nullifiers, some zero; with `fresh_only`, none already inserted or repeated (so the insertion succeeds). */ + std::vector some_nullifiers(size_t count, bool allow_zero, bool fresh_only) + { + std::vector out; + for (size_t i = 0; i < count; ++i) { + fr key = allow_zero && coin(20) ? fr(0) : (fresh_only || coin(90) ? fresh_key() : some_key()); + out.push_back({ .nullifier = bytes_of(key) }); + } + return out; + } + + /** Public-data writes to distinct slots (a batch rejects a repeated slot). */ + std::vector distinct_public_data(size_t count) + { + std::vector out; + std::vector used; + while (out.size() < count) { + fr slot = some_slot(); + if (std::find(used.begin(), used.end(), slot) != used.end()) { + continue; + } + used.push_back(slot); + out.push_back({ .slot = bytes_of(slot), + .value = bytes_of(fr(uniform(1, 1000))) }); + } + return out; + } + + std::vector some_public_data(size_t count, bool allow_zero) + { + std::vector out; + for (size_t i = 0; i < count; ++i) { + if (allow_zero && coin(15)) { + out.push_back( + { .slot = bytes_of(fr(0)), .value = bytes_of(fr(0)) }); + continue; + } + out.push_back({ .slot = bytes_of(some_slot()), + .value = bytes_of(fr(uniform(1, 1000))) }); + } + return out; + } + + std::vector some_note_hashes(size_t count) + { + std::vector out; + for (size_t i = 0; i < count; ++i) { + fr value = coin(25) ? fr(0) : fr(uniform(1, 1ULL << 62)); + if (!value.is_zero()) { + known_note_hashes_.push_back(value); + } + out.push_back(bytes_of(value)); + } + return out; + } + + /** + * A fork for writes that may fail. aztec-wsdb's batch and sequential insertions update its leaf caches before + * they find a duplicate key, so a failed insertion leaves that fork's caches out of step with its tree (wsdb-ref + * changes nothing on a failure). A fork an insertion failed on is deleted, and the canonical fork only gets + * writes that cannot fail. + */ + uint64_t scratch_fork() + { + std::vector others(forks_.begin() + 1, forks_.end()); + if (others.empty() || coin(15)) { + auto created = both("WsdbCreateFork", wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }); + forks_.push_back(decode(created).forkId); + return forks_.back(); + } + return others[uniform(0, others.size() - 1)]; + } + + void drop_if_failed(const Frame& frame, uint64_t fork) + { + if (frame.is_error() && fork != 0) { + both("WsdbDeleteFork", wire::WsdbDeleteFork{ .forkId = fork }); + std::erase(forks_, fork); + } + } + + void write_on_fork() + { + uint64_t fork = coin(25) ? 0 : scratch_fork(); + if (fork == 0) { + // Writes the canonical fork takes that cannot fail. + both("WsdbAppendLeaves", + wire::WsdbAppendLeaves{ + .treeId = coin(70) ? 1u : 3u, .leaves = some_note_hashes(uniform(0, 5)), .forkId = 0 }); + both("WsdbSequentialInsertNullifier", + wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(uniform(0, 3), true, true), + .forkId = 0 }); + both("WsdbBatchInsertPublicData", + wire::WsdbBatchInsertPublicData{ .leaves = distinct_public_data(4), .subtreeDepth = 2, .forkId = 0 }); + return; + } + Frame result; + switch (uniform(0, 6)) { + case 0: + both("WsdbAppendLeaves", + wire::WsdbAppendLeaves{ + .treeId = coin(70) ? 1u : 3u, .leaves = some_note_hashes(uniform(0, 5)), .forkId = fork }); + break; + case 1: + result = both("WsdbSequentialInsertNullifier", + wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(uniform(0, 4), true, false), + .forkId = fork }); + break; + case 2: + result = both("WsdbSequentialInsertPublicData", + wire::WsdbSequentialInsertPublicData{ .leaves = some_public_data(uniform(0, 4), true), + .forkId = fork }); + break; + case 3: + result = both("WsdbBatchInsertNullifier", + wire::WsdbBatchInsertNullifier{ .leaves = some_nullifiers(4, true, false), + .subtreeDepth = static_cast(uniform(0, 2)), + .forkId = fork }); + break; + case 4: + result = both("WsdbBatchInsertPublicData", + wire::WsdbBatchInsertPublicData{ .leaves = some_public_data(4, true), + .subtreeDepth = static_cast(uniform(0, 2)), + .forkId = fork }); + break; + case 5: + result = both("WsdbAppendNullifierLeaves", + wire::WsdbAppendNullifierLeaves{ .leaves = some_nullifiers(uniform(1, 3), true, false), + .forkId = fork }); + drop_if_failed(result, fork); + if (result.is_error()) { + return; + } + result = both( + "WsdbAppendPublicDataLeaves", + wire::WsdbAppendPublicDataLeaves{ .leaves = some_public_data(uniform(1, 3), true), .forkId = fork }); + break; + default: { + auto state = both("WsdbGetStateReference", + wire::WsdbGetStateReference{ + .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }); + if (state.is_error()) { + break; + } + auto block_state = decode(state).state; + if (coin(20)) { + block_state = decode( + both("WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{})) + .state; + } + both( + "WsdbUpdateArchive", + wire::WsdbUpdateArchive{ .blockStateRef = block_state, + .blockHeaderHash = bytes_of(fr(uniform(1, 1ULL << 62))), + .forkId = fork }); + } + } + drop_if_failed(result, fork); + } + + void checkpoint_op() + { + uint64_t fork = some_fork(); + switch (uniform(0, 4)) { + case 0: + case 1: + both("WsdbCreateCheckpoint", wire::WsdbCreateCheckpoint{ .forkId = fork }); + break; + case 2: + both(coin(50) ? "WsdbCommitCheckpoint" : "WsdbRevertCheckpoint", + wire::WsdbCommitCheckpoint{ .forkId = fork }); + break; + case 3: + both("WsdbCommitAllCheckpoints", wire::WsdbCommitAllCheckpoints{ .forkId = fork }); + break; + default: + both("WsdbRevertAllCheckpoints", wire::WsdbRevertAllCheckpoints{ .forkId = fork }); + } + } + + void fork_op() + { + if (coin(60) || forks_.size() < 2) { + bool latest = coin(70); + auto frame = + both("WsdbCreateFork", + wire::WsdbCreateFork{ .latest = latest, + .blockNumber = static_cast(uniform(0, latest_block() + 1)) }); + if (!frame.is_error()) { + forks_.push_back(decode(frame).forkId); + } + } else { + size_t at = uniform(0, forks_.size() - 1); + both("WsdbDeleteFork", wire::WsdbDeleteFork{ .forkId = forks_[at] }); + if (forks_[at] != 0) { + forks_.erase(forks_.begin() + static_cast(at)); + } + } + } + + void canonical_op() + { + // Direct writes to the canonical fork, then a commit or a rollback. + both("WsdbAppendLeaves", wire::WsdbAppendLeaves{ .treeId = 1, .leaves = some_note_hashes(2), .forkId = 0 }); + both("WsdbSequentialInsertNullifier", + wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(1, false, true), .forkId = 0 }); + if (coin(50)) { + both("WsdbCommit", wire::WsdbCommit{}); + } else { + both("WsdbRollback", wire::WsdbRollback{}); + } + } + + void chain_op() + { + uint32_t latest = latest_block(); + switch (uniform(0, 2)) { + case 0: + both("WsdbFinalizeBlocks", + wire::WsdbFinalizeBlocks{ .toBlockNumber = static_cast(uniform(0, latest + 1)) }); + break; + case 1: + both("WsdbUnwindBlocks", + wire::WsdbUnwindBlocks{ .toBlockNumber = static_cast(uniform(0, latest)) }); + break; + default: + both("WsdbRemoveHistoricalBlocks", + wire::WsdbRemoveHistoricalBlocks{ .toBlockNumber = static_cast(uniform(0, latest + 1)) }); + } + // Forks a chain operation dropped are gone on both; forget them. + std::erase_if(forks_, [&](uint64_t fork) { + if (fork == 0) { + return false; + } + return parse(call_ref(encode_request( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 0, + .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }))) + .is_error(); + }); + } + + /** Builds a block as a node does: its effects on a fork at the tip, then the same effects synced to fork 0. */ + void build_block() + { + auto created = both("WsdbCreateFork", wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }); + uint64_t fork = decode(created).forkId; + auto notes = some_note_hashes(coin(30) ? 64 : 8); + auto messages = coin(30) ? some_note_hashes(4) : std::vector{}; + auto nullifiers = some_nullifiers(coin(30) ? 64 : 8, true, true); + auto public_data = some_public_data(uniform(0, 4), false); + both("WsdbAppendLeaves", wire::WsdbAppendLeaves{ .treeId = 1, .leaves = notes, .forkId = fork }); + both("WsdbAppendLeaves", wire::WsdbAppendLeaves{ .treeId = 3, .leaves = messages, .forkId = fork }); + auto inserted = + both("WsdbBatchInsertNullifier", + wire::WsdbBatchInsertNullifier{ + .leaves = nullifiers, .subtreeDepth = nullifiers.size() == 64 ? 6u : 3u, .forkId = fork }); + both("WsdbSequentialInsertPublicData", + wire::WsdbSequentialInsertPublicData{ .leaves = public_data, .forkId = fork }); + auto state = decode( + both("WsdbGetStateReference", + wire::WsdbGetStateReference{ + .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } })); + fr header_hash(uniform(1, 1ULL << 62)); + both("WsdbUpdateArchive", + wire::WsdbUpdateArchive{ .blockStateRef = state.state, + .blockHeaderHash = bytes_of(header_hash), + .forkId = fork }); + auto archive = decode( + both("WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 4, .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } })); + auto previous = decode( + both("WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ + .treeId = 4, .revision = { .forkId = 0, .blockNumber = LATEST, .includeUncommitted = false } })); + both("WsdbDeleteFork", wire::WsdbDeleteFork{ .forkId = fork }); + if (inserted.is_error()) { + return; + } + // Sometimes a block whose claimed state is wrong, which both must reject without committing. + if (coin(10)) { + state.state[0].size += 1; + } + both("WsdbSyncBlock", + wire::WsdbSyncBlock{ .blockNumber = 0, + .blockStateRef = state.state, + .blockHeaderHash = bytes_of(header_hash), + .paddedNoteHashes = notes, + .paddedL1ToL2Messages = messages, + .paddedNullifiers = nullifiers, + .publicDataWrites = public_data, + .expectedArchiveRoot = coin(80) ? std::optional(archive.root) : std::nullopt, + .expectedPreviousArchiveRoot = + coin(80) ? std::optional(previous.root) : std::nullopt }); + } +}; + +} // namespace + +TEST(WsdbRefNativeDiff, MatchesAztecWsdbRequestForRequest) +{ + const char* binary = std::getenv("WSDB_NATIVE_BIN"); + if (binary == nullptr) { + GTEST_SKIP() << "set WSDB_NATIVE_BIN to an aztec-wsdb binary to compare against it"; + } + size_t seeds = std::getenv("WSDB_DIFF_SEEDS") ? std::stoul(std::getenv("WSDB_DIFF_SEEDS")) : 4; + size_t steps = std::getenv("WSDB_DIFF_STEPS") ? std::stoul(std::getenv("WSDB_DIFF_STEPS")) : 400; + for (uint64_t seed = 1; seed <= seeds && !HasFailure(); ++seed) { + auto dir = std::filesystem::temp_directory_path() / + ("wsdb-ref-diff-" + std::to_string(getpid()) + "-" + std::to_string(seed)); + { + NativeWsdb native(binary, dir); + DiffRun run(native, seed); + run.run(steps); + std::cout << "seed " << seed << ": " << run.requests << " requests, " << run.errors + << " answered with errors on both, " << run.message_mismatches.size() + << " with different messages, " << run.empty_slot_divergences + << " empty-slot reads answered differently\n"; + for (size_t i = 0; i < std::min(run.message_mismatches.size(), 5); ++i) { + std::cout << " " << run.message_mismatches[i] << "\n"; + } + } + std::filesystem::remove_all(dir); + } +} diff --git a/barretenberg/ts/.rebuild_patterns b/barretenberg/ts/.rebuild_patterns index 7f7971394db..365d7a9d0a3 100644 --- a/barretenberg/ts/.rebuild_patterns +++ b/barretenberg/ts/.rebuild_patterns @@ -6,4 +6,4 @@ ^barretenberg/cpp/src/barretenberg/bbapi/bb_schema\.json$ ^barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants\.json$ ^barretenberg/cpp/src/barretenberg/cdb/cdb_schema\.json$ -^barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema\.jsonc$ +^native-packages/wsdb/wsdb_schema\.jsonc$ diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index 17be017c379..8c9c904c095 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -82,13 +82,13 @@ function generate_bb_js_api_package { --package-wasm-threads-module "barretenberg-threads.wasm=$ROOT/barretenberg/cpp/build-wasm-threads/bin/barretenberg.wasm" } -# The in-memory reference world state over the wsdb base contract, fully generated: in-process +# The in-memory reference world state over the node world state's contract, fully generated: in-process # through its wasm module (node and browsers) or, opted into here with napi, its Node-API addon. # Both run the one FFI entry the bb build generates from the same schema (cpp/.../wsdb_ref). function generate_wsdb_ref_package { node --experimental-strip-types --no-warnings \ "$ROOT/ipc-codegen/src/generate.ts" \ - --schema "$ROOT/barretenberg/cpp/src/barretenberg/world_state_reference/wsdb_base_schema.jsonc" \ + --schema "$ROOT/native-packages/wsdb/wsdb_schema.jsonc" \ --lang ts \ --package "$ROOT/barretenberg/ts/wsdb-ref" \ --package-name "$WSDB_REF_PACKAGE" \ diff --git a/barretenberg/ts/scripts/wsdb_ref.test.mjs b/barretenberg/ts/scripts/wsdb_ref.test.mjs index c79ff196c7d..ef5284c333c 100644 --- a/barretenberg/ts/scripts/wsdb_ref.test.mjs +++ b/barretenberg/ts/scripts/wsdb_ref.test.mjs @@ -93,19 +93,26 @@ test('nullifier inserts return witnesses, and duplicates are the service saying await assert.rejects(service.sequentialInsertNullifier({ leaves: [{ nullifier }], forkId: 0 }), error => { assert.ok(error instanceof WsdbError); - assert.match(error.message, /already exists/); + assert.match(error.message, /is already present/); return true; }); }); -test('requests outside the reference are errors, not crashes', async () => { +test('forks are separate world states, and bad requests are errors, not crashes', async () => { + const { forkId } = await service.createFork({ latest: true, blockNumber: 0 }); + await service.appendLeaves({ treeId: NOTE_HASH_TREE, leaves: [randomFr()], forkId }); + const onFork = await service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, forkId } }); + const canonical = await service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: committed }); + assert.equal(onFork.size, canonical.size + 1); + await service.deleteFork({ forkId }); + await assert.rejects( - service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, forkId: 1 } }), - /the reference world state has one fork \(0\), not 1/, + service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, forkId } }), + /Fork not found/, ); await assert.rejects( service.getTreeInfo({ treeId: NOTE_HASH_TREE, revision: { ...uncommitted, blockNumber: 7 } }), - /keeps no block history/, + /latest block is 0/, ); // The service still answers afterwards (a wasm module that aborted would not). const info = await service.getTreeInfo({ treeId: ARCHIVE, revision: committed }); diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp index b9fb70a023b..a898b652d40 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp @@ -759,4 +759,14 @@ void handle_copy_stores(WsdbRequest& ctx, wire::WsdbCopyStores&& cmd, Responder< }); } +// aztec-wsdb takes its genesis on the command line, before it opens its stores. InitGenesis is for in-process world +// states (wsdb-ref), which have no command line. +void handle_init_genesis(WsdbRequest& /*ctx*/, + wire::WsdbInitGenesis&& /*cmd*/, + Responder respond) +{ + respond.error("aztec-wsdb takes its genesis on the command line (--prefilled-public-data, --prefilled-nullifiers, " + "--genesis-timestamp, --initial-header-generator-point), not over InitGenesis"); +} + } // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp index d3e4eb710ae..7845a3d5ab5 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp @@ -127,5 +127,8 @@ void handle_revert_all_checkpoints(WsdbRequest& ctx, wire::WsdbRevertAllCheckpoints&& cmd, Responder respond); void handle_copy_stores(WsdbRequest& ctx, wire::WsdbCopyStores&& cmd, Responder respond); +void handle_init_genesis(WsdbRequest& ctx, + wire::WsdbInitGenesis&& cmd, + Responder respond); } // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/wsdb_schema.jsonc b/native-packages/wsdb/wsdb_schema.jsonc index cea021eff17..e90e209b4ea 100644 --- a/native-packages/wsdb/wsdb_schema.jsonc +++ b/native-packages/wsdb/wsdb_schema.jsonc @@ -472,6 +472,17 @@ "compact": "bool?" }, "response": {} + }, + // Restarts an in-process world state (wsdb-ref) at this genesis, before any block. aztec-wsdb takes its + // genesis on the command line and answers this with an error. + "InitGenesis": { + "request": { + "prefilledPublicData": "PublicDataLeafValue[]", + "prefilledNullifiers": "Fr[]", + "genesisTimestamp": "u64", + "initialHeaderGeneratorPoint": "u32" + }, + "response": {} } } } From e6eff34747021ddc9f70b6fa16954c3f8c6bb6fb Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:51:35 +0000 Subject: [PATCH 05/20] fix(wsdb): atomic indexed inserts, a filled Commit status, empty slots read as empty Three aztec-wsdb bugs the wsdb-ref differential test found: - A batch or sequential indexed insertion that fails on a duplicate key has already written leaf preimages and key indices to the fork's cache, so later witnesses on that fork are wrong. Each insertion now runs inside a checkpoint of its tree, reverted on failure. The public processor's per-tx checkpoint already undid this, so the sequencer was not affected. - Commit answered an uninitialized status summary and dropped commit's failure. WorldState::commit now fills the summary, and the handler answers a failed commit with its error. Nothing in the node sends Commit. - A read of an in-range indexed slot that a batch left empty answered the empty leaf or failed, depending on whether a node beneath it was stored. It now answers the empty leaf. Clients only read slots they found by key. Each fix has a world state test that fails without it. The differential test drops its workarounds: failed writes now go to any fork, including the canonical one, and fork deletion after a failure is gone; Commit's summary is compared; empty-slot reads must match. It also stops treating any pair of failures with different messages as an empty-slot divergence, which had hidden every error-message mismatch. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G5s7nEWrYapDK2CWpEzcAf --- .../wsdb_ref/wsdb_ref_native_diff.test.cpp | 88 ++++---------- .../content_addressed_indexed_tree.hpp | 14 +++ .../wsdb/cpp/src/world_state/world_state.cpp | 3 + .../wsdb/cpp/src/world_state/world_state.hpp | 110 +++++++++++------- .../cpp/src/world_state/world_state.test.cpp | 65 +++++++++++ .../wsdb/cpp/src/wsdb/wsdb_handlers.cpp | 7 +- 6 files changed, 177 insertions(+), 110 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp index 5da7c4f98ab..34e8427412b 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp @@ -197,7 +197,7 @@ class DiffRun { size_t requests = 0; size_t errors = 0; - size_t empty_slot_divergences = 0; + size_t failed_writes_reverted = 0; std::vector message_mismatches; /** Sends `cmd` to both; returns the reference's frame. */ @@ -214,10 +214,6 @@ class DiffRun { : (name == "WsdbCreateFork" || name == "WsdbGetStatus" ? " -> " + dump(ref) : "")) << "\n"; } - if (empty_slot_divergence(name, ref, nat)) { - ++empty_slot_divergences; - return ref; - } if (ref.is_error() != nat.is_error()) { ADD_FAILURE() << name << " step " << step_ << " " << dump_request(request) << ": wsdb-ref " << (ref.is_error() ? "failed: " + ref.error : "succeeded") << ", aztec-wsdb " @@ -287,40 +283,6 @@ class DiffRun { return text.size() > 1500 ? text.substr(0, 1500) + "..." : text; } - /** - * A read of an indexed-tree slot a batch left empty. aztec-wsdb answers it with the empty leaf or fails (null - * for a leaf-value read) depending on how it stored the nodes above it: under a committed all-zero subtree the - * path is not found. wsdb-ref always answers the empty leaf. Clients only read slots they found by key. - */ - static bool empty_slot_divergence(const std::string& name, const Frame& ref, const Frame& nat) - { - auto empty_or_missing = [&](const Frame& frame) { - if (frame.is_error()) { - return true; - } - if (name == "WsdbGetNullifierLeafPreimage") { - auto p = decode(frame).preimage; - return p.has_value() && fr_of(p->leaf.nullifier).is_zero() && p->nextIndex == 0 && - fr_of(p->nextKey).is_zero(); - } - if (name == "WsdbGetPublicDataLeafPreimage") { - auto p = decode(frame).preimage; - return p.has_value() && fr_of(p->leaf.slot).is_zero() && fr_of(p->leaf.value).is_zero() && - p->nextIndex == 0 && fr_of(p->nextKey).is_zero(); - } - if (name == "WsdbGetNullifierLeafValue") { - auto v = decode(frame).value; - return !v.has_value() || fr_of(v->nullifier).is_zero(); - } - if (name == "WsdbGetPublicDataLeafValue") { - auto v = decode(frame).value; - return !v.has_value() || (fr_of(v->slot).is_zero() && fr_of(v->value).is_zero()); - } - return false; - }; - return ref.bytes != nat.bytes && empty_or_missing(ref) && empty_or_missing(nat); - } - static std::string dump_request(const std::vector& request) { auto unpacked = msgpack::unpack(reinterpret_cast(request.data()), request.size()); @@ -384,11 +346,7 @@ class DiffRun { decode(nat).status }; }(); const auto& [r, n] = statuses; - // aztec-wsdb's Commit leaves its summary unset (WorldState::commit does not populate it), so only its - // meta is compared. - if (name != "WsdbCommit") { - EXPECT_EQ(summary(r), summary(n)) << name << " step " << step_ << ": status summaries differ"; - } + EXPECT_EQ(summary(r), summary(n)) << name << " step " << step_ << ": status summaries differ"; EXPECT_EQ(meta(r.meta.nullifierTreeMeta), meta(n.meta.nullifierTreeMeta)) << name << " nullifier meta"; EXPECT_EQ(meta(r.meta.noteHashTreeMeta), meta(n.meta.noteHashTreeMeta)) << name << " note hash meta"; EXPECT_EQ(meta(r.meta.publicDataTreeMeta), meta(n.meta.publicDataTreeMeta)) << name << " public data meta"; @@ -603,12 +561,10 @@ class DiffRun { } /** - * A fork for writes that may fail. aztec-wsdb's batch and sequential insertions update its leaf caches before - * they find a duplicate key, so a failed insertion leaves that fork's caches out of step with its tree (wsdb-ref - * changes nothing on a failure). A fork an insertion failed on is deleted, and the canonical fork only gets - * writes that cannot fail. + * A fork other than the canonical one to write to, sometimes a new one. A failed write changes nothing on either + * side, so the fork stays in use after one. */ - uint64_t scratch_fork() + uint64_t other_fork() { std::vector others(forks_.begin() + 1, forks_.end()); if (others.empty() || coin(15)) { @@ -619,19 +575,11 @@ class DiffRun { return others[uniform(0, others.size() - 1)]; } - void drop_if_failed(const Frame& frame, uint64_t fork) - { - if (frame.is_error() && fork != 0) { - both("WsdbDeleteFork", wire::WsdbDeleteFork{ .forkId = fork }); - std::erase(forks_, fork); - } - } - void write_on_fork() { - uint64_t fork = coin(25) ? 0 : scratch_fork(); - if (fork == 0) { - // Writes the canonical fork takes that cannot fail. + uint64_t fork = coin(25) ? 0 : other_fork(); + if (fork == 0 && coin(50)) { + // Writes that cannot fail, so the canonical fork keeps growing. both("WsdbAppendLeaves", wire::WsdbAppendLeaves{ .treeId = coin(70) ? 1u : 3u, .leaves = some_note_hashes(uniform(0, 5)), .forkId = 0 }); @@ -642,6 +590,11 @@ class DiffRun { wire::WsdbBatchInsertPublicData{ .leaves = distinct_public_data(4), .subtreeDepth = 2, .forkId = 0 }); return; } + // As the public processor does with each tx: a checkpoint around the write, reverted if the write fails. + bool guarded = coin(50); + if (guarded) { + both("WsdbCreateCheckpoint", wire::WsdbCreateCheckpoint{ .forkId = fork }); + } Frame result; switch (uniform(0, 6)) { case 0: @@ -675,9 +628,8 @@ class DiffRun { result = both("WsdbAppendNullifierLeaves", wire::WsdbAppendNullifierLeaves{ .leaves = some_nullifiers(uniform(1, 3), true, false), .forkId = fork }); - drop_if_failed(result, fork); if (result.is_error()) { - return; + break; } result = both( "WsdbAppendPublicDataLeaves", @@ -703,7 +655,13 @@ class DiffRun { .forkId = fork }); } } - drop_if_failed(result, fork); + if (guarded) { + if (result.is_error()) { + failed_writes_reverted++; + } + both(result.is_error() || coin(50) ? "WsdbRevertCheckpoint" : "WsdbCommitCheckpoint", + wire::WsdbRevertCheckpoint{ .forkId = fork }); + } } void checkpoint_op() @@ -864,8 +822,8 @@ TEST(WsdbRefNativeDiff, MatchesAztecWsdbRequestForRequest) run.run(steps); std::cout << "seed " << seed << ": " << run.requests << " requests, " << run.errors << " answered with errors on both, " << run.message_mismatches.size() - << " with different messages, " << run.empty_slot_divergences - << " empty-slot reads answered differently\n"; + << " with different messages, " << run.failed_writes_reverted + << " failed writes reverted by a checkpoint\n"; for (size_t i = 0; i < std::min(run.message_mismatches.size(), 5); ++i) { std::cout << " " << run.message_mismatches[i] << "\n"; } diff --git a/native-packages/wsdb/cpp/src/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp b/native-packages/wsdb/cpp/src/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp index 2b3c6d6d031..66df2fda92e 100644 --- a/native-packages/wsdb/cpp/src/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp +++ b/native-packages/wsdb/cpp/src/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp @@ -359,6 +359,14 @@ void ContentAddressedIndexedTree::get_leaf(const index_t& requestContext.root = store_->get_current_root(*tx, includeUncommitted); std::optional leaf_hash = find_leaf_hash(index, requestContext, *tx, false); if (!leaf_hash.has_value()) { + // An unwritten subtree below the tree's size holds only empty leaves (a batch's padding). + TreeMeta meta; + store_->get_meta(meta, *tx, includeUncommitted); + if (index < meta.size) { + response.success = true; + response.inner.indexed_leaf = IndexedLeafValueType::empty(); + return; + } response.success = false; response.message = "Failed to find leaf hash for current root"; return; @@ -402,6 +410,12 @@ void ContentAddressedIndexedTree::get_leaf(const index_t& requestContext.root = blockData.root; std::optional leaf_hash = find_leaf_hash(index, requestContext, *tx, false); if (!leaf_hash.has_value()) { + // An unwritten subtree below the block's tree size holds only empty leaves (a batch's padding). + if (index < blockData.size) { + response.success = true; + response.inner.indexed_leaf = IndexedLeafValueType::empty(); + return; + } response.success = false; response.message = format("Failed to find leaf hash for root of block ", blockNumber); return; diff --git a/native-packages/wsdb/cpp/src/world_state/world_state.cpp b/native-packages/wsdb/cpp/src/world_state/world_state.cpp index 70486a0c75b..c367b513d2a 100644 --- a/native-packages/wsdb/cpp/src/world_state/world_state.cpp +++ b/native-packages/wsdb/cpp/src/world_state/world_state.cpp @@ -594,6 +594,9 @@ std::pair WorldState::commit(WorldStateStatusFull& status) } signal.wait_for_level(0); + if (success) { + populate_status_summary(status); + } return std::make_pair(success.load(), message); } diff --git a/native-packages/wsdb/cpp/src/world_state/world_state.hpp b/native-packages/wsdb/cpp/src/world_state/world_state.hpp index 078031fbddc..eb7dd61b8ce 100644 --- a/native-packages/wsdb/cpp/src/world_state/world_state.hpp +++ b/native-packages/wsdb/cpp/src/world_state/world_state.hpp @@ -361,6 +361,14 @@ class WorldState { static void populate_status_summary(WorldStateStatusFull& status); + /** + * Runs `insert(on_done)` on `tree` inside a checkpoint of that tree alone, committed if the insertion succeeds and + * reverted if it fails. An indexed insertion writes leaf preimages and key indices to the tree's cache before it + * finds a duplicate key, so without the checkpoint a failed insertion would leave the cache out of step with the + * tree. + */ + template static void insert_atomically(TreeType& tree, Insert&& insert); + template void commit_tree(TreeDBStats& dbStats, Signal& signal, @@ -678,6 +686,44 @@ template void WorldState::append_leaves(MerkleTreeId id, const std: } } +template void WorldState::insert_atomically(TreeType& tree, Insert&& insert) +{ + auto await_response = [](auto&& call) { + Signal signal; + bool success = true; + std::string message; + call([&](const auto& response) { + success = response.success; + message = response.message; + signal.signal_level(0); + }); + signal.wait_for_level(); + return std::make_pair(success, message); + }; + + auto [checkpointed, checkpoint_error] = await_response([&](auto on_done) { + tree.checkpoint([&, on_done](TypedResponse& response) { on_done(response); }); + }); + if (!checkpointed) { + throw std::runtime_error(checkpoint_error); + } + auto [inserted, insert_error] = await_response(insert); + auto [ended, end_error] = await_response([&](auto on_done) { + auto callback = [&, on_done](const Response& response) { on_done(response); }; + if (inserted) { + tree.commit_checkpoint(callback); + } else { + tree.revert_checkpoint(callback); + } + }); + if (!inserted) { + throw std::runtime_error(insert_error); + } + if (!ended) { + throw std::runtime_error(end_error); + } +} + template BatchInsertionResult WorldState::batch_insert_indexed_leaves(MerkleTreeId id, const std::vector& leaves, @@ -690,31 +736,20 @@ BatchInsertionResult WorldState::batch_insert_indexed_leaves(MerkleTreeId id, Fork::SharedPtr fork = retrieve_fork(fork_id); - Signal signal; BatchInsertionResult result; const auto& wrapper = std::get>(fork->_trees.at(id)); - bool success = true; - std::string error_msg; - - wrapper.tree->add_or_update_values( - leaves, subtree_depth, [&](const TypedResponse>& response) { - if (response.success) { - result.lowLeafWitnessData = *response.inner.lowLeafWitnessData; - result.sortedLeaves = *response.inner.sortedLeaves; - result.subtreePath = response.inner.subtreePath; - } else { - success = false; - error_msg = response.message; - } - signal.signal_level(0); - }); - - signal.wait_for_level(); - - if (!success) { - throw std::runtime_error(error_msg); - } + insert_atomically(*wrapper.tree, [&](auto on_done) { + wrapper.tree->add_or_update_values( + leaves, subtree_depth, [&, on_done](const TypedResponse>& response) { + if (response.success) { + result.lowLeafWitnessData = *response.inner.lowLeafWitnessData; + result.sortedLeaves = *response.inner.sortedLeaves; + result.subtreePath = response.inner.subtreePath; + } + on_done(response); + }); + }); return result; } @@ -730,30 +765,19 @@ SequentialInsertionResult WorldState::insert_indexed_leaves(MerkleTreeId id, Fork::SharedPtr fork = retrieve_fork(fork_id); - Signal signal; SequentialInsertionResult result; const auto& wrapper = std::get>(fork->_trees.at(id)); - bool success = true; - std::string error_msg; - - wrapper.tree->add_or_update_values_sequentially( - leaves, [&](const TypedResponse>& response) { - if (response.success) { - result.lowLeafWitnessData = *response.inner.lowLeafWitnessData; - result.insertionWitnessData = *response.inner.insertionWitnessData; - } else { - success = false; - error_msg = response.message; - } - - signal.signal_level(0); - }); - signal.wait_for_level(); - - if (!success) { - throw std::runtime_error(error_msg); - } + insert_atomically(*wrapper.tree, [&](auto on_done) { + wrapper.tree->add_or_update_values_sequentially( + leaves, [&, on_done](const TypedResponse>& response) { + if (response.success) { + result.lowLeafWitnessData = *response.inner.lowLeafWitnessData; + result.insertionWitnessData = *response.inner.insertionWitnessData; + } + on_done(response); + }); + }); return result; } diff --git a/native-packages/wsdb/cpp/src/world_state/world_state.test.cpp b/native-packages/wsdb/cpp/src/world_state/world_state.test.cpp index faa2fea98da..570945e712f 100644 --- a/native-packages/wsdb/cpp/src/world_state/world_state.test.cpp +++ b/native-packages/wsdb/cpp/src/world_state/world_state.test.cpp @@ -591,6 +591,71 @@ TEST_F(WorldStateTest, NullifierBatchInsert) } } +TEST_F(WorldStateTest, FailedIndexedInsertionsLeaveTheForkUnchanged) +{ + // Failed insertions that repeat 150 sit between inserting 150 and 160: the later witnesses and state must match a + // world state that never saw them. + std::string clean_dir = random_temp_directory(); + std::filesystem::create_directories(clean_dir); + { + WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); + WorldState clean( + thread_pool_size, clean_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); + auto id = MerkleTreeId::NULLIFIER_TREE; + ws.batch_insert_indexed_leaves(id, { NullifierLeafValue(150) }, 0); + clean.batch_insert_indexed_leaves(id, { NullifierLeafValue(150) }, 0); + + EXPECT_THROW(ws.batch_insert_indexed_leaves( + id, { NullifierLeafValue(170), NullifierLeafValue(150) }, 1), + std::runtime_error); + EXPECT_THROW( + ws.insert_indexed_leaves(id, { NullifierLeafValue(180), NullifierLeafValue(150) }), + std::runtime_error); + + auto batch = ws.batch_insert_indexed_leaves(id, { NullifierLeafValue(160) }, 0); + auto clean_batch = clean.batch_insert_indexed_leaves(id, { NullifierLeafValue(160) }, 0); + EXPECT_EQ(batch.lowLeafWitnessData, clean_batch.lowLeafWitnessData); + auto sequential = ws.insert_indexed_leaves(id, { NullifierLeafValue(175) }); + auto clean_sequential = clean.insert_indexed_leaves(id, { NullifierLeafValue(175) }); + EXPECT_EQ(sequential.lowLeafWitnessData, clean_sequential.lowLeafWitnessData); + EXPECT_EQ(sequential.insertionWitnessData, clean_sequential.insertionWitnessData); + EXPECT_EQ(ws.get_state_reference(uncommitted_revision()), clean.get_state_reference(uncommitted_revision())); + } + std::filesystem::remove_all(clean_dir); +} + +TEST_F(WorldStateTest, CommitReportsTheStatusSummary) +{ + WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); + ws.append_leaves(MerkleTreeId::NOTE_HASH_TREE, { fr(42) }); + + WorldStateStatusFull status; + status.summary = { + .unfinalizedBlockNumber = 99, .finalizedBlockNumber = 99, .oldestHistoricalBlock = 99, .treesAreSynched = false + }; + EXPECT_TRUE(ws.commit(status).first); + + WorldStateStatusSummary expected; + ws.get_status_summary(expected); + EXPECT_EQ(status.summary, expected); +} + +TEST_F(WorldStateTest, EmptySlotsOfABatchReadAsTheEmptyLeaf) +{ + WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); + auto id = MerkleTreeId::NULLIFIER_TREE; + // A batch of zeros fills slots 128 to 131 with empty leaves; no node is stored beneath their subtree's root. + ws.batch_insert_indexed_leaves(id, std::vector(4, NullifierLeafValue(0)), 2); + WorldStateStatusFull status; + ws.commit(status); + + WorldStateRevision block_1{ .forkId = CANONICAL_FORK_ID, .blockNumber = 1, .includeUncommitted = false }; + for (const auto& revision : { uncommitted_revision(), committed_revision(), block_1 }) { + EXPECT_EQ(ws.get_indexed_leaf(revision, id, 130), IndexedLeaf::empty()); + EXPECT_THROW(ws.get_indexed_leaf(revision, id, 132), std::runtime_error); + } +} + TEST_F(WorldStateTest, PublicDataTree) { WorldState ws(thread_pool_size, data_dir, map_size, tree_heights, tree_prefill, initial_header_generator_point); diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp index a898b652d40..8ec46e94e7b 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp @@ -543,8 +543,11 @@ void handle_update_archive(WsdbRequest& ctx, void handle_commit(WsdbRequest& ctx, wire::WsdbCommit&& /*cmd*/, Responder respond) { schedule_write(ctx, 0, std::move(respond), [&ctx]() -> wire::WsdbCommitResponse { - WorldStateStatusFull status; - ctx.world_state.commit(status); + WorldStateStatusFull status{}; + auto [committed, message] = ctx.world_state.commit(status); + if (!committed) { + throw std::runtime_error(message); + } return wire::WsdbCommitResponse{ .status = status }; }); } From db47469c86395b82f94776fc8a960540893c8e48 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:48:12 +0000 Subject: [PATCH 06/20] feat(bb): wsdb-ref owns the wsdb schema and ships a server binary wsdb_schema.jsonc moves from native-packages/wsdb to barretenberg's wsdb_ref, next to its reference implementation. The AVM's wsdb client, wsdb-ref, the @aztec-foundation/wsdb-ref package and aztec-wsdb all generate from it there, which also removes bb's one read of a downstream package. aztec-wsdb reads it from the working tree for now; a NOTE in its CMakeLists.txt says to take it from the pinned sparse clone once native-packages leaves aztec-packages. wsdb-ref gains a binary, `wsdb-ref msgpack run --input `, serving the dispatch over a socket or shared memory. The package now runs wsdb-ref as a spawned process (uds or shm), in-process through the napi addon, or through the wasm module, and its tests run against all four. The differential test moves to native-packages/wsdb (wsdb_ref_diff.test.cpp), since wsdb-ref knows nothing of its downstream implementations. It spawns aztec-wsdb and the wsdb-ref binary and compares them over the wire, so no barretenberg code is compiled or linked into wsdb_tests (linking wsdb-ref into it clashes with aztec-wsdb's own msgpack-c). wsdb-tests now depends on bb-cpp-native for the binary, and wsdb-ref's sources are in aztec-wsdb's rebuild patterns. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G5s7nEWrYapDK2CWpEzcAf --- Makefile | 6 +- barretenberg/cpp/CMakePresets.json | 8 +- .../src/barretenberg/vm2_wsdb/CMakeLists.txt | 11 +- .../src/barretenberg/vm2_wsdb/wsdb_wire.hpp | 2 +- .../src/barretenberg/wsdb_ref/CMakeLists.txt | 27 ++- .../cpp/src/barretenberg/wsdb_ref/main.cpp | 34 ++++ .../wsdb_ref/wsdb_ref_handlers.hpp | 2 +- .../barretenberg/wsdb_ref}/wsdb_schema.jsonc | 0 barretenberg/ts/.rebuild_patterns | 2 +- barretenberg/ts/bootstrap.sh | 20 ++- barretenberg/ts/scripts/wsdb_ref.test.mjs | 18 +- barretenberg/ts/scripts/wsdb_ref_test.sh | 4 +- barretenberg/ts/yarn.lock | 2 + native-packages/wsdb/.rebuild_patterns | 3 +- native-packages/wsdb/README.md | 8 +- native-packages/wsdb/bootstrap.sh | 7 +- native-packages/wsdb/cpp/CMakeLists.txt | 26 ++- .../wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp | 158 +++++++++--------- 18 files changed, 205 insertions(+), 133 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp rename {native-packages/wsdb => barretenberg/cpp/src/barretenberg/wsdb_ref}/wsdb_schema.jsonc (100%) rename barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp => native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp (89%) diff --git a/Makefile b/Makefile index 9e28f6559c0..1c56f3019aa 100644 --- a/Makefile +++ b/Makefile @@ -436,14 +436,16 @@ wsdb: lmdblib # Native-package tests. lmdblib and wsdb are self-contained gtest binaries. kvdb has no # C++ tests: its NAPI protocol is driven from node (ts/test) and exercised further by # yarn-project's kv-store tests. wsdb_tests use no bb headers; the bb-header -# parity/equivalence target is manual (WSDB_BUILD_BB_TESTS). +# parity/equivalence target is manual (WSDB_BUILD_BB_TESTS). wsdb_tests compare aztec-wsdb with +# bb's wsdb-ref binary, which bb-cpp-native builds into the working tree (until native-packages +# leaves aztec-packages: see native-packages/wsdb/cpp/CMakeLists.txt). lmdblib-tests: lmdblib $(call test,$@,native-packages/lmdblib) kvdb-tests: kvdb $(call test,$@,native-packages/kvdb) -wsdb-tests: wsdb +wsdb-tests: wsdb bb-cpp-native $(call test,$@,native-packages/wsdb) #============================================================================== diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index 288d9b441a9..ef0cfde18ac 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -804,25 +804,25 @@ "name": "amd64-linux", "configurePreset": "amd64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb_ref_napi"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "arm64-linux", "configurePreset": "arm64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim", "wsdb_ref_napi"] + "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "amd64-macos", "configurePreset": "amd64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb_ref_napi"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "arm64-macos", "configurePreset": "arm64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb_ref_napi"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "amd64-windows", diff --git a/barretenberg/cpp/src/barretenberg/vm2_wsdb/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/vm2_wsdb/CMakeLists.txt index 0a3c1b70769..950c532eb95 100644 --- a/barretenberg/cpp/src/barretenberg/vm2_wsdb/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/vm2_wsdb/CMakeLists.txt @@ -1,13 +1,10 @@ # VM2 binding to the wsdb service: the generated IPC client plus the AVM adapter # (WsdbIpcMerkleDB) that drives native AVM simulation against aztec-wsdb. # -# The wsdb wire contract (wsdb_schema.jsonc) and the server live in the -# top-level native-packages/wsdb package, which owns the service. bb only -# needs the generated CLIENT, so it codegens from that external schema path. -# This is a deliberate (and currently the only) build-order inversion: -# bb reads native-packages/wsdb/wsdb_schema.jsonc at configure/build time. +# The wsdb wire contract (wsdb_schema.jsonc) lives with wsdb-ref, its reference implementation, in +# ../wsdb_ref. The AVM only needs the generated client. if(TARGET vm2_sim) - set(WSDB_SCHEMA ${CMAKE_SOURCE_DIR}/../../native-packages/wsdb/wsdb_schema.jsonc) + set(WSDB_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/../wsdb_ref/wsdb_schema.jsonc) # Generated code is per build tree (each preset generates its own, so presets building at once never # share a file). Remove the copy older checkouts generated into the source tree: it would shadow this one. file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/generated) @@ -37,7 +34,7 @@ if(TARGET vm2_sim) --cpp-namespace bb::wsdb --cpp-include-dir barretenberg/vm2_wsdb/generated DEPENDS ${WSDB_SCHEMA} ${IPC_CODEGEN_SRC} - COMMENT "Generating WSDB IPC client from native-packages/wsdb/wsdb_schema.jsonc" + COMMENT "Generating WSDB IPC client from wsdb_ref/wsdb_schema.jsonc" VERBATIM ) add_custom_target(wsdb_ipc_generated DEPENDS ${WSDB_GEN_OUTPUTS}) diff --git a/barretenberg/cpp/src/barretenberg/vm2_wsdb/wsdb_wire.hpp b/barretenberg/cpp/src/barretenberg/vm2_wsdb/wsdb_wire.hpp index cd4395a9e7c..41d09cbbc25 100644 --- a/barretenberg/cpp/src/barretenberg/vm2_wsdb/wsdb_wire.hpp +++ b/barretenberg/cpp/src/barretenberg/vm2_wsdb/wsdb_wire.hpp @@ -3,7 +3,7 @@ * @file wsdb_wire.hpp * @brief The boundary between bb's merkle-tree domain types and the generated wsdb wire types. * - * The generated `wire::` records (native-packages/wsdb/wsdb_schema.jsonc) are the serialisation + * The generated `wire::` records (wsdb_ref/wsdb_schema.jsonc) are the serialisation * types; bb's own merkle types are what the AVM computes with. `Wire` names a domain type's * wire counterpart and converts both ways: * diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt index 5ce22249897..ddb37d67229 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt @@ -1,11 +1,13 @@ # wsdb-ref: the in-memory reference world state (world_state_reference) as a service over the node -# world state's contract, native-packages/wsdb/wsdb_schema.jsonc (the schema the AVM's wsdb client in -# vm2_wsdb is generated from too). The generated dispatch runs the handlers here; the generated FFI -# entry (wsdb_ipc_ffi_entry) runs the dispatch in-process. Two artifacts carry that entry to -# @aztec-foundation/wsdb-ref (barretenberg/ts/wsdb-ref): +# world state's contract, wsdb_schema.jsonc here. wsdb-ref owns the schema: the AVM's wsdb client in +# vm2_wsdb and native-packages/wsdb's aztec-wsdb are generated from it too. The generated dispatch +# runs the handlers here; the generated FFI entry (wsdb_ipc_ffi_entry) runs the dispatch in-process. +# Three artifacts carry it to @aztec-foundation/wsdb-ref (barretenberg/ts/wsdb-ref), one per transport: +# - the wsdb-ref binary (native builds), the dispatch served over IPC, for its process transports +# (and for a downstream world state's conformance test, which runs it beside its own server); # - wsdb-ref.wasm, a WASI reactor (wasm builds), for the package's wasm transport; # - wsdb_ref_napi.node, a Node-API addon over the entry (native builds), for its napi transport. -set(WSDB_REF_SCHEMA ${CMAKE_SOURCE_DIR}/../../native-packages/wsdb/wsdb_schema.jsonc) +set(WSDB_REF_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_schema.jsonc) set(WSDB_REF_GEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/generated) set(WSDB_REF_GEN_OUTPUTS ${WSDB_REF_GEN_DIR}/wsdb_dispatch.hpp @@ -33,7 +35,7 @@ add_custom_command( --cpp-namespace bb::wsdb_ref --cpp-include-dir barretenberg/wsdb_ref/generated DEPENDS ${WSDB_REF_SCHEMA} ${IPC_CODEGEN_SRC} - COMMENT "Generating the wsdb-ref dispatch, FFI entry and Node-API addon from native-packages/wsdb/wsdb_schema.jsonc" + COMMENT "Generating the wsdb-ref dispatch, FFI entry and Node-API addon from wsdb_schema.jsonc" VERBATIM ) add_custom_target(wsdb_ref_generated DEPENDS ${WSDB_REF_GEN_OUTPUTS}) @@ -41,10 +43,6 @@ add_custom_target(wsdb_ref_generated DEPENDS ${WSDB_REF_GEN_OUTPUTS}) # Explicit sources: the generated Node-API addon must stay out of the module (it needs napi.h), and # the generated FFI entry does not exist yet when a clean tree is configured. file(GLOB WSDB_REF_TESTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.test.cpp) -if(WASM) - # The comparison with aztec-wsdb spawns it and talks over its socket. - list(FILTER WSDB_REF_TESTS EXCLUDE REGEX "wsdb_ref_native_diff\\.test\\.cpp$") -endif() barretenberg_module_with_sources( wsdb_ref SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_ref_handlers.cpp ${WSDB_REF_GEN_DIR}/wsdb_ffi.cpp @@ -56,11 +54,6 @@ add_dependencies(wsdb_ref_objects wsdb_ref_generated) if(TARGET wsdb_ref_test_objects) add_dependencies(wsdb_ref_test_objects wsdb_ref_generated) endif() -if(TARGET wsdb_ref_tests AND NOT WASM) - # wsdb_ref_native_diff.test.cpp reaches aztec-wsdb through ipc-runtime's socket client. - target_link_libraries(wsdb_ref_test_objects PRIVATE ipc_runtime) - target_link_libraries(wsdb_ref_tests PRIVATE ipc_runtime) -endif() set_target_properties(wsdb_ref_objects PROPERTIES POSITION_INDEPENDENT_CODE ON) if(WASM) @@ -97,6 +90,10 @@ else() string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}") string(REGEX REPLACE "[\r\n\"]" "" NODE_API_HEADERS_DIR "${NODE_API_HEADERS_DIR}") + add_executable(wsdb-ref ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp) + add_dependencies(wsdb-ref wsdb_ref_generated) + target_link_libraries(wsdb-ref PRIVATE wsdb_ref world_state_reference ipc_runtime) + add_library(wsdb_ref_napi SHARED ${WSDB_REF_GEN_DIR}/wsdb_napi.cpp) add_dependencies(wsdb_ref_napi wsdb_ref_generated) target_compile_definitions(wsdb_ref_napi PRIVATE NAPI_VERSION=9) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp new file mode 100644 index 00000000000..9546e0c357a --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp @@ -0,0 +1,34 @@ +/** + * @file main.cpp + * @brief The wsdb-ref binary: wsdb-ref served over IPC, for a client that runs its world state as a process. + * + * `wsdb-ref msgpack run --input ` serves one reference world state on a .sock or .shm path until a + * shutdown signal. It starts at the canonical genesis; a client sends InitGenesis for another. + */ +#include "barretenberg/bb/deps/cli11.hpp" +#include "barretenberg/common/log.hpp" +#include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" + +#include "barretenberg/wsdb_ref/generated/wsdb_ipc_server.hpp" + +#include + +int main(int argc, char* argv[]) +{ + CLI::App app{ "wsdb-ref: the reference world state served over IPC" }; + app.require_subcommand(1); + CLI::App* msgpack_command = app.add_subcommand("msgpack", "Msgpack API interface."); + CLI::App* run_command = msgpack_command->add_subcommand("run", "Start the wsdb-ref IPC server."); + std::string input_path; + run_command->add_option("-i,--input", input_path, "IPC socket/shm path (.sock or .shm)")->required(); + CLI11_PARSE(app, argc, argv); + + try { + bb::wsdb_ref::WsdbRefContext ctx; + bb::wsdb_ref::serve(input_path, ctx); + } catch (const std::exception& e) { + info("Error: ", e.what()); + return 1; + } + return 0; +} diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp index 91b36d4a2a8..cbfd41a2ee9 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp @@ -1,7 +1,7 @@ #pragma once /** * @file wsdb_ref_handlers.hpp - * @brief The node world state's contract (native-packages/wsdb/wsdb_schema.jsonc) served by the in-memory + * @brief The node world state's contract (wsdb_schema.jsonc) served by the in-memory * reference world state. * * The generated dispatch (generated/wsdb_dispatch.hpp) declares template handle_; the diff --git a/native-packages/wsdb/wsdb_schema.jsonc b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc similarity index 100% rename from native-packages/wsdb/wsdb_schema.jsonc rename to barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc diff --git a/barretenberg/ts/.rebuild_patterns b/barretenberg/ts/.rebuild_patterns index 365d7a9d0a3..d7bc14a2e98 100644 --- a/barretenberg/ts/.rebuild_patterns +++ b/barretenberg/ts/.rebuild_patterns @@ -6,4 +6,4 @@ ^barretenberg/cpp/src/barretenberg/bbapi/bb_schema\.json$ ^barretenberg/cpp/src/barretenberg/bbapi/bb_curve_constants\.json$ ^barretenberg/cpp/src/barretenberg/cdb/cdb_schema\.json$ -^native-packages/wsdb/wsdb_schema\.jsonc$ +^barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema\.jsonc$ diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index 8c9c904c095..9d45f3bbff5 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -9,6 +9,7 @@ CDB_PACKAGE=@aztec-foundation/cdb BB_JS_API_PACKAGE=@aztec-foundation/bb.js-api WSDB_REF_PACKAGE=@aztec-foundation/wsdb-ref WSDB_REF_ADDON=wsdb_ref_napi.node +WSDB_REF_BINARY=wsdb-ref hash=$(hash_str \ $(bb.js/bootstrap.sh hash) \ @@ -82,17 +83,20 @@ function generate_bb_js_api_package { --package-wasm-threads-module "barretenberg-threads.wasm=$ROOT/barretenberg/cpp/build-wasm-threads/bin/barretenberg.wasm" } -# The in-memory reference world state over the node world state's contract, fully generated: in-process -# through its wasm module (node and browsers) or, opted into here with napi, its Node-API addon. -# Both run the one FFI entry the bb build generates from the same schema (cpp/.../wsdb_ref). +# The in-memory reference world state over the node world state's contract, fully generated: a spawned +# wsdb-ref process over a socket or shared memory (node), in-process through its wasm module (node and +# browsers), or in-process through its Node-API addon (node). All three run the dispatch the bb build +# generates from the same schema (cpp/.../wsdb_ref). function generate_wsdb_ref_package { node --experimental-strip-types --no-warnings \ "$ROOT/ipc-codegen/src/generate.ts" \ - --schema "$ROOT/native-packages/wsdb/wsdb_schema.jsonc" \ + --schema "$ROOT/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc" \ --lang ts \ --package "$ROOT/barretenberg/ts/wsdb-ref" \ --package-name "$WSDB_REF_PACKAGE" \ - --package-transports napi,wasm \ + --package-transports uds,shm,napi,wasm \ + --package-ipc-path-args 'msgpack,run,--input,{path}' \ + --binary-name "$WSDB_REF_BINARY" \ --package-napi-addon "$WSDB_REF_ADDON" \ --package-napi-addon-env-var WSDB_REF_NAPI_PATH \ --package-wasm-module wsdb-ref.wasm @@ -193,6 +197,7 @@ function copy_wsdb_ref_native { local target_dir="wsdb-ref/build/$(arch)-$(os)" mkdir -p "$target_dir" cp "$ROOT/barretenberg/cpp/build/lib/$WSDB_REF_ADDON" "$target_dir/$WSDB_REF_ADDON" + cp "$ROOT/barretenberg/cpp/build/bin/$WSDB_REF_BINARY" "$target_dir/$WSDB_REF_BINARY" } function copy_wsdb_ref_cross { @@ -200,10 +205,13 @@ function copy_wsdb_ref_cross { local cross_arch="$1" mkdir -p "wsdb-ref/build/$cross_arch" cp "$ROOT/barretenberg/cpp/build-$cross_arch/lib/$WSDB_REF_ADDON" "wsdb-ref/build/$cross_arch/$WSDB_REF_ADDON" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/bin/$WSDB_REF_BINARY" "wsdb-ref/build/$cross_arch/$WSDB_REF_BINARY" elif semver check "${REF_NAME:-}" && [ "$(arch)" == "amd64" ]; then for cross_arch in arm64-linux amd64-macos arm64-macos; do mkdir -p "wsdb-ref/build/$cross_arch" cp "$ROOT/barretenberg/cpp/build-$cross_arch/lib/$WSDB_REF_ADDON" "wsdb-ref/build/$cross_arch/$WSDB_REF_ADDON" + cp "$ROOT/barretenberg/cpp/build-$cross_arch/bin/$WSDB_REF_BINARY" \ + "wsdb-ref/build/$cross_arch/$WSDB_REF_BINARY" done else echo "This task is expected to be run with an explicit arch or in an x86 release context." @@ -294,7 +302,7 @@ function test_cmds { ;; wsdb_ref) local backend - for backend in napi wasm; do + for backend in process-uds process-shm napi wasm; do echo "$hash barretenberg/ts/scripts/wsdb_ref_test.sh $backend" done ;; diff --git a/barretenberg/ts/scripts/wsdb_ref.test.mjs b/barretenberg/ts/scripts/wsdb_ref.test.mjs index ef5284c333c..84c8d871da9 100644 --- a/barretenberg/ts/scripts/wsdb_ref.test.mjs +++ b/barretenberg/ts/scripts/wsdb_ref.test.mjs @@ -1,5 +1,6 @@ // Tests of the generated @aztec-foundation/wsdb-ref package against one backend, from its build. -// Run through wsdb_ref_test.sh, which sets WSDB_REF_TEST_BACKEND to napi or wasm. +// Run through wsdb_ref_test.sh, which sets WSDB_REF_TEST_BACKEND to process-uds, process-shm (a spawned +// wsdb-ref binary over a socket or shared memory), napi or wasm. // // Every napi backend in a process shares one world state (node loads the addon once), so the // tests read the genesis through committed reads, which writes never change, and write only fresh @@ -13,8 +14,14 @@ import { after, before, test } from 'node:test'; import { WsdbError, WsdbService, WsdbServiceSync } from '@aztec-foundation/wsdb-ref'; -const backend = process.env.WSDB_REF_TEST_BACKEND; -assert.ok(backend === 'napi' || backend === 'wasm', `WSDB_REF_TEST_BACKEND must be napi or wasm, got ${backend}`); +const BACKENDS = { + 'process-uds': { backend: 'process', process: { transport: 'uds' } }, + 'process-shm': { backend: 'process', process: { transport: 'shm' } }, + napi: { backend: 'napi' }, + wasm: { backend: 'wasm' }, +}; +const options = BACKENDS[process.env.WSDB_REF_TEST_BACKEND]; +assert.ok(options, `WSDB_REF_TEST_BACKEND must be one of ${Object.keys(BACKENDS).join(', ')}`); const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim(); const constantsNr = readFileSync( @@ -40,7 +47,7 @@ const randomFr = () => new Uint8Array([0, ...randomBytes(31)]); let service; before(async () => { - service = await WsdbService.create({ backend }); + service = await WsdbService.create(options); }); after(async () => { await service.destroy(); @@ -120,7 +127,8 @@ test('forks are separate world states, and bad requests are errors, not crashes' }); test('the synchronous service reaches the same world state', async () => { - const sync = await WsdbServiceSync.create({ backend }); + // A synchronous process client is always over shared memory. + const sync = await WsdbServiceSync.create({ backend: options.backend }); try { const archive = sync.getTreeInfo({ treeId: ARCHIVE, revision: committed }); assert.equal(toBigInt(archive.root), protocolConstant('GENESIS_ARCHIVE_ROOT')); diff --git a/barretenberg/ts/scripts/wsdb_ref_test.sh b/barretenberg/ts/scripts/wsdb_ref_test.sh index 2fb0ba4878a..a146bdc9ebe 100755 --- a/barretenberg/ts/scripts/wsdb_ref_test.sh +++ b/barretenberg/ts/scripts/wsdb_ref_test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Runs the @aztec-foundation/wsdb-ref package tests against one backend: napi or wasm. +# Runs the @aztec-foundation/wsdb-ref package tests against one backend: process-uds, process-shm, napi or wasm. # Needs the package built (make bb-wsdb-ref, or ./bootstrap.sh build_wsdb_ref here). set -euo pipefail -backend=${1:?usage: wsdb_ref_test.sh } +backend=${1:?usage: wsdb_ref_test.sh } cd "$(dirname "$0")/.." WSDB_REF_TEST_BACKEND="$backend" node --test --test-reporter=spec scripts/wsdb_ref.test.mjs diff --git a/barretenberg/ts/yarn.lock b/barretenberg/ts/yarn.lock index 20edce0514a..63d65c5327c 100644 --- a/barretenberg/ts/yarn.lock +++ b/barretenberg/ts/yarn.lock @@ -215,6 +215,8 @@ __metadata: optional: true "@aztec-foundation/wsdb-ref-linux-x64": optional: true + bin: + wsdb-ref: ./dest/bin.js languageName: unknown linkType: soft diff --git a/native-packages/wsdb/.rebuild_patterns b/native-packages/wsdb/.rebuild_patterns index 66783cd75ab..e5227bf5c51 100644 --- a/native-packages/wsdb/.rebuild_patterns +++ b/native-packages/wsdb/.rebuild_patterns @@ -1,7 +1,8 @@ ^native-packages/wsdb/bootstrap\.sh$ ^native-packages/wsdb/package\.json$ ^native-packages/wsdb/\.rebuild_patterns$ -^native-packages/wsdb/wsdb_schema\.jsonc$ +^barretenberg/cpp/src/barretenberg/wsdb_ref/ +^barretenberg/cpp/src/barretenberg/world_state_reference/ ^native-packages/wsdb/cpp/ ^native-packages/lmdblib/ ^native-packages/wsdb/foundation\.pin$ diff --git a/native-packages/wsdb/README.md b/native-packages/wsdb/README.md index 414e9012320..824562deed8 100644 --- a/native-packages/wsdb/README.md +++ b/native-packages/wsdb/README.md @@ -1,19 +1,21 @@ # aztec-wsdb The world-state DB service: a persistent, content-addressed merkle store behind an IPC contract -(`wsdb_schema.jsonc`). `cpp/` is the C++ server and its tests, `ts/` the generated TypeScript +(`wsdb_schema.jsonc`, owned by barretenberg's wsdb-ref in `barretenberg/cpp/src/barretenberg/wsdb_ref/`). `cpp/` is the C++ server and its tests, `ts/` the generated TypeScript client package (`@aztec-foundation/wsdb`) that launches the server binary. ## Dependencies -wsdb compiles no barretenberg header. Everything it needs from outside this directory comes from -one aztec-packages release, pinned by tag in `foundation.pin` (`cpp/CMakeLists.txt`): +wsdb compiles no barretenberg header. Everything it needs from outside this directory, except the +schema, comes from one aztec-packages release, pinned by tag in `foundation.pin` (`cpp/CMakeLists.txt`): | Dependency | Source | |---|---| | barretenberg (`bb_poseidon2_hash*`, and the reference world state's `bb_wsref_*` C ABI for the conformance test) | `barretenberg-static--.tar.gz` from the barretenberg GitHub release of the tag | | ipc-runtime (C++, compiled in) and ipc-codegen (the generated dispatch) | a sparse partial clone of aztec-packages at the tag, holding only `ipc-runtime/cpp` and `ipc-codegen` (a few MB) | | protocol constants (`aztec_constants.hpp`) | `npx @aztec-foundation/constants-codegen@`, which embeds its release's `constants.nr` | +| the `wsdb-ref` binary (the differential test, `wsdb_ref_diff.test.cpp`, runs it beside `aztec-wsdb`) | the working tree's barretenberg build, `barretenberg/cpp/build/bin/wsdb-ref`; the test skips without it. When native-packages leaves aztec-packages, take it from the release instead (see the NOTE in `cpp/CMakeLists.txt`) | +| the wire contract (`wsdb_schema.jsonc`) | the working tree, `barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc`. When native-packages leaves aztec-packages, take it from the sparse clone instead (see the NOTE in `cpp/CMakeLists.txt`) | The lmdblib sibling package (`../lmdblib`) is always built from source. The TypeScript client is generated by `bootstrap.sh` from the same ipc-codegen. diff --git a/native-packages/wsdb/bootstrap.sh b/native-packages/wsdb/bootstrap.sh index 69109e3f66f..6d6ba73d7de 100755 --- a/native-packages/wsdb/bootstrap.sh +++ b/native-packages/wsdb/bootstrap.sh @@ -5,9 +5,10 @@ source $(git rev-parse --show-toplevel)/ci3/source_bootstrap ROOT=$(git rev-parse --show-toplevel) PKG="$ROOT/native-packages/wsdb" WSDB_BINARY=aztec-wsdb -# This package owns the wire contract; both the TS client and the C++ server -# codegen from this schema. -WSDB_SCHEMA="$PKG/wsdb_schema.jsonc" +# The wire contract, owned by barretenberg's wsdb-ref; both the TS client and the C++ server codegen +# from it. Read from the working tree for now: see the NOTE in cpp/CMakeLists.txt for where it comes +# from once native-packages leaves aztec-packages. +WSDB_SCHEMA="$ROOT/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc" # Everything outside this package comes from the release pinned in foundation.pin (see # cpp/CMakeLists.txt), so the build's identity is this package plus the lmdblib sibling. diff --git a/native-packages/wsdb/cpp/CMakeLists.txt b/native-packages/wsdb/cpp/CMakeLists.txt index 1e05a9f9cda..8ecd82ebdb6 100644 --- a/native-packages/wsdb/cpp/CMakeLists.txt +++ b/native-packages/wsdb/cpp/CMakeLists.txt @@ -50,19 +50,27 @@ set(IPC_RUNTIME_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(IPC_RUNTIME_BUILD_NAPI OFF CACHE BOOL "" FORCE) add_subdirectory(${foundation}/ipc-runtime/cpp ${deps}/ipc-runtime) -# The generated server dispatch and wire types (../wsdb_schema.jsonc owns the wire contract), and -# the protocol constants from @aztec-foundation/constants-codegen at the pinned version; both are -# regenerated whenever configure runs, which their inputs below trigger. +# The wire contract is wsdb_schema.jsonc, owned by barretenberg's wsdb-ref (the reference +# implementation). It is the one input read from the working tree rather than the pinned release. +# NOTE: when native-packages moves out of aztec-packages, take the schema from the sparse clone +# instead: add barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc to its sparse-checkout +# above and set wsdb_schema to ${foundation}/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc +# (bootstrap.sh's WSDB_SCHEMA and .rebuild_patterns follow). +set(wsdb_schema "${pkg}/../../barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc") + +# The generated server dispatch and wire types from that schema, and the protocol constants from +# @aztec-foundation/constants-codegen at the pinned version; both are regenerated whenever configure +# runs, which their inputs below trigger. execute_process( COMMAND node --experimental-strip-types --no-warnings ${foundation}/ipc-codegen/src/generate.ts - --schema ${pkg}/wsdb_schema.jsonc --lang cpp --out ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/generated + --schema ${wsdb_schema} --lang cpp --out ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/generated --server --cpp-namespace azteclabs::wsdb --cpp-include-dir wsdb/generated COMMAND_ERROR_IS_FATAL ANY) execute_process( COMMAND "${CMAKE_COMMAND}" -E env "WSDB_FOUNDATION_VERSION=${version}" "${pkg}/scripts/remake-constants.sh" COMMAND_ERROR_IS_FATAL ANY) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS - ${pkg}/wsdb_schema.jsonc ${pkg}/foundation.pin ${pkg}/scripts/constants-codegen/cpp.json) + ${wsdb_schema} ${pkg}/foundation.pin ${pkg}/scripts/constants-codegen/cpp.json) set(CLI11_BUILD_TESTS OFF CACHE INTERNAL "") set(CLI11_BUILD_EXAMPLES OFF CACHE INTERNAL "") @@ -87,6 +95,14 @@ target_link_libraries(aztec-wsdb PRIVATE ${libs} CLI11::CLI11) list(REMOVE_ITEM sources ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/main.cpp) add_executable(wsdb_tests ${sources} ${tests}) target_link_libraries(wsdb_tests PRIVATE ${libs} CLI11::CLI11 gmock_main gmock gtest) +# wsdb_ref_diff.test.cpp runs the server built alongside next to barretenberg's wsdb-ref binary, taken +# from the working tree's barretenberg build (the test skips when it is not built). +# NOTE: when native-packages moves out of aztec-packages, take wsdb-ref from the pinned release instead +# (its @aztec-foundation/wsdb-ref- package ships the binary), as for the schema above. +set(WSDB_REF_BIN "${pkg}/../../barretenberg/cpp/build/bin/wsdb-ref" CACHE FILEPATH "wsdb-ref binary") +add_dependencies(wsdb_tests aztec-wsdb) +target_compile_definitions(wsdb_tests PRIVATE AZTEC_WSDB_BIN="$" + WSDB_REF_BIN="${WSDB_REF_BIN}") add_executable(wsdb_bench ${sources} ${benches}) target_link_libraries(wsdb_bench PRIVATE ${libs} CLI11::CLI11 benchmark::benchmark_main) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp similarity index 89% rename from barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp rename to native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp index 34e8427412b..34c98d4fde1 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_native_diff.test.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp @@ -1,21 +1,24 @@ /** - * @file wsdb_ref_native_diff.test.cpp - * @brief wsdb-ref against the node world state (native-packages/wsdb's aztec-wsdb), request for request. + * @file wsdb_ref_diff.test.cpp + * @brief aztec-wsdb against wsdb-ref, barretenberg's reference world state, request for request. * - * Both serve wsdb_schema.jsonc, so one encoded request goes to both: wsdb-ref through its in-process FFI - * entry, aztec-wsdb through its socket. A seeded random run builds blocks the way a node does (effects on a + * Both serve wsdb_schema.jsonc, so one encoded request goes to both, each a child process reached over its + * socket: aztec-wsdb, and barretenberg's wsdb-ref binary (so no barretenberg code is compiled or linked + * here). A seeded random run builds blocks the way a node does (effects on a * fork, then SyncBlock), interleaved with reads at every revision, writes and checkpoints on forks, unwinds, - * finalization and history removal. Every response must be byte-identical, apart from the store statistics a - * full status carries; a request must fail on both or on neither. + * finalization and history removal. A request must fail on both or on neither, and every success must be + * byte-identical, apart from the store statistics a full status carries. Error messages are counted, not + * compared. * - * Opt-in: set WSDB_NATIVE_BIN to an aztec-wsdb binary built from the same schema. WSDB_DIFF_SEEDS (default 4) - * and WSDB_DIFF_STEPS (default 400) size the run. + * Skipped when the wsdb-ref binary (WSDB_REF_BIN, see cpp/CMakeLists.txt) is not built. WSDB_DIFF_SEEDS (default 4) and + * WSDB_DIFF_STEPS (default 400) size the run. */ -#include "barretenberg/aztec/aztec_constants.hpp" -#include "barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp" -#include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" -#include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" +#include "common/aztec_constants.hpp" +#include "field/field_element.hpp" #include "ipc_runtime/ipc_client.hpp" +#include "world_state/genesis_protocol_nullifiers.hpp" +#include "wsdb/generated/ipc_codegen/msgpack_adaptor.hpp" +#include "wsdb/generated/wsdb_types.hpp" #include @@ -35,8 +38,7 @@ extern char** environ; -using namespace bb; -using namespace bb::wsdb_ref; +using namespace azteclabs::wsdb; namespace { @@ -95,46 +97,54 @@ template Resp decode(const Frame& frame) return resp; } -/** aztec-wsdb in a child process, at the same genesis wsdb-ref is given. */ -class NativeWsdb { +/** aztec-wsdb's command line: this test's tree shapes and genesis, served on `socket`. */ +std::vector aztec_wsdb_command(const std::string& binary, + const std::filesystem::path& dir, + const std::string& socket) +{ + std::filesystem::create_directories(dir / "data"); + std::ostringstream nullifiers; + nullifiers << "["; + auto seeds = world_state::genesis_protocol_nullifiers(); + for (size_t i = 0; i < seeds.size(); ++i) { + std::ostringstream hex; + hex << seeds[i]; + nullifiers << (i > 0 ? "," : "") << "\"" << hex.str().substr(2) << "\""; + } + nullifiers << "]"; + std::ostringstream heights; + heights << "{0:" << NULLIFIER_TREE_HEIGHT << ",1:" << NOTE_HASH_TREE_HEIGHT << ",2:" << PUBLIC_DATA_TREE_HEIGHT + << ",3:" << L1_TO_L2_MSG_TREE_HEIGHT << ",4:" << ARCHIVE_HEIGHT << "}"; + return { + binary, + "msgpack", + "run", + "-i", + socket, + "-d", + (dir / "data").string(), + "-t", + "4", + "--tree-heights", + heights.str(), + "--tree-prefill", + "{0:128,2:128}", + "--initial-header-generator-point", + std::to_string(DOM_SEP__BLOCK_HEADER_HASH), + "--prefilled-public-data", + R"([["00000000000000000000000000000000000000000000000000000000000003e8","0000000000000000000000000000000000000000000000000000000000000007"]])", + "--prefilled-nullifiers", + nullifiers.str() + }; +} + +/** A wsdb server in a child process, reached over its socket. */ +class WsdbProcess { public: - NativeWsdb(const std::string& binary, const std::filesystem::path& dir) - : socket_path_((dir / "wsdb.sock").string()) + WsdbProcess(std::vector args, std::string socket_path) + : socket_path_(std::move(socket_path)) { - std::filesystem::create_directories(dir / "data"); - std::ostringstream nullifiers; - nullifiers << "["; - auto seeds = world_state::genesis_protocol_nullifiers(); - for (size_t i = 0; i < seeds.size(); ++i) { - std::ostringstream hex; - hex << seeds[i]; - nullifiers << (i > 0 ? "," : "") << "\"" << hex.str().substr(2) << "\""; - } - nullifiers << "]"; - std::ostringstream heights; - heights << "{0:" << NULLIFIER_TREE_HEIGHT << ",1:" << NOTE_HASH_TREE_HEIGHT << ",2:" << PUBLIC_DATA_TREE_HEIGHT - << ",3:" << L1_TO_L2_MSG_TREE_HEIGHT << ",4:" << ARCHIVE_HEIGHT << "}"; - std::vector args{ - binary, - "msgpack", - "run", - "-i", - socket_path_, - "-d", - (dir / "data").string(), - "-t", - "4", - "--tree-heights", - heights.str(), - "--tree-prefill", - "{0:128,2:128}", - "--initial-header-generator-point", - std::to_string(DOM_SEP__BLOCK_HEADER_HASH), - "--prefilled-public-data", - R"([["00000000000000000000000000000000000000000000000000000000000003e8","0000000000000000000000000000000000000000000000000000000000000007"]])", - "--prefilled-nullifiers", - nullifiers.str() - }; + const std::string binary = args[0]; std::vector argv; for (auto& arg : args) { argv.push_back(arg.data()); @@ -149,7 +159,7 @@ class NativeWsdb { } } - ~NativeWsdb() + ~WsdbProcess() { client_->close(); kill(pid_, SIGTERM); @@ -160,11 +170,11 @@ class NativeWsdb { std::vector call(const std::vector& request) { if (!client_->send(request.data(), request.size(), 0)) { - throw std::runtime_error("send to aztec-wsdb failed"); + throw std::runtime_error("send to " + socket_path_ + " failed"); } auto response = client_->receive(0); if (response.data() == nullptr) { - throw std::runtime_error("receive from aztec-wsdb failed"); + throw std::runtime_error("receive from " + socket_path_ + " failed"); } std::vector out(response.begin(), response.end()); client_->release(response.size()); @@ -177,21 +187,12 @@ class NativeWsdb { std::unique_ptr client_; }; -std::vector call_ref(const std::vector& request) -{ - uint8_t* output = nullptr; - size_t output_len = 0; - wsdb_ipc_ffi_entry(request.data(), request.size(), &output, &output_len); - std::vector frame(output, output + output_len); - wsdb_ipc_ffi_free(output); - return frame; -} - /** One differential run: a random but node-shaped workload, every request answered by both. */ class DiffRun { public: - DiffRun(NativeWsdb& native, uint64_t seed) - : native_(native) + DiffRun(WsdbProcess& ref, WsdbProcess& native, uint64_t seed) + : ref_(ref) + , native_(native) , rng_(seed) {} @@ -204,7 +205,7 @@ class DiffRun { template Frame both(const std::string& name, const Cmd& cmd) { auto request = encode_request(name, cmd); - Frame ref = parse(call_ref(request)); + Frame ref = parse(ref_.call(request)); Frame nat = parse(native_.call(request)); ++requests; if (std::getenv("WSDB_DIFF_TRACE") != nullptr) { @@ -266,7 +267,8 @@ class DiffRun { } private: - NativeWsdb& native_; + WsdbProcess& ref_; + WsdbProcess& native_; std::mt19937_64 rng_; size_t step_ = 0; std::vector forks_{ 0 }; @@ -415,7 +417,7 @@ class DiffRun { genesis.prefilledNullifiers.push_back(bytes_of(seed)); } auto request = encode_request("WsdbInitGenesis", genesis); - ASSERT_FALSE(parse(call_ref(request)).is_error()); + ASSERT_FALSE(parse(ref_.call(request)).is_error()); slots_.push_back(fr(1000)); both("WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}); } @@ -738,7 +740,7 @@ class DiffRun { if (fork == 0) { return false; } - return parse(call_ref(encode_request( + return parse(ref_.call(encode_request( "WsdbGetTreeInfo", wire::WsdbGetTreeInfo{ .treeId = 0, @@ -805,11 +807,10 @@ class DiffRun { } // namespace -TEST(WsdbRefNativeDiff, MatchesAztecWsdbRequestForRequest) +TEST(WsdbRefDiff, MatchesWsdbRefRequestForRequest) { - const char* binary = std::getenv("WSDB_NATIVE_BIN"); - if (binary == nullptr) { - GTEST_SKIP() << "set WSDB_NATIVE_BIN to an aztec-wsdb binary to compare against it"; + if (!std::filesystem::exists(WSDB_REF_BIN)) { + GTEST_SKIP() << "no wsdb-ref binary at " << WSDB_REF_BIN << " (build barretenberg's wsdb-ref target)"; } size_t seeds = std::getenv("WSDB_DIFF_SEEDS") ? std::stoul(std::getenv("WSDB_DIFF_SEEDS")) : 4; size_t steps = std::getenv("WSDB_DIFF_STEPS") ? std::stoul(std::getenv("WSDB_DIFF_STEPS")) : 400; @@ -817,8 +818,11 @@ TEST(WsdbRefNativeDiff, MatchesAztecWsdbRequestForRequest) auto dir = std::filesystem::temp_directory_path() / ("wsdb-ref-diff-" + std::to_string(getpid()) + "-" + std::to_string(seed)); { - NativeWsdb native(binary, dir); - DiffRun run(native, seed); + auto native_socket = (dir / "aztec-wsdb.sock").string(); + auto ref_socket = (dir / "wsdb-ref.sock").string(); + WsdbProcess native(aztec_wsdb_command(AZTEC_WSDB_BIN, dir, native_socket), native_socket); + WsdbProcess ref({ WSDB_REF_BIN, "msgpack", "run", "--input", ref_socket }, ref_socket); + DiffRun run(ref, native, seed); run.run(steps); std::cout << "seed " << seed << ": " << run.requests << " requests, " << run.errors << " answered with errors on both, " << run.message_mismatches.size() From 8e0974e0906e8e13ef07e6ca8cedae185b989b11 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:41:43 +0000 Subject: [PATCH 07/20] feat(bb): InitGenesis carries the tree heights and initial sizes InitGenesis gains treeHeights (each of the five trees once) and treePrefill (the nullifier and public data trees' initial sizes, prefill included), the two maps aztec-wsdb takes as --tree-heights and --tree-prefill. With them InitGenesis holds a world state's whole genesis, so aztec-wsdb can take it over the wire instead of its command line once native-packages lives in labs, without another schema change here. wsdb-ref builds its trees from them rather than from the protocol constants, and rejects what aztec-wsdb rejects (an initial size below 2, more prefill than initial size) plus a malformed list. The differential test passes the same heights and sizes to both servers, and alternates seeds between the protocol's initial sizes and others. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G5s7nEWrYapDK2CWpEzcAf --- .../reference_world_state.cpp | 65 +++++++++++------- .../reference_world_state.hpp | 18 +++-- .../barretenberg/wsdb_ref/wsdb_ref.test.cpp | 68 ++++++++++++++++++- .../wsdb_ref/wsdb_ref_handlers.cpp | 35 +++++++++- .../barretenberg/wsdb_ref/wsdb_schema.jsonc | 15 +++- .../wsdb/cpp/src/wsdb/wsdb_handlers.cpp | 5 +- .../wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp | 43 ++++++++---- 7 files changed, 200 insertions(+), 49 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp index 4e2fd4e901d..3ab1eb2f751 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.cpp @@ -68,11 +68,24 @@ ReferenceWorldState::ReferenceWorldState(const GenesisConfig& genesis) std::optional ReferenceWorldState::check_genesis(const GenesisConfig& genesis) { - auto check = [](const auto& prefilled) -> std::optional { - if (prefilled.size() > INDEXED_TREE_INITIAL_SIZE) { + for (auto tree_id : ALL_TREES) { + uint32_t height = genesis.tree_heights[static_cast(tree_id)]; + if (height == 0 || height > 63) { + return "tree " + std::to_string(static_cast(tree_id)) + ": height " + std::to_string(height) + + " is not in 1..63"; + } + } + auto check = [&](const auto& prefilled, uint64_t initial_size, MerkleTreeId tree_id) -> std::optional { + if (initial_size < 2) { + return "Indexed trees must have initial size > 1"; + } + if (initial_size > (uint64_t(1) << genesis.tree_heights[static_cast(tree_id)])) { + return "Initial size is more than the tree holds"; + } + if (prefilled.size() > initial_size) { return "Number of prefilled values can't be more than initial size"; } - size_t num_padding = INDEXED_TREE_INITIAL_SIZE - prefilled.size(); + size_t num_padding = static_cast(initial_size - prefilled.size()); using LeafType = std::remove_cvref_t; uint256_t previous = num_padding > 0 ? uint256_t(LeafType::padding(num_padding - 1).get_key()) : uint256_t(0); for (size_t i = 0; i < prefilled.size(); ++i) { @@ -85,10 +98,12 @@ std::optional ReferenceWorldState::check_genesis(const GenesisConfi } return std::nullopt; }; - if (auto problem = check(genesis.prefilled_nullifiers)) { + if (auto problem = + check(genesis.prefilled_nullifiers, genesis.nullifier_tree_initial_size, MerkleTreeId::NULLIFIER_TREE)) { return "prefilled nullifiers: " + *problem; } - if (auto problem = check(genesis.prefilled_public_data)) { + if (auto problem = check( + genesis.prefilled_public_data, genesis.public_data_tree_initial_size, MerkleTreeId::PUBLIC_DATA_TREE)) { return "prefilled public data: " + *problem; } return std::nullopt; @@ -99,6 +114,7 @@ void ReferenceWorldState::reset(const GenesisConfig& genesis) if (auto problem = check_genesis(genesis)) { fail(*problem); } + tree_heights_ = genesis.tree_heights; blocks_.clear(); forks_.clear(); blocks_.push_back(build_genesis(genesis)); @@ -108,32 +124,24 @@ void ReferenceWorldState::reset(const GenesisConfig& genesis) oldest_historical_block_ = 0; } -uint32_t ReferenceWorldState::tree_height(MerkleTreeId tree_id) +uint32_t ReferenceWorldState::tree_height(MerkleTreeId tree_id) const { - switch (tree_id) { - case MerkleTreeId::NULLIFIER_TREE: - return NULLIFIER_TREE_HEIGHT; - case MerkleTreeId::NOTE_HASH_TREE: - return NOTE_HASH_TREE_HEIGHT; - case MerkleTreeId::PUBLIC_DATA_TREE: - return PUBLIC_DATA_TREE_HEIGHT; - case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: - return L1_TO_L2_MSG_TREE_HEIGHT; - case MerkleTreeId::ARCHIVE: - return ARCHIVE_HEIGHT; + auto index = static_cast(tree_id); + if (index >= tree_heights_.size()) { + fail("Unknown tree id ", static_cast(tree_id)); } - fail("Unknown tree id ", static_cast(tree_id)); + return tree_heights_[index]; } ReferenceWorldState::Trees ReferenceWorldState::build_genesis(const GenesisConfig& genesis) { // The indexed trees start as on a node: LeafType::padding(i) for the leaves the prefill leaves // free, then the prefill, linked as one ascending chain. - auto indexed = [](auto tree, const auto& prefilled) { + auto indexed = [](auto tree, const auto& prefilled, uint64_t initial_size) { using Tree = decltype(tree); using LeafType = std::remove_cvref_t; std::vector> leaves; - size_t num_padding = INDEXED_TREE_INITIAL_SIZE - prefilled.size(); + size_t num_padding = static_cast(initial_size - prefilled.size()); for (size_t i = 0; i < num_padding; ++i) { leaves.push_back(IndexedLeaf(LeafType::padding(i), 0, 0)); } @@ -152,16 +160,21 @@ ReferenceWorldState::Trees ReferenceWorldState::build_genesis(const GenesisConfi tree.size = leaves.size(); return tree; }; - auto append_only = [](MerkleTreeId tree_id) { - return AppendOnlyTree{ .tree = decltype(AppendOnlyTree::tree)(tree_height(tree_id)), .indices = {}, .size = 0 }; + auto height = [&](MerkleTreeId tree_id) { return genesis.tree_heights[static_cast(tree_id)]; }; + auto append_only = [&](MerkleTreeId tree_id) { + return AppendOnlyTree{ .tree = decltype(AppendOnlyTree::tree)(height(tree_id)), .indices = {}, .size = 0 }; }; Trees trees{ - .nullifier = indexed(NullifierTree{ .tree = decltype(NullifierTree::tree)(NULLIFIER_TREE_HEIGHT) }, - genesis.prefilled_nullifiers), + .nullifier = + indexed(NullifierTree{ .tree = decltype(NullifierTree::tree)(height(MerkleTreeId::NULLIFIER_TREE)) }, + genesis.prefilled_nullifiers, + genesis.nullifier_tree_initial_size), .note_hash = append_only(MerkleTreeId::NOTE_HASH_TREE), - .public_data = indexed(PublicDataTree{ .tree = decltype(PublicDataTree::tree)(PUBLIC_DATA_TREE_HEIGHT) }, - genesis.prefilled_public_data), + .public_data = + indexed(PublicDataTree{ .tree = decltype(PublicDataTree::tree)(height(MerkleTreeId::PUBLIC_DATA_TREE)) }, + genesis.prefilled_public_data, + genesis.public_data_tree_initial_size), .l1_to_l2 = append_only(MerkleTreeId::L1_TO_L2_MESSAGE_TREE), .archive = append_only(MerkleTreeId::ARCHIVE), }; diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp index ecc9ea31058..552699d3ee7 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/reference_world_state.hpp @@ -32,10 +32,17 @@ using numeric::uint256_t; /** * @brief The genesis a world state starts from, as a node world state is configured with it. - * @details canonical() is the protocol's: the protocol contracts' registration nullifiers, no public - * data, timestamp 0. + * @details canonical() is the protocol's: the protocol's tree heights and initial sizes, the protocol contracts' + * registration nullifiers, no public data, timestamp 0. */ struct GenesisConfig { + /** Each tree's height, by MerkleTreeId. */ + std::array tree_heights = { + NULLIFIER_TREE_HEIGHT, NOTE_HASH_TREE_HEIGHT, PUBLIC_DATA_TREE_HEIGHT, L1_TO_L2_MSG_TREE_HEIGHT, ARCHIVE_HEIGHT + }; + /** Leaves each indexed tree starts with, its prefill included. */ + uint64_t nullifier_tree_initial_size = 128; + uint64_t public_data_tree_initial_size = 128; std::vector prefilled_nullifiers; std::vector prefilled_public_data; uint64_t genesis_timestamp = 0; @@ -101,7 +108,7 @@ template struct BatchInsertionResult { */ class ReferenceWorldState { public: - /** Leaves each indexed tree starts with, prefill included, as on a node. */ + /** Leaves each indexed tree of the canonical genesis starts with, prefill included, as on a node. */ static constexpr size_t INDEXED_TREE_INITIAL_SIZE = 128; explicit ReferenceWorldState(const GenesisConfig& genesis = GenesisConfig::canonical()); @@ -112,8 +119,8 @@ class ReferenceWorldState { /** Restarts from `genesis`: every block, fork and uncommitted write is dropped. */ void reset(const GenesisConfig& genesis); - /** The tree height the protocol fixes for `tree_id`. */ - static uint32_t tree_height(MerkleTreeId tree_id); + /** The height this world state's genesis gives `tree_id`. */ + uint32_t tree_height(MerkleTreeId tree_id) const; // Reads. std::vector get_state_reference(const WorldStateRevision& revision) const; @@ -272,6 +279,7 @@ class ReferenceWorldState { static SequentialInsertionResult sequential_insert(Tree& tree, std::span leaves); + std::array tree_heights_{}; std::vector blocks_; std::map forks_; uint64_t next_fork_id_ = 1; diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp index 97e5da06494..b8b67258b1e 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp @@ -210,6 +210,70 @@ TEST(WsdbRef, ForksAndHistoryOverTheWire) std::string::npos); } +std::vector protocol_tree_heights() +{ + return { { .treeId = 0, .height = NULLIFIER_TREE_HEIGHT }, + { .treeId = 1, .height = NOTE_HASH_TREE_HEIGHT }, + { .treeId = 2, .height = PUBLIC_DATA_TREE_HEIGHT }, + { .treeId = 3, .height = L1_TO_L2_MSG_TREE_HEIGHT }, + { .treeId = 4, .height = ARCHIVE_HEIGHT } }; +} + +TEST(WsdbRef, InitGenesisSetsTreeHeightsAndInitialSizes) +{ + Service service; + auto heights = protocol_tree_heights(); + heights[1].height = 3; + ok(service.call( + "WsdbInitGenesis", + wire::WsdbInitGenesis{ .treeHeights = heights, + .treePrefill = { { .treeId = 0, .size = 4 }, { .treeId = 2, .size = 8 } }, + .prefilledPublicData = {}, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }, + "WsdbInitGenesisResponse")); + auto tree_info = [&](uint32_t tree_id) { + return ok(service.call( + "WsdbGetTreeInfo", + wire::WsdbGetTreeInfo{ .treeId = tree_id, .revision = LATEST_COMMITTED }, + "WsdbGetTreeInfoResponse")); + }; + EXPECT_EQ(tree_info(0).size, 4); + EXPECT_EQ(tree_info(2).size, 8); + EXPECT_EQ(tree_info(1).depth, 3); + // A three-level note hash tree holds eight leaves. + auto append = [&](size_t count) { + return service.call( + "WsdbAppendLeaves", + wire::WsdbAppendLeaves{ + .treeId = 1, .leaves = std::vector(count, bytes_of(fr(1))), .forkId = 0 }, + "WsdbAppendLeavesResponse"); + }; + ok(append(8)); + EXPECT_FALSE(std::holds_alternative(append(1))); + + auto rejected = [&](std::vector tree_heights, std::vector prefill) { + return error_of( + service.call("WsdbInitGenesis", + wire::WsdbInitGenesis{ .treeHeights = std::move(tree_heights), + .treePrefill = std::move(prefill), + .prefilledPublicData = {}, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = 0 }, + "WsdbInitGenesisResponse")); + }; + const std::vector both = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }; + EXPECT_NE(rejected({ heights.begin(), heights.end() - 1 }, both).find("each of the five trees"), std::string::npos); + EXPECT_NE(rejected(protocol_tree_heights(), { both[0] }).find("nullifier and public data"), std::string::npos); + EXPECT_NE( + rejected(protocol_tree_heights(), { both[0], both[1], { .treeId = 1, .size = 4 } }).find("not an indexed tree"), + std::string::npos); + EXPECT_NE(rejected(protocol_tree_heights(), { { .treeId = 0, .size = 1 }, both[1] }).find("initial size > 1"), + std::string::npos); +} + TEST(WsdbRef, InitGenesisRestartsAtTheGivenGenesis) { Service service; @@ -223,7 +287,9 @@ TEST(WsdbRef, InitGenesisRestartsAtTheGivenGenesis) EXPECT_EQ(nullifier_root(), fr(GENESIS_NULLIFIER_TREE_ROOT)); ok(service.call( "WsdbInitGenesis", - wire::WsdbInitGenesis{ .prefilledPublicData = { { .slot = bytes_of(fr(1000)), + wire::WsdbInitGenesis{ .treeHeights = protocol_tree_heights(), + .treePrefill = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }, + .prefilledPublicData = { { .slot = bytes_of(fr(1000)), .value = bytes_of(fr(5)) } }, .prefilledNullifiers = {}, .genesisTimestamp = 0, diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp index eef30178b1c..79b2e79a2ab 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp @@ -1,6 +1,8 @@ #include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" #include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" +#include +#include #include #include #include @@ -327,7 +329,7 @@ void handle_get_tree_info(WsdbRefContext& ctx, return { .treeId = cmd.treeId, .root = from_fr(info.root), .size = info.size, - .depth = ReferenceWorldState::tree_height(id) }; + .depth = ctx.world_state.tree_height(id) }; }); } @@ -792,6 +794,37 @@ void handle_init_genesis(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbInitGenesisResponse { GenesisConfig genesis; + std::array has_height{}; + for (const auto& [tree_id, height] : cmd.treeHeights) { + auto id = tree_id_of(tree_id); + if (has_height[static_cast(id)]) { + throw std::runtime_error("treeHeights names tree " + std::to_string(tree_id) + " twice"); + } + has_height[static_cast(id)] = true; + genesis.tree_heights[static_cast(id)] = height; + } + if (std::ranges::find(has_height, false) != has_height.end()) { + throw std::runtime_error("treeHeights must name each of the five trees"); + } + std::optional nullifier_size; + std::optional public_data_size; + for (const auto& [tree_id, size] : cmd.treePrefill) { + auto id = tree_id_of(tree_id); + if (id != MerkleTreeId::NULLIFIER_TREE && id != MerkleTreeId::PUBLIC_DATA_TREE) { + throw std::runtime_error("treePrefill names tree " + std::to_string(tree_id) + + ", which is not an indexed tree"); + } + auto& slot = id == MerkleTreeId::NULLIFIER_TREE ? nullifier_size : public_data_size; + if (slot.has_value()) { + throw std::runtime_error("treePrefill names tree " + std::to_string(tree_id) + " twice"); + } + slot = size; + } + if (!nullifier_size || !public_data_size) { + throw std::runtime_error("treePrefill must give the nullifier and public data trees their sizes"); + } + genesis.nullifier_tree_initial_size = *nullifier_size; + genesis.public_data_tree_initial_size = *public_data_size; genesis.prefilled_public_data = all_from_wire(cmd.prefilledPublicData); for (const auto& nullifier : cmd.prefilledNullifiers) { genesis.prefilled_nullifiers.emplace_back(to_fr(nullifier)); diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc index e90e209b4ea..3b3c6b63c5d 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc @@ -17,6 +17,14 @@ "blockNumber": "BlockNumber", "includeUncommitted": "bool" }, + "TreeHeight": { + "treeId": "MerkleTreeId", + "height": "u32" + }, + "TreePrefill": { + "treeId": "MerkleTreeId", + "size": "LeafIndex" + }, "TreeStateReference": { "treeId": "MerkleTreeId", "root": "Fr", @@ -473,10 +481,13 @@ }, "response": {} }, - // Restarts an in-process world state (wsdb-ref) at this genesis, before any block. aztec-wsdb takes its - // genesis on the command line and answers this with an error. + // Restarts an in-process world state (wsdb-ref) at this genesis, before any block. treeHeights names each + // of the five trees once; treePrefill gives each indexed tree (nullifier, public data) its initial size, + // prefill included. aztec-wsdb takes its genesis on the command line and answers this with an error. "InitGenesis": { "request": { + "treeHeights": "TreeHeight[]", + "treePrefill": "TreePrefill[]", "prefilledPublicData": "PublicDataLeafValue[]", "prefilledNullifiers": "Fr[]", "genesisTimestamp": "u64", diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp index 8ec46e94e7b..ada871bb7a0 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp @@ -768,8 +768,9 @@ void handle_init_genesis(WsdbRequest& /*ctx*/, wire::WsdbInitGenesis&& /*cmd*/, Responder respond) { - respond.error("aztec-wsdb takes its genesis on the command line (--prefilled-public-data, --prefilled-nullifiers, " - "--genesis-timestamp, --initial-header-generator-point), not over InitGenesis"); + respond.error("aztec-wsdb takes its genesis on the command line (--tree-heights, --tree-prefill, " + "--prefilled-public-data, --prefilled-nullifiers, --genesis-timestamp, " + "--initial-header-generator-point), not over InitGenesis"); } } // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp index 34c98d4fde1..66309c6dd86 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp @@ -97,10 +97,17 @@ template Resp decode(const Frame& frame) return resp; } +/** The indexed trees' initial sizes, prefill included: a run's one varied part of the genesis. */ +struct InitialSizes { + uint64_t nullifier; + uint64_t public_data; +}; + /** aztec-wsdb's command line: this test's tree shapes and genesis, served on `socket`. */ std::vector aztec_wsdb_command(const std::string& binary, const std::filesystem::path& dir, - const std::string& socket) + const std::string& socket, + const InitialSizes& sizes) { std::filesystem::create_directories(dir / "data"); std::ostringstream nullifiers; @@ -128,7 +135,7 @@ std::vector aztec_wsdb_command(const std::string& binary, "--tree-heights", heights.str(), "--tree-prefill", - "{0:128,2:128}", + "{0:" + std::to_string(sizes.nullifier) + ",2:" + std::to_string(sizes.public_data) + "}", "--initial-header-generator-point", std::to_string(DOM_SEP__BLOCK_HEADER_HASH), "--prefilled-public-data", @@ -190,9 +197,10 @@ class WsdbProcess { /** One differential run: a random but node-shaped workload, every request answered by both. */ class DiffRun { public: - DiffRun(WsdbProcess& ref, WsdbProcess& native, uint64_t seed) + DiffRun(WsdbProcess& ref, WsdbProcess& native, const InitialSizes& sizes, uint64_t seed) : ref_(ref) , native_(native) + , sizes_(sizes) , rng_(seed) {} @@ -269,6 +277,7 @@ class DiffRun { private: WsdbProcess& ref_; WsdbProcess& native_; + InitialSizes sizes_; std::mt19937_64 rng_; size_t step_ = 0; std::vector forks_{ 0 }; @@ -406,13 +415,21 @@ class DiffRun { void init() { - // The genesis aztec-wsdb was started with: the protocol nullifiers and one public-data slot. - wire::WsdbInitGenesis genesis{ .prefilledPublicData = { { .slot = bytes_of(fr(1000)), - .value = bytes_of(fr(7)) } }, - .prefilledNullifiers = {}, - .genesisTimestamp = 0, - .initialHeaderGeneratorPoint = - static_cast(DOM_SEP__BLOCK_HEADER_HASH) }; + // The genesis aztec-wsdb was started with: the protocol's tree heights, this run's initial sizes, the + // protocol nullifiers and one public-data slot. + wire::WsdbInitGenesis genesis{ + .treeHeights = { { .treeId = 0, .height = NULLIFIER_TREE_HEIGHT }, + { .treeId = 1, .height = NOTE_HASH_TREE_HEIGHT }, + { .treeId = 2, .height = PUBLIC_DATA_TREE_HEIGHT }, + { .treeId = 3, .height = L1_TO_L2_MSG_TREE_HEIGHT }, + { .treeId = 4, .height = ARCHIVE_HEIGHT } }, + .treePrefill = { { .treeId = 0, .size = sizes_.nullifier }, { .treeId = 2, .size = sizes_.public_data } }, + .prefilledPublicData = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(7)) } }, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) + }; for (const auto& seed : world_state::genesis_protocol_nullifiers()) { genesis.prefilledNullifiers.push_back(bytes_of(seed)); } @@ -820,9 +837,11 @@ TEST(WsdbRefDiff, MatchesWsdbRefRequestForRequest) { auto native_socket = (dir / "aztec-wsdb.sock").string(); auto ref_socket = (dir / "wsdb-ref.sock").string(); - WsdbProcess native(aztec_wsdb_command(AZTEC_WSDB_BIN, dir, native_socket), native_socket); + // Odd seeds run the protocol's initial sizes, even ones others. + InitialSizes sizes = seed % 2 == 1 ? InitialSizes{ 128, 128 } : InitialSizes{ 64, 256 }; + WsdbProcess native(aztec_wsdb_command(AZTEC_WSDB_BIN, dir, native_socket, sizes), native_socket); WsdbProcess ref({ WSDB_REF_BIN, "msgpack", "run", "--input", ref_socket }, ref_socket); - DiffRun run(ref, native, seed); + DiffRun run(ref, native, sizes, seed); run.run(steps); std::cout << "seed " << seed << ": " << run.requests << " requests, " << run.errors << " answered with errors on both, " << run.message_mismatches.size() From a3e6426aa49a97965cb8eef54174617e36837539 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:01:26 +0000 Subject: [PATCH 08/20] chore(bb.js): install wsdb-ref.wasm through the package codegen --- barretenberg/ts/bootstrap.sh | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index 9d45f3bbff5..d0f260864b9 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -99,7 +99,7 @@ function generate_wsdb_ref_package { --binary-name "$WSDB_REF_BINARY" \ --package-napi-addon "$WSDB_REF_ADDON" \ --package-napi-addon-env-var WSDB_REF_NAPI_PATH \ - --package-wasm-module wsdb-ref.wasm + --package-wasm-module "wsdb-ref.wasm=$ROOT/barretenberg/cpp/build-wasm/bin/wsdb-ref.wasm" } # bb-avm-sim, cdb, bb.js-api and wsdb-ref are gitignored workspaces declared in package.json, so @@ -186,13 +186,6 @@ function build_bb_js { (cd bb.js && ./bootstrap.sh) } -# The single-thread module only: the reference world state runs no threads. -function copy_wsdb_ref_wasm { - rm -rf wsdb-ref/wasm - mkdir -p wsdb-ref/wasm - cp "$ROOT/barretenberg/cpp/build-wasm/bin/wsdb-ref.wasm" wsdb-ref/wasm/wsdb-ref.wasm -} - function copy_wsdb_ref_native { local target_dir="wsdb-ref/build/$(arch)-$(os)" mkdir -p "$target_dir" @@ -225,7 +218,6 @@ function prepare_wsdb_ref_arch_packages { function build_wsdb_ref { echo_header "wsdb-ref package build" generate_packages - copy_wsdb_ref_wasm copy_wsdb_ref_native npm_install_deps "$IPC_RUNTIME_PKG" yarn workspace "$WSDB_REF_PACKAGE" build @@ -385,7 +377,6 @@ function release_bb_avm_sim { function release_wsdb_ref { generate_packages - copy_wsdb_ref_wasm copy_wsdb_ref_native copy_wsdb_ref_cross npm_install_deps "$IPC_RUNTIME_PKG" @@ -441,7 +432,7 @@ function release { export -f generate_bb_avm_sim_package copy_bb_avm_sim_native copy_bb_avm_sim_cross generate_cdb_package generate_packages export -f generate_bb_js_api_package copy_bb_js_api_native copy_bb_js_api_cross prepare_bb_js_api_arch_packages build_bb_js_api_ts build_bb_js_api export -f build_bb_js build_bb_avm_sim build_cdb build cross_copy_bb_js cross_copy_bb_avm_sim cross_copy_bb_js_api release release_cdb release_bb_js_api -export -f generate_wsdb_ref_package copy_wsdb_ref_wasm copy_wsdb_ref_native copy_wsdb_ref_cross prepare_wsdb_ref_arch_packages build_wsdb_ref cross_copy_wsdb_ref release_wsdb_ref +export -f generate_wsdb_ref_package copy_wsdb_ref_native copy_wsdb_ref_cross prepare_wsdb_ref_arch_packages build_wsdb_ref cross_copy_wsdb_ref release_wsdb_ref case "$cmd" in "") From cb62937a1c3af238a0ad11e8bfc762fb65db6a10 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:02:38 +0000 Subject: [PATCH 09/20] fix(wsdb): word InitGenesis and its aztec-wsdb error without assuming how the service runs --- .../cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc | 6 +++--- native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc index 3b3c6b63c5d..8e63ed03635 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc @@ -481,9 +481,9 @@ }, "response": {} }, - // Restarts an in-process world state (wsdb-ref) at this genesis, before any block. treeHeights names each - // of the five trees once; treePrefill gives each indexed tree (nullifier, public data) its initial size, - // prefill included. aztec-wsdb takes its genesis on the command line and answers this with an error. + // Restarts the world state at this genesis, before any block. treeHeights names each of the five trees + // once; treePrefill gives each indexed tree (nullifier, public data) its initial size, prefill included. + // A service whose genesis is fixed when it is created (aztec-wsdb) answers this with an error. "InitGenesis": { "request": { "treeHeights": "TreeHeight[]", diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp index ada871bb7a0..19103129d68 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp @@ -762,15 +762,12 @@ void handle_copy_stores(WsdbRequest& ctx, wire::WsdbCopyStores&& cmd, Responder< }); } -// aztec-wsdb takes its genesis on the command line, before it opens its stores. InitGenesis is for in-process world -// states (wsdb-ref), which have no command line. +// aztec-wsdb opens its stores at its genesis when it is constructed, so it cannot take a genesis afterwards. void handle_init_genesis(WsdbRequest& /*ctx*/, wire::WsdbInitGenesis&& /*cmd*/, Responder respond) { - respond.error("aztec-wsdb takes its genesis on the command line (--tree-heights, --tree-prefill, " - "--prefilled-public-data, --prefilled-nullifiers, --genesis-timestamp, " - "--initial-header-generator-point), not over InitGenesis"); + respond.error("aztec-wsdb does not support InitGenesis: its genesis is fixed when the service is created"); } } // namespace azteclabs::wsdb From 0110275856238b3c6f4edfdc308e88ecc6f7d106 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:06:50 +0000 Subject: [PATCH 10/20] chore(bb): wsdb-ref's genesis seeds and roots, regenerated for next's protocol contracts --- .../world_state_reference/genesis_protocol_nullifiers.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp b/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp index 27a578b3f51..9fd0606961d 100644 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp +++ b/barretenberg/cpp/src/barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp @@ -25,7 +25,7 @@ inline std::vector genesis_protocol_nullifiers() bb::fr("0x0eb50b367fb754d3a7d1238bfc105cc9b391a02e187be69038876ae9a502e877"), bb::fr("0x16f917edd8e333013e5bb0633f7f484725bd19403854d6112992dccef4961e32"), bb::fr("0x1cea539e01abaa5db980e7ff52ef0d2a7772310306ac625783ae435756ee326d"), - bb::fr("0x2c3a57c8d7c387652babd36c4d79ab03c0fe593e160315a4dc098f92caf592c3"), + bb::fr("0x268b77cfa59397698ab19812d17573156d9a964fae96ab2d80929882f4a93837"), }; } @@ -40,17 +40,17 @@ inline std::vector genesis_protocol_nullifiers() */ inline bb::fr genesis_nullifier_tree_root() { - return bb::fr("0x15c67f4d7495a669ed57c5a9468533c63ace43f86b787c52edabefec85e4afc0"); + return bb::fr("0x238a0a0523fa750b77e243f8d6f485f03e90f1319fe76f5876f4af3d88b3fa89"); } inline bb::fr genesis_block_header_hash() { - return bb::fr("0x199b52350e8f18eeb9cd455fd074bc6fa62ea6a8e3253e2c663aa4909bbd5718"); + return bb::fr("0x0a72ed6f85f785d34e0a09a57c971b1623bb579531f3b85325e1d60f8209b3b8"); } inline bb::fr genesis_archive_root() { - return bb::fr("0x29eb2c527f8d45276430363214e6c8d709ef3f657a3670ebac3179373d41e5c4"); + return bb::fr("0x1fdfe7b76afd4dc9b9b6bd09f5fd6fb354f22cf7055ef1f11490b55a4e197291"); } } // namespace bb::world_state From a5c16e9bdc507c3c603f56277c09b9bda496aac8 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 12 Nov 2026 11:58:00 +0000 Subject: [PATCH 11/20] chore(bb): wsdb-ref's generated code lives in the build tree Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt index ddb37d67229..04f3e86824f 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt @@ -8,7 +8,10 @@ # - wsdb-ref.wasm, a WASI reactor (wasm builds), for the package's wasm transport; # - wsdb_ref_napi.node, a Node-API addon over the entry (native builds), for its napi transport. set(WSDB_REF_SCHEMA ${CMAKE_CURRENT_SOURCE_DIR}/wsdb_schema.jsonc) -set(WSDB_REF_GEN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/generated) +# Generated code is per build tree (each preset generates its own, so presets building at once never +# share a file). Remove the copy older checkouts generated into the source tree: it would shadow this one. +file(REMOVE_RECURSE ${CMAKE_CURRENT_SOURCE_DIR}/generated) +set(WSDB_REF_GEN_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated) set(WSDB_REF_GEN_OUTPUTS ${WSDB_REF_GEN_DIR}/wsdb_dispatch.hpp ${WSDB_REF_GEN_DIR}/wsdb_ffi.cpp @@ -41,7 +44,7 @@ add_custom_command( add_custom_target(wsdb_ref_generated DEPENDS ${WSDB_REF_GEN_OUTPUTS}) # Explicit sources: the generated Node-API addon must stay out of the module (it needs napi.h), and -# the generated FFI entry does not exist yet when a clean tree is configured. +# the generated FFI entry is in the build tree, which the module's source glob does not cover. file(GLOB WSDB_REF_TESTS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.test.cpp) barretenberg_module_with_sources( wsdb_ref From 3f7ce328e2b4b542e9176c6a4e742af6d2abd1f8 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Thu, 12 Nov 2026 18:21:53 +0000 Subject: [PATCH 12/20] feat(wsdb): Init, the first command, replaces InitGenesis; wsdb-ref has no built-in genesis wsdb-ref starts with no world state: its client sends Init with the genesis to start from, and a second Init restarts it at the new one. The canonical genesis is built by whoever calls Init (the C++ tests from genesis_protocol_nullifiers.hpp), not by the service. aztec-wsdb, whose genesis still comes from its command line, answers Init with an error. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- .../cpp/src/barretenberg/wsdb_ref/main.cpp | 2 +- .../barretenberg/wsdb_ref/wsdb_ref.test.cpp | 118 +++++++++++------ .../wsdb_ref/wsdb_ref_handlers.cpp | 122 +++++++++--------- .../wsdb_ref/wsdb_ref_handlers.hpp | 10 +- .../barretenberg/wsdb_ref/wsdb_schema.jsonc | 10 +- barretenberg/ts/scripts/wsdb_ref.test.mjs | 44 +++++-- .../wsdb/cpp/src/wsdb/wsdb_handlers.cpp | 6 +- .../wsdb/cpp/src/wsdb/wsdb_handlers.hpp | 4 +- .../wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp | 27 ++-- 9 files changed, 203 insertions(+), 140 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp index 9546e0c357a..6c4b52c43bc 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/main.cpp @@ -3,7 +3,7 @@ * @brief The wsdb-ref binary: wsdb-ref served over IPC, for a client that runs its world state as a process. * * `wsdb-ref msgpack run --input ` serves one reference world state on a .sock or .shm path until a - * shutdown signal. It starts at the canonical genesis; a client sends InitGenesis for another. + * shutdown signal. Its client sends Init first, with the genesis to start from. */ #include "barretenberg/bb/deps/cli11.hpp" #include "barretenberg/common/log.hpp" diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp index b8b67258b1e..ffdf5b44e7c 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp @@ -1,6 +1,7 @@ #include "barretenberg/wsdb_ref/wsdb_ref_handlers.hpp" #include "barretenberg/aztec/aztec_constants.hpp" +#include "barretenberg/world_state_reference/genesis_protocol_nullifiers.hpp" #include "barretenberg/wsdb_ref/generated/wsdb_ffi.hpp" #include @@ -48,6 +49,13 @@ template Result decode_response(const std::vector return resp; } +wire::Fr bytes_of_fr(const fr& value) +{ + wire::Fr w{}; + fr::serialize_to_buffer(value, w.data()); + return w; +} + /** Call a service through the generated in-process FFI entry: the path the napi and wasm transports take. */ template Result call_ffi(const char* name, const auto& cmd, const char* expected) { @@ -60,11 +68,43 @@ template Result call_ffi(const char* name, const auto& cmd return decode_response(frame, expected); } -/** Call a fresh service instance through the generated dispatch, independent of the FFI's shared state. */ +std::vector protocol_tree_heights() +{ + return { { .treeId = 0, .height = NULLIFIER_TREE_HEIGHT }, + { .treeId = 1, .height = NOTE_HASH_TREE_HEIGHT }, + { .treeId = 2, .height = PUBLIC_DATA_TREE_HEIGHT }, + { .treeId = 3, .height = L1_TO_L2_MSG_TREE_HEIGHT }, + { .treeId = 4, .height = ARCHIVE_HEIGHT } }; +} + +/** The canonical genesis, built as a C++ caller of Init builds it: the protocol's tree heights and initial sizes and + * the protocol contracts' registration nullifiers. */ +wire::WsdbInit canonical_init() +{ + wire::WsdbInit init{ .treeHeights = protocol_tree_heights(), + .treePrefill = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }, + .prefilledPublicData = {}, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }; + for (const auto& nullifier : world_state::genesis_protocol_nullifiers()) { + init.prefilledNullifiers.push_back(bytes_of_fr(nullifier)); + } + return init; +} + +/** A fresh service instance at the canonical genesis, called through the generated dispatch, independent of the + * FFI's shared state. */ struct Service { WsdbRefContext ctx; AsyncDispatchHandler handler = make_wsdb_handler(ctx); + Service() + { + auto init = call("WsdbInit", canonical_init(), "WsdbInitResponse"); + EXPECT_TRUE(std::holds_alternative(init)); + } + template Result call(const char* name, const auto& cmd, const char* expected) { auto request = encode_request(name, cmd); @@ -102,10 +142,17 @@ fr fr_of(const auto& w) return fr::serialize_from_buffer(w.data()); } +/** Init the FFI entry's shared world state at the canonical genesis (a later Init restarts it). */ +void init_ffi() +{ + ok(call_ffi("WsdbInit", canonical_init(), "WsdbInitResponse")); +} + } // namespace TEST(WsdbRef, FfiEntryServesTheCanonicalGenesis) { + init_ffi(); auto archive = ok(call_ffi("WsdbGetTreeInfo", wire::WsdbGetTreeInfo{ .treeId = 4, .revision = LATEST_COMMITTED }, @@ -137,6 +184,7 @@ TEST(WsdbRef, FfiEntryServesTheCanonicalGenesis) TEST(WsdbRef, FfiEntryAnswersBadRequestsWithErrors) { + init_ffi(); msgpack::sbuffer buf; msgpack::packer pk(buf); pk.pack_array(1); @@ -210,29 +258,20 @@ TEST(WsdbRef, ForksAndHistoryOverTheWire) std::string::npos); } -std::vector protocol_tree_heights() -{ - return { { .treeId = 0, .height = NULLIFIER_TREE_HEIGHT }, - { .treeId = 1, .height = NOTE_HASH_TREE_HEIGHT }, - { .treeId = 2, .height = PUBLIC_DATA_TREE_HEIGHT }, - { .treeId = 3, .height = L1_TO_L2_MSG_TREE_HEIGHT }, - { .treeId = 4, .height = ARCHIVE_HEIGHT } }; -} - -TEST(WsdbRef, InitGenesisSetsTreeHeightsAndInitialSizes) +TEST(WsdbRef, InitSetsTreeHeightsAndInitialSizes) { Service service; auto heights = protocol_tree_heights(); heights[1].height = 3; - ok(service.call( - "WsdbInitGenesis", - wire::WsdbInitGenesis{ .treeHeights = heights, - .treePrefill = { { .treeId = 0, .size = 4 }, { .treeId = 2, .size = 8 } }, - .prefilledPublicData = {}, - .prefilledNullifiers = {}, - .genesisTimestamp = 0, - .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }, - "WsdbInitGenesisResponse")); + ok(service.call( + "WsdbInit", + wire::WsdbInit{ .treeHeights = heights, + .treePrefill = { { .treeId = 0, .size = 4 }, { .treeId = 2, .size = 8 } }, + .prefilledPublicData = {}, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }, + "WsdbInitResponse")); auto tree_info = [&](uint32_t tree_id) { return ok(service.call( "WsdbGetTreeInfo", @@ -254,15 +293,14 @@ TEST(WsdbRef, InitGenesisSetsTreeHeightsAndInitialSizes) EXPECT_FALSE(std::holds_alternative(append(1))); auto rejected = [&](std::vector tree_heights, std::vector prefill) { - return error_of( - service.call("WsdbInitGenesis", - wire::WsdbInitGenesis{ .treeHeights = std::move(tree_heights), - .treePrefill = std::move(prefill), - .prefilledPublicData = {}, - .prefilledNullifiers = {}, - .genesisTimestamp = 0, - .initialHeaderGeneratorPoint = 0 }, - "WsdbInitGenesisResponse")); + return error_of(service.call("WsdbInit", + wire::WsdbInit{ .treeHeights = std::move(tree_heights), + .treePrefill = std::move(prefill), + .prefilledPublicData = {}, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = 0 }, + "WsdbInitResponse")); }; const std::vector both = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }; EXPECT_NE(rejected({ heights.begin(), heights.end() - 1 }, both).find("each of the five trees"), std::string::npos); @@ -274,7 +312,7 @@ TEST(WsdbRef, InitGenesisSetsTreeHeightsAndInitialSizes) std::string::npos); } -TEST(WsdbRef, InitGenesisRestartsAtTheGivenGenesis) +TEST(WsdbRef, ASecondInitRestartsAtTheGivenGenesis) { Service service; auto nullifier_root = [&]() { @@ -285,16 +323,16 @@ TEST(WsdbRef, InitGenesisRestartsAtTheGivenGenesis) return fr_of(info.root); }; EXPECT_EQ(nullifier_root(), fr(GENESIS_NULLIFIER_TREE_ROOT)); - ok(service.call( - "WsdbInitGenesis", - wire::WsdbInitGenesis{ .treeHeights = protocol_tree_heights(), - .treePrefill = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }, - .prefilledPublicData = { { .slot = bytes_of(fr(1000)), - .value = bytes_of(fr(5)) } }, - .prefilledNullifiers = {}, - .genesisTimestamp = 0, - .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }, - "WsdbInitGenesisResponse")); + ok(service.call( + "WsdbInit", + wire::WsdbInit{ .treeHeights = protocol_tree_heights(), + .treePrefill = { { .treeId = 0, .size = 128 }, { .treeId = 2, .size = 128 } }, + .prefilledPublicData = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(5)) } }, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }, + "WsdbInitResponse")); EXPECT_NE(nullifier_root(), fr(GENESIS_NULLIFIER_TREE_ROOT)); auto slot = ok(service.call( "WsdbFindPublicDataLeafIndices", diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp index 79b2e79a2ab..50cb374cdf1 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp @@ -325,11 +325,11 @@ void handle_get_tree_info(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbGetTreeInfoResponse { auto id = tree_id_of(cmd.treeId); - auto info = ctx.world_state.get_tree_info(revision_of(cmd.revision), id); + auto info = ctx.world_state->get_tree_info(revision_of(cmd.revision), id); return { .treeId = cmd.treeId, .root = from_fr(info.root), .size = info.size, - .depth = ctx.world_state.tree_height(id) }; + .depth = ctx.world_state->tree_height(id) }; }); } @@ -338,7 +338,7 @@ void handle_get_state_reference(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbGetStateReferenceResponse { - return { .state = state_to_wire(ctx.world_state.get_state_reference(revision_of(cmd.revision))) }; + return { .state = state_to_wire(ctx.world_state->get_state_reference(revision_of(cmd.revision))) }; }); } @@ -347,7 +347,7 @@ void handle_get_initial_state_reference(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbGetInitialStateReferenceResponse { - return { .state = state_to_wire(ctx.world_state.get_initial_state_reference()) }; + return { .state = state_to_wire(ctx.world_state->get_initial_state_reference()) }; }); } @@ -362,7 +362,7 @@ void handle_get_leaf_value(WsdbRefContext& ctx, serve(respond, [&]() -> wire::WsdbGetLeafValueResponse { auto id = tree_id_of(cmd.treeId); require_append_only(id, "get_leaf_value"); - auto value = ctx.world_state.get_leaf_value(revision_of(cmd.revision), id, cmd.leafIndex); + auto value = ctx.world_state->get_leaf_value(revision_of(cmd.revision), id, cmd.leafIndex); return { .value = value.has_value() ? std::optional(from_fr(*value)) : std::nullopt }; }); } @@ -372,8 +372,8 @@ void handle_get_public_data_leaf_value(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbGetPublicDataLeafValueResponse { - auto leaf = leaf_or_null(ctx.world_state, cmd.revision.forkId, [&] { - return ctx.world_state.get_public_data_preimage(revision_of(cmd.revision), cmd.leafIndex); + auto leaf = leaf_or_null(*ctx.world_state, cmd.revision.forkId, [&] { + return ctx.world_state->get_public_data_preimage(revision_of(cmd.revision), cmd.leafIndex); }); return { .value = leaf.has_value() ? std::optional(to_wire(leaf->leaf)) : std::nullopt }; }); @@ -384,8 +384,8 @@ void handle_get_nullifier_leaf_value(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbGetNullifierLeafValueResponse { - auto leaf = leaf_or_null(ctx.world_state, cmd.revision.forkId, [&] { - return ctx.world_state.get_nullifier_preimage(revision_of(cmd.revision), cmd.leafIndex); + auto leaf = leaf_or_null(*ctx.world_state, cmd.revision.forkId, [&] { + return ctx.world_state->get_nullifier_preimage(revision_of(cmd.revision), cmd.leafIndex); }); return { .value = leaf.has_value() ? std::optional(to_wire(leaf->leaf)) : std::nullopt }; }); @@ -397,7 +397,7 @@ void handle_get_public_data_leaf_preimage(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbGetPublicDataLeafPreimageResponse { // As on a node, a slot past the tree's size is an error here, while the leaf-value read of it answers null. - auto leaf = ctx.world_state.get_public_data_preimage(revision_of(cmd.revision), cmd.leafIndex); + auto leaf = ctx.world_state->get_public_data_preimage(revision_of(cmd.revision), cmd.leafIndex); if (!leaf.has_value()) { throw std::runtime_error("Failed to find indexed leaf: no leaf at index " + std::to_string(cmd.leafIndex)); } @@ -411,7 +411,7 @@ void handle_get_nullifier_leaf_preimage(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbGetNullifierLeafPreimageResponse { // As on a node, a slot past the tree's size is an error here, while the leaf-value read of it answers null. - auto leaf = ctx.world_state.get_nullifier_preimage(revision_of(cmd.revision), cmd.leafIndex); + auto leaf = ctx.world_state->get_nullifier_preimage(revision_of(cmd.revision), cmd.leafIndex); if (!leaf.has_value()) { throw std::runtime_error("Failed to find indexed leaf: no leaf at index " + std::to_string(cmd.leafIndex)); } @@ -424,7 +424,7 @@ void handle_get_sibling_path(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbGetSiblingPathResponse { - return { .path = path_to_wire(ctx.world_state.get_sibling_path( + return { .path = path_to_wire(ctx.world_state->get_sibling_path( revision_of(cmd.revision), tree_id_of(cmd.treeId), cmd.leafIndex)) }; }); } @@ -434,7 +434,7 @@ void handle_get_block_numbers_for_leaf_indices(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbGetBlockNumbersForLeafIndicesResponse { - return { .blockNumbers = ctx.world_state.get_block_numbers_for_leaf_indices( + return { .blockNumbers = ctx.world_state->get_block_numbers_for_leaf_indices( revision_of(cmd.revision), tree_id_of(cmd.treeId), cmd.leafIndices) }; }); } @@ -451,7 +451,7 @@ void handle_find_leaf_indices(WsdbRefContext& ctx, auto id = tree_id_of(cmd.treeId); require_append_only(id, "find_leaf_indices"); return { .indices = find_indices( - ctx.world_state, revision_of(cmd.revision), id, frs_from_wire(cmd.leaves), cmd.startIndex) }; + *ctx.world_state, revision_of(cmd.revision), id, frs_from_wire(cmd.leaves), cmd.startIndex) }; }); } @@ -463,7 +463,7 @@ void handle_find_public_data_leaf_indices(WsdbRefContext& ctx, auto keys = keys_of(all_from_wire(cmd.leaves)); return { .indices = find_indices( - ctx.world_state, revision_of(cmd.revision), MerkleTreeId::PUBLIC_DATA_TREE, keys, cmd.startIndex) + *ctx.world_state, revision_of(cmd.revision), MerkleTreeId::PUBLIC_DATA_TREE, keys, cmd.startIndex) }; }); } @@ -475,7 +475,7 @@ void handle_find_nullifier_leaf_indices(WsdbRefContext& ctx, serve(respond, [&]() -> wire::WsdbFindNullifierLeafIndicesResponse { auto keys = keys_of(all_from_wire(cmd.leaves)); return { .indices = find_indices( - ctx.world_state, revision_of(cmd.revision), MerkleTreeId::NULLIFIER_TREE, keys, cmd.startIndex) }; + *ctx.world_state, revision_of(cmd.revision), MerkleTreeId::NULLIFIER_TREE, keys, cmd.startIndex) }; }); } @@ -484,7 +484,7 @@ void handle_find_low_leaf(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbFindLowLeafResponse { - auto low = ctx.world_state.find_low_leaf(revision_of(cmd.revision), tree_id_of(cmd.treeId), to_fr(cmd.key)); + auto low = ctx.world_state->find_low_leaf(revision_of(cmd.revision), tree_id_of(cmd.treeId), to_fr(cmd.key)); return { .alreadyPresent = low.is_already_present, .index = low.index }; }); } @@ -496,7 +496,7 @@ void handle_find_sibling_paths(WsdbRefContext& ctx, serve(respond, [&]() -> wire::WsdbFindSiblingPathsResponse { auto id = tree_id_of(cmd.treeId); require_append_only(id, "find_sibling_paths"); - return { .paths = find_paths(ctx.world_state, revision_of(cmd.revision), id, frs_from_wire(cmd.leaves)) }; + return { .paths = find_paths(*ctx.world_state, revision_of(cmd.revision), id, frs_from_wire(cmd.leaves)) }; }); } @@ -507,7 +507,7 @@ void handle_find_public_data_sibling_paths(WsdbRefContext& ctx, serve(respond, [&]() -> wire::WsdbFindPublicDataSiblingPathsResponse { auto keys = keys_of(all_from_wire(cmd.leaves)); return { .paths = - find_paths(ctx.world_state, revision_of(cmd.revision), MerkleTreeId::PUBLIC_DATA_TREE, keys) }; + find_paths(*ctx.world_state, revision_of(cmd.revision), MerkleTreeId::PUBLIC_DATA_TREE, keys) }; }); } @@ -517,7 +517,7 @@ void handle_find_nullifier_sibling_paths(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbFindNullifierSiblingPathsResponse { auto keys = keys_of(all_from_wire(cmd.leaves)); - return { .paths = find_paths(ctx.world_state, revision_of(cmd.revision), MerkleTreeId::NULLIFIER_TREE, keys) }; + return { .paths = find_paths(*ctx.world_state, revision_of(cmd.revision), MerkleTreeId::NULLIFIER_TREE, keys) }; }); } @@ -531,7 +531,7 @@ void handle_append_leaves(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbAppendLeavesResponse { auto leaves = frs_from_wire(cmd.leaves); - ctx.world_state.append_leaves(cmd.forkId, tree_id_of(cmd.treeId), leaves); + ctx.world_state->append_leaves(cmd.forkId, tree_id_of(cmd.treeId), leaves); return {}; }); } @@ -543,7 +543,7 @@ void handle_append_public_data_leaves(WsdbRefContext& ctx, // As on a node: a batch insertion without its witnesses. serve(respond, [&]() -> wire::WsdbAppendPublicDataLeavesResponse { auto leaves = all_from_wire(cmd.leaves); - ctx.world_state.batch_insert_public_data(cmd.forkId, leaves, 0); + ctx.world_state->batch_insert_public_data(cmd.forkId, leaves, 0); return {}; }); } @@ -554,7 +554,7 @@ void handle_append_nullifier_leaves(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbAppendNullifierLeavesResponse { auto leaves = all_from_wire(cmd.leaves); - ctx.world_state.batch_insert_nullifiers(cmd.forkId, leaves, 0); + ctx.world_state->batch_insert_nullifiers(cmd.forkId, leaves, 0); return {}; }); } @@ -565,7 +565,7 @@ void handle_batch_insert_public_data(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbBatchInsertPublicDataResponse { auto leaves = all_from_wire(cmd.leaves); - auto result = ctx.world_state.batch_insert_public_data(cmd.forkId, leaves, cmd.subtreeDepth); + auto result = ctx.world_state->batch_insert_public_data(cmd.forkId, leaves, cmd.subtreeDepth); return { .result = batch_to_wire(result) }; @@ -578,7 +578,7 @@ void handle_batch_insert_nullifier(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbBatchInsertNullifierResponse { auto leaves = all_from_wire(cmd.leaves); - auto result = ctx.world_state.batch_insert_nullifiers(cmd.forkId, leaves, cmd.subtreeDepth); + auto result = ctx.world_state->batch_insert_nullifiers(cmd.forkId, leaves, cmd.subtreeDepth); return { .result = batch_to_wire(result) }; @@ -591,7 +591,7 @@ void handle_sequential_insert_public_data(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbSequentialInsertPublicDataResponse { auto leaves = all_from_wire(cmd.leaves); - auto result = ctx.world_state.sequential_insert_public_data(cmd.forkId, leaves); + auto result = ctx.world_state->sequential_insert_public_data(cmd.forkId, leaves); return { .result = sequential_to_wire( @@ -606,7 +606,7 @@ void handle_sequential_insert_nullifier(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbSequentialInsertNullifierResponse { auto leaves = all_from_wire(cmd.leaves); - auto result = ctx.world_state.sequential_insert_nullifiers(cmd.forkId, leaves); + auto result = ctx.world_state->sequential_insert_nullifiers(cmd.forkId, leaves); return { .result = sequential_to_wire( result) }; @@ -618,7 +618,7 @@ void handle_update_archive(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbUpdateArchiveResponse { - ctx.world_state.update_archive(cmd.forkId, state_from_wire(cmd.blockStateRef), to_fr(cmd.blockHeaderHash)); + ctx.world_state->update_archive(cmd.forkId, state_from_wire(cmd.blockStateRef), to_fr(cmd.blockHeaderHash)); return {}; }); } @@ -630,15 +630,15 @@ void handle_update_archive(WsdbRefContext& ctx, void handle_commit(WsdbRefContext& ctx, wire::WsdbCommit&& /*cmd*/, Responder respond) { serve(respond, [&]() -> wire::WsdbCommitResponse { - ctx.world_state.commit(); - return { .status = full_status(ctx.world_state) }; + ctx.world_state->commit(); + return { .status = full_status(*ctx.world_state) }; }); } void handle_rollback(WsdbRefContext& ctx, wire::WsdbRollback&& /*cmd*/, Responder respond) { serve(respond, [&]() -> wire::WsdbRollbackResponse { - ctx.world_state.rollback(); + ctx.world_state->rollback(); return {}; }); } @@ -653,15 +653,15 @@ void handle_sync_block(WsdbRefContext& ctx, wire::WsdbSyncBlock&& cmd, Responder auto optional_fr = [](const std::optional& w) { return w.has_value() ? std::optional(to_fr(*w)) : std::nullopt; }; - ctx.world_state.sync_block(state_from_wire(cmd.blockStateRef), - to_fr(cmd.blockHeaderHash), - note_hashes, - messages, - nullifiers, - public_data, - optional_fr(cmd.expectedArchiveRoot), - optional_fr(cmd.expectedPreviousArchiveRoot)); - return { .status = full_status(ctx.world_state) }; + ctx.world_state->sync_block(state_from_wire(cmd.blockStateRef), + to_fr(cmd.blockHeaderHash), + note_hashes, + messages, + nullifiers, + public_data, + optional_fr(cmd.expectedArchiveRoot), + optional_fr(cmd.expectedPreviousArchiveRoot)); + return { .status = full_status(*ctx.world_state) }; }); } @@ -671,7 +671,7 @@ void handle_create_fork(WsdbRefContext& ctx, { serve(respond, [&]() -> wire::WsdbCreateForkResponse { auto block = cmd.latest ? std::nullopt : std::optional(cmd.blockNumber); - return { .forkId = ctx.world_state.create_fork(block) }; + return { .forkId = ctx.world_state->create_fork(block) }; }); } @@ -680,7 +680,7 @@ void handle_delete_fork(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbDeleteForkResponse { - ctx.world_state.delete_fork(cmd.forkId); + ctx.world_state->delete_fork(cmd.forkId); return {}; }); } @@ -690,8 +690,8 @@ void handle_finalize_blocks(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbFinalizeBlocksResponse { - ctx.world_state.finalize_blocks(cmd.toBlockNumber); - return { .status = summary_to_wire(ctx.world_state.status()) }; + ctx.world_state->finalize_blocks(cmd.toBlockNumber); + return { .status = summary_to_wire(ctx.world_state->status()) }; }); } @@ -700,8 +700,8 @@ void handle_unwind_blocks(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbUnwindBlocksResponse { - ctx.world_state.unwind_blocks(cmd.toBlockNumber); - return { .status = full_status(ctx.world_state) }; + ctx.world_state->unwind_blocks(cmd.toBlockNumber); + return { .status = full_status(*ctx.world_state) }; }); } @@ -710,8 +710,8 @@ void handle_remove_historical_blocks(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbRemoveHistoricalBlocksResponse { - ctx.world_state.remove_historical_blocks(cmd.toBlockNumber); - return { .status = full_status(ctx.world_state) }; + ctx.world_state->remove_historical_blocks(cmd.toBlockNumber); + return { .status = full_status(*ctx.world_state) }; }); } @@ -720,7 +720,7 @@ void handle_get_status(WsdbRefContext& ctx, Responder respond) { serve(respond, - [&]() -> wire::WsdbGetStatusResponse { return { .status = summary_to_wire(ctx.world_state.status()) }; }); + [&]() -> wire::WsdbGetStatusResponse { return { .status = summary_to_wire(ctx.world_state->status()) }; }); } // --------------------------------------------------------------------------- @@ -732,7 +732,7 @@ void handle_create_checkpoint(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbCreateCheckpointResponse { - ctx.world_state.create_checkpoint(cmd.forkId); + ctx.world_state->create_checkpoint(cmd.forkId); return {}; }); } @@ -742,7 +742,7 @@ void handle_commit_checkpoint(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbCommitCheckpointResponse { - ctx.world_state.commit_checkpoint(cmd.forkId); + ctx.world_state->commit_checkpoint(cmd.forkId); return {}; }); } @@ -752,7 +752,7 @@ void handle_revert_checkpoint(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbRevertCheckpointResponse { - ctx.world_state.revert_checkpoint(cmd.forkId); + ctx.world_state->revert_checkpoint(cmd.forkId); return {}; }); } @@ -762,7 +762,7 @@ void handle_commit_all_checkpoints(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbCommitAllCheckpointsResponse { - ctx.world_state.commit_all_checkpoints(cmd.forkId); + ctx.world_state->commit_all_checkpoints(cmd.forkId); return {}; }); } @@ -772,7 +772,7 @@ void handle_revert_all_checkpoints(WsdbRefContext& ctx, Responder respond) { serve(respond, [&]() -> wire::WsdbRevertAllCheckpointsResponse { - ctx.world_state.revert_all_checkpoints(cmd.forkId); + ctx.world_state->revert_all_checkpoints(cmd.forkId); return {}; }); } @@ -788,11 +788,9 @@ void handle_copy_stores(WsdbRefContext& /*ctx*/, respond.error("CopyStores is not supported: wsdb-ref keeps its state in memory, with no stores to copy"); } -void handle_init_genesis(WsdbRefContext& ctx, - wire::WsdbInitGenesis&& cmd, - Responder respond) +void handle_init(WsdbRefContext& ctx, wire::WsdbInit&& cmd, Responder respond) { - serve(respond, [&]() -> wire::WsdbInitGenesisResponse { + serve(respond, [&]() -> wire::WsdbInitResponse { GenesisConfig genesis; std::array has_height{}; for (const auto& [tree_id, height] : cmd.treeHeights) { @@ -831,13 +829,17 @@ void handle_init_genesis(WsdbRefContext& ctx, } genesis.genesis_timestamp = cmd.genesisTimestamp; genesis.header_generator_point = FF(cmd.initialHeaderGeneratorPoint); - ctx.world_state.reset(genesis); + if (ctx.world_state.has_value()) { + ctx.world_state->reset(genesis); + } else { + ctx.world_state.emplace(genesis); + } return {}; }); } // The in-process FFI entry's context (generated/wsdb_ffi.hpp): one reference world state per loaded -// module, at the canonical genesis until InitGenesis, shared by every call as over one transport connection. +// module, created by Init, shared by every call as over one transport connection. AsyncDispatchHandler& ipc_ffi_dispatcher() { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp index cbfd41a2ee9..c25a3d33bd6 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref_handlers.hpp @@ -14,12 +14,14 @@ #include "barretenberg/wsdb_ref/generated/wsdb_dispatch.hpp" #include "barretenberg/wsdb_ref/generated/wsdb_types.hpp" +#include + namespace bb::wsdb_ref { -/** @brief The state a wsdb-ref service runs over: one reference world state, at the canonical genesis until - * InitGenesis. */ +/** @brief The state a wsdb-ref service runs over: one reference world state, which Init creates (and a later + * Init replaces). Every other command assumes it exists. */ struct WsdbRefContext { - world_state::ReferenceWorldState world_state; + std::optional world_state; }; // clang-format off @@ -63,7 +65,7 @@ void handle_revert_checkpoint(WsdbRefContext& ctx, wire::WsdbRevertCheckpoint&& void handle_commit_all_checkpoints(WsdbRefContext& ctx, wire::WsdbCommitAllCheckpoints&& cmd, Responder respond); void handle_revert_all_checkpoints(WsdbRefContext& ctx, wire::WsdbRevertAllCheckpoints&& cmd, Responder respond); void handle_copy_stores(WsdbRefContext& ctx, wire::WsdbCopyStores&& cmd, Responder respond); -void handle_init_genesis(WsdbRefContext& ctx, wire::WsdbInitGenesis&& cmd, Responder respond); +void handle_init(WsdbRefContext& ctx, wire::WsdbInit&& cmd, Responder respond); // clang-format on } // namespace bb::wsdb_ref diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc index 8e63ed03635..e3fdf4f02d2 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc @@ -481,10 +481,12 @@ }, "response": {} }, - // Restarts the world state at this genesis, before any block. treeHeights names each of the five trees - // once; treePrefill gives each indexed tree (nullifier, public data) its initial size, prefill included. - // A service whose genesis is fixed when it is created (aztec-wsdb) answers this with an error. - "InitGenesis": { + // The first command: the genesis the world state starts from, before any block. Anything sent before it is + // undefined. treeHeights names each of the five trees once; treePrefill gives each indexed tree (nullifier, + // public data) its initial size, prefill included. What a second Init does is the service's own: wsdb-ref + // restarts at the new genesis. A service whose genesis is fixed when it is created (aztec-wsdb) answers Init + // with an error. + "Init": { "request": { "treeHeights": "TreeHeight[]", "treePrefill": "TreePrefill[]", diff --git a/barretenberg/ts/scripts/wsdb_ref.test.mjs b/barretenberg/ts/scripts/wsdb_ref.test.mjs index 84c8d871da9..32c9c206900 100644 --- a/barretenberg/ts/scripts/wsdb_ref.test.mjs +++ b/barretenberg/ts/scripts/wsdb_ref.test.mjs @@ -4,7 +4,7 @@ // // Every napi backend in a process shares one world state (node loads the addon once), so the // tests read the genesis through committed reads, which writes never change, and write only fresh -// random keys. +// random keys. Each service is sent Init first, with the test's own genesis. import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { randomBytes } from 'node:crypto'; @@ -28,12 +28,32 @@ const constantsNr = readFileSync( join(root, 'noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr'), 'utf8', ); -function protocolConstant(name) { - const match = constantsNr.match(new RegExp(`pub global ${name}: Field =\\s*(0x[0-9a-f]+);`)); +function protocolU32(name) { + const match = constantsNr.match(new RegExp(`pub global ${name}: u32 = (\\d+);`)); assert.ok(match, `${name} not found in constants.nr`); - return BigInt(match[1]); + return Number(match[1]); } +// The test's own genesis: protocol-sized trees with no prefilled leaves. The canonical genesis, with the protocol +// contracts' nullifiers and the roots constants.nr records, is checked by wsdb-ref's C++ tests. +const GENESIS = { + treeHeights: [ + { treeId: 0, height: 42 }, + { treeId: 1, height: 42 }, + { treeId: 2, height: 40 }, + { treeId: 3, height: 36 }, + { treeId: 4, height: 30 }, + ], + treePrefill: [ + { treeId: 0, size: 128 }, + { treeId: 2, size: 128 }, + ], + prefilledPublicData: [], + prefilledNullifiers: [], + genesisTimestamp: 0, + initialHeaderGeneratorPoint: protocolU32('DOM_SEP__BLOCK_HEADER_HASH'), +}; + const NULLIFIER_TREE = 0; const NOTE_HASH_TREE = 1; const ARCHIVE = 4; @@ -46,24 +66,27 @@ const toBigInt = bytes => BigInt('0x' + Buffer.from(bytes).toString('hex')); const randomFr = () => new Uint8Array([0, ...randomBytes(31)]); let service; +let genesisArchiveRoot; before(async () => { service = await WsdbService.create(options); + await service.init(GENESIS); }); after(async () => { await service.destroy(); }); -test('serves the canonical genesis', async () => { +test('serves the genesis Init gave it', async () => { const nullifiers = await service.getTreeInfo({ treeId: NULLIFIER_TREE, revision: committed }); - assert.equal(toBigInt(nullifiers.root), protocolConstant('GENESIS_NULLIFIER_TREE_ROOT')); assert.equal(nullifiers.size, 128); + assert.equal(nullifiers.depth, 42); const archive = await service.getTreeInfo({ treeId: ARCHIVE, revision: committed }); - assert.equal(toBigInt(archive.root), protocolConstant('GENESIS_ARCHIVE_ROOT')); assert.equal(archive.size, 1); + assert.equal(archive.depth, 30); + genesisArchiveRoot = toBigInt(archive.root); const header = await service.getLeafValue({ treeId: ARCHIVE, revision: committed, leafIndex: 0 }); - assert.equal(toBigInt(header.value), protocolConstant('GENESIS_BLOCK_HEADER_HASH')); + assert.ok(header.value, 'the genesis block header is in the archive'); const { state } = await service.getInitialStateReference({}); assert.deepEqual(state.map(t => t.treeId).sort(), [0, 1, 2, 3]); @@ -126,12 +149,13 @@ test('forks are separate world states, and bad requests are errors, not crashes' assert.equal(info.size, 1); }); -test('the synchronous service reaches the same world state', async () => { +test('the synchronous service, given the same Init, reaches the same genesis', async () => { // A synchronous process client is always over shared memory. const sync = await WsdbServiceSync.create({ backend: options.backend }); try { + sync.init(GENESIS); const archive = sync.getTreeInfo({ treeId: ARCHIVE, revision: committed }); - assert.equal(toBigInt(archive.root), protocolConstant('GENESIS_ARCHIVE_ROOT')); + assert.equal(toBigInt(archive.root), genesisArchiveRoot); } finally { sync.destroy(); } diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp index 19103129d68..70c435c9eba 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.cpp @@ -763,11 +763,9 @@ void handle_copy_stores(WsdbRequest& ctx, wire::WsdbCopyStores&& cmd, Responder< } // aztec-wsdb opens its stores at its genesis when it is constructed, so it cannot take a genesis afterwards. -void handle_init_genesis(WsdbRequest& /*ctx*/, - wire::WsdbInitGenesis&& /*cmd*/, - Responder respond) +void handle_init(WsdbRequest& /*ctx*/, wire::WsdbInit&& /*cmd*/, Responder respond) { - respond.error("aztec-wsdb does not support InitGenesis: its genesis is fixed when the service is created"); + respond.error("aztec-wsdb does not support Init: its genesis is fixed when the service is created"); } } // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp index 7845a3d5ab5..6c0d09afa6e 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_handlers.hpp @@ -127,8 +127,6 @@ void handle_revert_all_checkpoints(WsdbRequest& ctx, wire::WsdbRevertAllCheckpoints&& cmd, Responder respond); void handle_copy_stores(WsdbRequest& ctx, wire::WsdbCopyStores&& cmd, Responder respond); -void handle_init_genesis(WsdbRequest& ctx, - wire::WsdbInitGenesis&& cmd, - Responder respond); +void handle_init(WsdbRequest& ctx, wire::WsdbInit&& cmd, Responder respond); } // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp index 66309c6dd86..0a0508729a8 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp @@ -417,23 +417,22 @@ class DiffRun { { // The genesis aztec-wsdb was started with: the protocol's tree heights, this run's initial sizes, the // protocol nullifiers and one public-data slot. - wire::WsdbInitGenesis genesis{ - .treeHeights = { { .treeId = 0, .height = NULLIFIER_TREE_HEIGHT }, - { .treeId = 1, .height = NOTE_HASH_TREE_HEIGHT }, - { .treeId = 2, .height = PUBLIC_DATA_TREE_HEIGHT }, - { .treeId = 3, .height = L1_TO_L2_MSG_TREE_HEIGHT }, - { .treeId = 4, .height = ARCHIVE_HEIGHT } }, - .treePrefill = { { .treeId = 0, .size = sizes_.nullifier }, { .treeId = 2, .size = sizes_.public_data } }, - .prefilledPublicData = { { .slot = bytes_of(fr(1000)), - .value = bytes_of(fr(7)) } }, - .prefilledNullifiers = {}, - .genesisTimestamp = 0, - .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) - }; + wire::WsdbInit genesis{ .treeHeights = { { .treeId = 0, .height = NULLIFIER_TREE_HEIGHT }, + { .treeId = 1, .height = NOTE_HASH_TREE_HEIGHT }, + { .treeId = 2, .height = PUBLIC_DATA_TREE_HEIGHT }, + { .treeId = 3, .height = L1_TO_L2_MSG_TREE_HEIGHT }, + { .treeId = 4, .height = ARCHIVE_HEIGHT } }, + .treePrefill = { { .treeId = 0, .size = sizes_.nullifier }, + { .treeId = 2, .size = sizes_.public_data } }, + .prefilledPublicData = { { .slot = bytes_of(fr(1000)), + .value = bytes_of(fr(7)) } }, + .prefilledNullifiers = {}, + .genesisTimestamp = 0, + .initialHeaderGeneratorPoint = static_cast(DOM_SEP__BLOCK_HEADER_HASH) }; for (const auto& seed : world_state::genesis_protocol_nullifiers()) { genesis.prefilledNullifiers.push_back(bytes_of(seed)); } - auto request = encode_request("WsdbInitGenesis", genesis); + auto request = encode_request("WsdbInit", genesis); ASSERT_FALSE(parse(ref_.call(request)).is_error()); slots_.push_back(fr(1000)); both("WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}); From 8064133c4d4c1b1156c8ba279bb468a438c78218 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 13 Nov 2026 16:32:55 +0000 Subject: [PATCH 13/20] docs(bb): an interactive explainer of the reference world state's persistent treap and forks treap_forks.html walks through PersistentKeyIndex's insert (the same split-based insert and splitmix64 priorities) on versions you can fork, showing which nodes each version allocates and which it shares, then how Trees being ten root pointers makes blocks, forks and checkpoints free. Linked from the root README under a new Learn section. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- README.md | 4 + .../barretenberg/wsdb_ref/treap_forks.html | 327 ++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html diff --git a/README.md b/README.md index 0d8defc3238..7cb61aec5d5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ All the packages that make up the [Aztec](https://docs.aztec.network) protocol. Additionally, the [**`labs`**](https://github.com/aztec-labs-eng/aztec-node) repository is linked as a submodule. Go there for the aztec node, client, Aztec.nr, docs, and deployment infrastructure. +## Learn + +- [Persistent treap forks](barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html): an interactive explainer of how wsdb-ref's reference world state forks and checkpoints for the cost of a pointer copy. Download it and open it in a browser. + ## Releases The projects in this repo are released via npm ([@aztec-foundation](https://www.npmjs.com/org/aztec-foundation)) and Github releases. diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html b/barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html new file mode 100644 index 00000000000..305ea11766a --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html @@ -0,0 +1,327 @@ + + + + + +Persistent Treap Forks + + + + + +
+
+

Persistent Treap Forks

+

How wsdb-ref's PersistentKeyIndex keeps an ordered key → leaf-index map that can be forked, checkpointed and kept per block for the cost of copying one pointer.

+
+ +
+

A treap is a search tree and a heap at once

+

Each node carries a key and a priority. The tree obeys two orderings at the same time, and for a given set of keys and priorities only one tree satisfies both.

+
+
+

By key: a search tree

Smaller keys go left, larger keys go right, so find and predecessor (the indexed trees' low-leaf lookup) walk one root-to-leaf path.

+

By priority: a heap

A parent's priority is higher than its children's. Random priorities make the shape that of a random insertion order, so the depth stays O(log n) on average.

+

Priority = hash(key)

wsdb-ref uses splitmix64 of the key instead of a random number, so the shape depends only on the set of keys and every run builds the same tree.

+
+ +
+
+
+ + +
+
+
+
+

Versions

+
+
+
+ allocated by this version + the same node object as in the compared version + - - - new node pointing into a shared subtree +
+
+
+
+
+ +
+

Why an insert copies only one path

+

Nodes are immutable (shared_ptr<const Node>). An insert walks down from the root while the existing node's priority is higher, rebuilding each node it passes with one new child pointer. Where the new key's priority wins, it splits that subtree by key and takes the two halves as its children; the split also rebuilds only the nodes on its path. Every subtree off that path is reused as is.

+

So a version is just a root pointer. The old root still describes the old tree, untouched, and the new root shares everything except about log n nodes with it. Select an older version above and insert into it: that is a fork, and both branches keep sharing the nodes they had in common.

+
+ +
+

How the reference world state forks

+

Each of the five trees pairs a PersistentMerkleTree (built on the same path-copying idea) with a PersistentKeyIndex. Both are a root pointer, so the whole state is ten pointers:

+
struct Trees {
+    NullifierTree   nullifier;    // tree root + key index root
+    AppendOnlyTree  note_hash;    // tree root + key index root
+    PublicDataTree  public_data;  // tree root + key index root
+    AppendOnlyTree  l1_to_l2;     // tree root + key index root
+    AppendOnlyTree  archive;      // tree root + key index root
+};  // "Copying one is copying ten pointers."
+

From there each operation is a copy of Trees:

+
    +
  • Committed blocks: keep a Trees per block, so reads at any block are direct.
  • +
  • Fork: copy the Trees of the block it starts from, then write to the copy.
  • +
  • Checkpoint: push a copy of the fork's current Trees; revert pops it back, commit drops it.
  • +
+

No operation copies tree contents. Writes pay O(log n) new nodes each, whichever version they write to, and memory is shared by every version that still points at it.

+
+
+ + + + From 074149406f1ef6cf53be5b3582b5fb6db29526a3 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 13 Nov 2026 16:34:57 +0000 Subject: [PATCH 14/20] docs(bb): link the treap explainer from a wsdb_ref README, not the root README wsdb_ref/README.md says what wsdb-ref is (the reference world state over the wsdb schema it owns), the three outputs that carry it to @aztec-foundation/wsdb-ref's transports, and links treap_forks.html. The root README is back as it was. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- README.md | 4 --- .../cpp/src/barretenberg/wsdb_ref/README.md | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/README.md diff --git a/README.md b/README.md index 7cb61aec5d5..0d8defc3238 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,6 @@ All the packages that make up the [Aztec](https://docs.aztec.network) protocol. Additionally, the [**`labs`**](https://github.com/aztec-labs-eng/aztec-node) repository is linked as a submodule. Go there for the aztec node, client, Aztec.nr, docs, and deployment infrastructure. -## Learn - -- [Persistent treap forks](barretenberg/cpp/src/barretenberg/wsdb_ref/treap_forks.html): an interactive explainer of how wsdb-ref's reference world state forks and checkpoints for the cost of a pointer copy. Download it and open it in a browser. - ## Releases The projects in this repo are released via npm ([@aztec-foundation](https://www.npmjs.com/org/aztec-foundation)) and Github releases. diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/README.md b/barretenberg/cpp/src/barretenberg/wsdb_ref/README.md new file mode 100644 index 00000000000..2b46d37b7dc --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/README.md @@ -0,0 +1,25 @@ +# wsdb-ref + +The in-memory reference world state (`world_state_reference/`) served as a service over the node world state's +contract, `wsdb_schema.jsonc`. wsdb-ref owns that schema: the AVM's wsdb client (`vm2_wsdb/`) and +`native-packages/wsdb`'s `aztec-wsdb` are generated from it too, so wsdb-ref answers the same requests as the +production world state and serves as its reference. + +The ipc-codegen dispatch runs the handlers in `wsdb_ref_handlers.cpp`. Each of the three build outputs carries +it to one transport of `@aztec-foundation/wsdb-ref` (`barretenberg/ts/wsdb-ref`): + +| Output | Built in | Transport | +|---|---|---| +| `wsdb-ref` binary (`main.cpp`) | native builds | a spawned process over a socket or shared memory | +| `wsdb_ref_napi.node` | native builds | in-process in node, through Node-API | +| `wsdb-ref.wasm` (WASI reactor) | wasm builds | in-process in node or a browser | + +A client sends `Init` first, with the genesis to start from; wsdb-ref has no built-in genesis. + +## How forks stay cheap + +Every tree in the reference world state is persistent: a write copies only the path it changes and shares the +rest with the version it came from. Blocks, forks and checkpoints are therefore copies of ten root pointers. +[`treap_forks.html`](treap_forks.html) is an interactive explainer of the treap behind the key index +(`world_state_reference/persistent_key_index.hpp`) and of how the forks share structure. Download it and open +it in a browser. From b1eccac576ed34c74bd87b52fc6949b691e9f562 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:57:42 +0000 Subject: [PATCH 15/20] feat(wsdb): in-process differential test over both FFI entries; libwsdb-ref.a for all four platforms - ipc-codegen: --ffi-name names an implementation's FFI symbols (_ipc_ffi_entry/alloc/free and the TS wasm export), separate from the schema's service, which names the wire protocol both implementations serve. wsdb-ref's entry is wsdb_ref_ipc_ffi_entry; aztec-wsdb gains one, wsdb_labs_ipc_ffi_entry, opened with wsdb_labs_ffi_open. - The differential test drives aztec-wsdb and wsdb-ref in-process through their FFI entries with the generated typed client. The out-of-process comparison and the memory_merkle_db C API it replaces are removed. - barretenberg builds libwsdb-ref.a: wsdb-ref behind its FFI entry plus the poseidon2 C bindings, with every other symbol private so barretenberg's C++ (its msgpack-c among it) cannot merge with a consumer's. On ELF it is one relocatable object with only the exports global; on Mach-O, which has no usable -r, every other defined symbol is renamed under a private prefix. aztec-wsdb links it instead of bb-external, against this checkout's build. - Fix the aztec-wsdb release, which since wsdb left barretenberg published only linux-x64: release cross-builds it for linux-arm64, darwin-x64 and darwin-arm64 (cpp/CMakePresets.json, zig), linking barretenberg's per-platform libwsdb-ref.a, which the cross presets and static tarballs now carry. wsdb builds lmdblib as a subproject, so each target gets its own. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- Makefile | 9 +- barretenberg/cpp/CMakePresets.json | 8 +- barretenberg/cpp/bootstrap.sh | 8 +- .../cpp/scripts/rename_private_symbols.sh | 27 + barretenberg/cpp/src/CMakeLists.txt | 68 ++- .../crypto/poseidon2/poseidon2_capi.cpp | 7 +- .../memory_merkle_db_capi.cpp | 163 ------ .../src/barretenberg/wsdb_ref/CMakeLists.txt | 5 +- .../barretenberg/wsdb_ref/wsdb_ref.test.cpp | 8 +- barretenberg/ts/bootstrap.sh | 1 + ipc-codegen/SCHEMA_SPEC.md | 11 +- ipc-codegen/src/cpp_codegen.ts | 11 +- ipc-codegen/src/generate.ts | 16 + ipc-codegen/src/rust_codegen.ts | 6 +- ipc-codegen/src/typescript_package_codegen.ts | 6 +- ipc-codegen/src/zig_codegen.ts | 7 +- native-packages/wsdb/.gitignore | 1 + native-packages/wsdb/.rebuild_patterns | 2 - native-packages/wsdb/README.md | 29 +- native-packages/wsdb/bootstrap.sh | 33 +- native-packages/wsdb/cpp/CMakeLists.txt | 103 ++-- native-packages/wsdb/cpp/CMakePresets.json | 93 +++ native-packages/wsdb/cpp/src/wsdb/cli.cpp | 52 +- .../cpp/src/wsdb/memory_merkle_db.test.cpp | 313 ---------- .../wsdb/cpp/src/wsdb/reference_merkle_db.hpp | 169 ------ .../wsdb/cpp/src/wsdb/wsdb_ffi_service.cpp | 79 +++ .../wsdb/cpp/src/wsdb/wsdb_ffi_service.hpp | 16 + .../wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp | 239 +------- .../wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp | 16 +- .../wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp | 548 ++++++++---------- .../wsdb/cpp/src/wsdb/wsdb_scheduler.hpp | 13 +- .../wsdb/cpp/src/wsdb/wsdb_service.cpp | 235 ++++++++ .../wsdb/cpp/src/wsdb/wsdb_service.hpp | 76 +++ 33 files changed, 1019 insertions(+), 1359 deletions(-) create mode 100755 barretenberg/cpp/scripts/rename_private_symbols.sh delete mode 100644 barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db_capi.cpp create mode 100644 native-packages/wsdb/cpp/CMakePresets.json delete mode 100644 native-packages/wsdb/cpp/src/wsdb/memory_merkle_db.test.cpp delete mode 100644 native-packages/wsdb/cpp/src/wsdb/reference_merkle_db.hpp create mode 100644 native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.cpp create mode 100644 native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.hpp create mode 100644 native-packages/wsdb/cpp/src/wsdb/wsdb_service.cpp create mode 100644 native-packages/wsdb/cpp/src/wsdb/wsdb_service.hpp diff --git a/Makefile b/Makefile index 1c56f3019aa..70b69775d1e 100644 --- a/Makefile +++ b/Makefile @@ -430,22 +430,21 @@ lmdblib: kvdb: lmdblib $(call build,$@,native-packages/kvdb) -wsdb: lmdblib +# wsdb links the working tree's barretenberg static library (see native-packages/wsdb/README.md). +wsdb: bb-cpp-native $(call build,$@,native-packages/wsdb) # Native-package tests. lmdblib and wsdb are self-contained gtest binaries. kvdb has no # C++ tests: its NAPI protocol is driven from node (ts/test) and exercised further by # yarn-project's kv-store tests. wsdb_tests use no bb headers; the bb-header -# parity/equivalence target is manual (WSDB_BUILD_BB_TESTS). wsdb_tests compare aztec-wsdb with -# bb's wsdb-ref binary, which bb-cpp-native builds into the working tree (until native-packages -# leaves aztec-packages: see native-packages/wsdb/cpp/CMakeLists.txt). +# parity/equivalence target is manual (WSDB_BUILD_BB_TESTS). lmdblib-tests: lmdblib $(call test,$@,native-packages/lmdblib) kvdb-tests: kvdb $(call test,$@,native-packages/kvdb) -wsdb-tests: wsdb bb-cpp-native +wsdb-tests: wsdb $(call test,$@,native-packages/wsdb) #============================================================================== diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index ef0cfde18ac..2bb7d773c9e 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -804,25 +804,25 @@ "name": "amd64-linux", "configurePreset": "amd64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi", "libwsdb-ref.a"] }, { "name": "arm64-linux", "configurePreset": "arm64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] + "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi", "libwsdb-ref.a"] }, { "name": "amd64-macos", "configurePreset": "amd64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi", "libwsdb-ref.a"] }, { "name": "arm64-macos", "configurePreset": "arm64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi", "libwsdb-ref.a"] }, { "name": "amd64-windows", diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 72cc79b8d1f..6a5f578cab6 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -274,10 +274,10 @@ function build_release_dir { release_tar build-release/barretenberg-avm-arm64-linux.tar.gz build-arm64-linux/bin bb-avm # Package static libraries for FFI bindings (stripped at build time via CMake POST_BUILD). - tar -czf build-release/barretenberg-static-amd64-linux.tar.gz -C $native_build_dir/lib libbb-external.a - tar -czf build-release/barretenberg-static-arm64-linux.tar.gz -C build-arm64-linux/lib libbb-external.a - tar -czf build-release/barretenberg-static-amd64-darwin.tar.gz -C build-amd64-macos/lib libbb-external.a - tar -czf build-release/barretenberg-static-arm64-darwin.tar.gz -C build-arm64-macos/lib libbb-external.a + tar -czf build-release/barretenberg-static-amd64-linux.tar.gz -C $native_build_dir/lib libbb-external.a libwsdb-ref.a + tar -czf build-release/barretenberg-static-arm64-linux.tar.gz -C build-arm64-linux/lib libbb-external.a libwsdb-ref.a + tar -czf build-release/barretenberg-static-amd64-darwin.tar.gz -C build-amd64-macos/lib libbb-external.a libwsdb-ref.a + tar -czf build-release/barretenberg-static-arm64-darwin.tar.gz -C build-arm64-macos/lib libbb-external.a libwsdb-ref.a tar -czf build-release/barretenberg-static-amd64-windows.tar.gz -C build-amd64-windows/lib libbb-external.a tar -czf build-release/barretenberg-static-arm64-ios.tar.gz -C build-arm64-ios/lib libbb-external.a tar -czf build-release/barretenberg-static-arm64-ios-sim.tar.gz -C build-arm64-ios-sim/lib libbb-external.a diff --git a/barretenberg/cpp/scripts/rename_private_symbols.sh b/barretenberg/cpp/scripts/rename_private_symbols.sh new file mode 100755 index 00000000000..a41cf5cea92 --- /dev/null +++ b/barretenberg/cpp/scripts/rename_private_symbols.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Usage: rename_private_symbols.sh ... +# +# Archives the objects listed in (one per line) into with every symbol they +# define renamed under a private prefix, except the s and the replaceable operator new and +# delete. A consumer's own C++ (its msgpack-c, its libc++ instantiations) then cannot be merged with +# barretenberg's, while the archive keeps ordinary lazy member semantics. The Mach-O counterpart of +# the ELF relocatable link in src/CMakeLists.txt, which ld64.lld cannot do. +set -euo pipefail +nm=$1 objcopy=$2 ar=$3 out=$4 rsp=$5 +shift 5 +mapfile -t objects < "$rsp" +dir=$(mktemp -d "$out.XXXXXX") +trap 'rm -rf "$dir"' EXIT + +printf '%s\n' "$@" > "$dir/exports" +"$nm" --defined-only --extern-only --just-symbol-name "${objects[@]}" 2>/dev/null | + grep -v -e '^$' -e ':$' | sort -u | + grep -vxF -f "$dir/exports" | grep -vE '^__Z(nw|na|dl|da)' | + awk '{ print $1 " _wsdb_ref." $1 }' > "$dir/renames" + +i=0 +for object in "${objects[@]}"; do + "$objcopy" --redefine-syms="$dir/renames" "$object" "$dir/$((i++)).o" +done +rm -f "$out" +(cd "$dir" && "$ar" qc "$out" $(ls *.o | sort -n)) diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index fe247f1a8f0..f2bb6857a5b 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -231,14 +231,12 @@ set(BARRETENBERG_TARGET_OBJECTS # The core object list: what FFI consumers of bb-external need (bb_ipc_ffi_entry()). set(BB_EXTERNAL_TARGET_OBJECTS ${BARRETENBERG_TARGET_OBJECTS}) -# world_state_reference is the in-memory reference merkle DB and its bb_wsref_* C ABI, which external -# world-state implementations link for their conformance tests. It is vm2-free; vm2_stub satisfies its +# world_state_reference is the in-memory reference world state. It is vm2-free; vm2_stub satisfies its # Stats references. if(BB_ARCHIVE_WORLD_STATE_REFERENCE) list(APPEND BARRETENBERG_TARGET_OBJECTS $ $) - list(APPEND BB_EXTERNAL_TARGET_OBJECTS $) endif() add_library( @@ -258,6 +256,70 @@ if(NOT WASM) ) endif() +# libwsdb-ref.a: wsdb-ref behind its FFI entry, plus the poseidon2 C bindings, for aztec-wsdb (which +# links it instead of bb-external). aztec-wsdb is separately built C++ with its own msgpack-c, and a +# static link merges same-named inline and template code across both, so only those C symbols may +# keep their names; everything else, barretenberg's msgpack and libc++ instantiations included, must +# be private to this library. +# ELF: one relocatable object in which everything else is local. lld links the objects as lazy +# members, so only what the exports reach is kept, and --force-group-allocation dissolves the COMDAT +# groups, or the final link would discard this object's copies in favour of the consumer's. +# Mach-O: ld64.lld has no -r (and zig's crashes the final link), so rename_private_symbols.sh renames +# every other symbol the objects define under a private prefix instead. +if(NOT WASM AND BB_WITH_WSDB_REF AND BB_ARCHIVE_WORLD_STATE_REFERENCE AND CMAKE_SYSTEM_NAME MATCHES "Linux|Darwin") + find_program(BB_LLVM_OBJCOPY NAMES llvm-objcopy llvm-objcopy-20) + find_program(BB_LLVM_NM NAMES llvm-nm llvm-nm-20) + # zig's lld (the zig presets' own linker) when there is one. lld links every ELF target, so the + # cross presets need no per-architecture linker. + find_program(BB_ZIG zig) + find_program(BB_LD_LLD NAMES ld.lld ld.lld-20) + if(BB_ZIG) + set(WSDB_REF_LD ${BB_ZIG} ld.lld) + elseif(BB_LD_LLD) + set(WSDB_REF_LD ${BB_LD_LLD}) + endif() + set(WSDB_REF_EXPORTS + wsdb_ref_ipc_ffi_entry wsdb_ref_ipc_ffi_alloc wsdb_ref_ipc_ffi_free + bb_poseidon2_hash bb_poseidon2_hash_pair_with_separator) + set(WSDB_REF_OBJECTS ${BB_EXTERNAL_TARGET_OBJECTS} $ + $ $) + set(WSDB_REF_DIR ${CMAKE_CURRENT_BINARY_DIR}/wsdb_ref_library) + set(WSDB_REF_ARCHIVE ${CMAKE_BINARY_DIR}/lib/libwsdb-ref.a) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND WSDB_REF_LD AND BB_LLVM_OBJCOPY) + # Every object is a lazy archive member (--start-lib), so only what the exports reach is linked. + file(GENERATE OUTPUT ${WSDB_REF_DIR}/objects.rsp + CONTENT "--start-lib\n$\n--end-lib\n") + list(TRANSFORM WSDB_REF_EXPORTS PREPEND "-u" OUTPUT_VARIABLE WSDB_REF_UNDEFINED) + list(TRANSFORM WSDB_REF_EXPORTS PREPEND "--keep-global-symbol=" OUTPUT_VARIABLE WSDB_REF_KEEP) + set(WSDB_REF_COMMANDS + COMMAND ${WSDB_REF_LD} -r --force-group-allocation ${WSDB_REF_UNDEFINED} + -o ${WSDB_REF_DIR}/wsdb_ref.o @${WSDB_REF_DIR}/objects.rsp + COMMAND ${BB_LLVM_OBJCOPY} ${WSDB_REF_KEEP} ${WSDB_REF_DIR}/wsdb_ref.o + COMMAND ${CMAKE_COMMAND} -E rm -f ${WSDB_REF_ARCHIVE} + COMMAND ${CMAKE_AR} qc ${WSDB_REF_ARCHIVE} ${WSDB_REF_DIR}/wsdb_ref.o) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND BB_LLVM_OBJCOPY AND BB_LLVM_NM) + file(GENERATE OUTPUT ${WSDB_REF_DIR}/objects.rsp CONTENT "$\n") + list(TRANSFORM WSDB_REF_EXPORTS PREPEND "_") + set(WSDB_REF_COMMANDS + COMMAND ${CMAKE_SOURCE_DIR}/scripts/rename_private_symbols.sh ${BB_LLVM_NM} ${BB_LLVM_OBJCOPY} + ${CMAKE_AR} ${WSDB_REF_ARCHIVE} ${WSDB_REF_DIR}/objects.rsp ${WSDB_REF_EXPORTS}) + endif() + if(NOT WSDB_REF_COMMANDS) + message(WARNING "Not building libwsdb-ref.a, which aztec-wsdb links: it needs llvm-objcopy, and ld.lld (or zig) on Linux or llvm-nm on macOS") + else() + add_custom_command( + OUTPUT ${WSDB_REF_ARCHIVE} + ${WSDB_REF_COMMANDS} + COMMAND ${CMAKE_RANLIB} ${WSDB_REF_ARCHIVE} + DEPENDS ${WSDB_REF_OBJECTS} ${WSDB_REF_DIR}/objects.rsp ${CMAKE_SOURCE_DIR}/scripts/rename_private_symbols.sh + COMMENT "Archiving libwsdb-ref.a with only its C entry points public" + VERBATIM + ) + # Named after its file so a preset's target list caches it (see preset_cache_paths in bootstrap.sh). + add_custom_target(libwsdb-ref.a ALL DEPENDS ${WSDB_REF_ARCHIVE}) + endif() +endif() + if(WASM) # A standard WASI reactor: wasi-sdk's reactor startup provides _initialize, and the module imports # nothing but WASI. diff --git a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp index 56560c9253b..33e960c5061 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp +++ b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp @@ -9,7 +9,8 @@ // frequency but want no dependency on barretenberg's C++ headers, field type, // stdlib, or build flags (e.g. the standalone world-state DB service). Field // elements cross as 32 canonical bytes (barretenberg's field serialization); -// the montgomery form stays private to barretenberg. +// the montgomery form stays private to barretenberg. Default visibility, as an exported C ABI: the +// libwsdb-ref.a link keeps only exported symbols global (see src/CMakeLists.txt). namespace { using FF = bb::fr; using Poseidon2Bn254 = bb::crypto::Poseidon2; @@ -18,7 +19,7 @@ using Poseidon2Bn254 = bb::crypto::Poseidon2 inputs; inputs.reserve(n); @@ -30,7 +31,7 @@ void bb_poseidon2_hash(const uint8_t* elems, size_t n, uint8_t* out) // Hash { separator, lhs, rhs } — matches Poseidon2HashPolicy::hash_pair_with_separator, // the domain-separated internal-node hash used by the Aztec merkle trees. -void bb_poseidon2_hash_pair_with_separator(uint64_t separator, const uint8_t* lhs, const uint8_t* rhs, uint8_t* out) +__attribute__((visibility("default"))) void bb_poseidon2_hash_pair_with_separator(uint64_t separator, const uint8_t* lhs, const uint8_t* rhs, uint8_t* out) { std::vector inputs{ FF(separator), FF::serialize_from_buffer(lhs), FF::serialize_from_buffer(rhs) }; FF::serialize_to_buffer(Poseidon2Bn254::hash(inputs), out); diff --git a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db_capi.cpp b/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db_capi.cpp deleted file mode 100644 index 20ae3af87ef..00000000000 --- a/barretenberg/cpp/src/barretenberg/world_state_reference/memory_merkle_db_capi.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include -#include -#include - -#include "barretenberg/common/throw_or_abort.hpp" -#include "barretenberg/world_state_reference/memory_merkle_db.hpp" - -// Flat C ABI over the in-memory reference world state, for out-of-tree world-state -// implementations that prove conformance against it (e.g. the standalone wsdb service) -// without depending on barretenberg's C++ headers, field type, or build flags. Field -// elements cross as 32 canonical bytes (barretenberg's field serialization); tree ids -// are the bb::world_state::MerkleTreeId values (0 = nullifier, 1 = note hash, -// 2 = public data, 3 = L1-to-L2 message). Passing a tree id a method does not support -// throws, as in the underlying class; conformance drivers must pass valid ids. -namespace { -using bb::world_state::MemoryMerkleDB; -using bb::world_state::MerkleTreeId; -using FF = bb::fr; - -MemoryMerkleDB& db(void* handle) -{ - return *static_cast(handle); -} - -MerkleTreeId tree(uint8_t tree_id) -{ - return static_cast(tree_id); -} -} // namespace - -extern "C" { - -void* bb_wsref_create(size_t nullifier_tree_prefill, size_t public_data_tree_prefill) -{ - return new MemoryMerkleDB(nullifier_tree_prefill, public_data_tree_prefill); -} - -void bb_wsref_destroy(void* handle) -{ - delete static_cast(handle); -} - -// Snapshot of one tree: root as 32 bytes plus the next free leaf index. -void bb_wsref_get_snapshot(void* handle, uint8_t tree_id, uint8_t* out_root, uint64_t* out_next_index) -{ - const auto roots = db(handle).get_tree_roots(); - const bb::world_state::TreeSnapshot* snap = nullptr; - switch (tree(tree_id)) { - case MerkleTreeId::NULLIFIER_TREE: - snap = &roots.nullifier_tree; - break; - case MerkleTreeId::NOTE_HASH_TREE: - snap = &roots.note_hash_tree; - break; - case MerkleTreeId::PUBLIC_DATA_TREE: - snap = &roots.public_data_tree; - break; - case MerkleTreeId::L1_TO_L2_MESSAGE_TREE: - snap = &roots.l1_to_l2_message_tree; - break; - default: - throw_or_abort("bb_wsref_get_snapshot: unsupported tree id"); - } - FF::serialize_to_buffer(snap->root, out_root); - *out_next_index = snap->next_available_leaf_index; -} - -// Writes the sibling path (leaf to root) as consecutive 32-byte nodes into `out`, which -// must hold tree-height nodes. Returns the node count. -size_t bb_wsref_get_sibling_path(void* handle, uint8_t tree_id, uint64_t leaf_index, uint8_t* out) -{ - const auto path = db(handle).get_sibling_path(tree(tree_id), leaf_index); - for (size_t i = 0; i < path.size(); ++i) { - FF::serialize_to_buffer(path[i], out + (i * 32)); - } - return path.size(); -} - -void bb_wsref_get_low_indexed_leaf( - void* handle, uint8_t tree_id, const uint8_t* key, uint8_t* out_is_present, uint64_t* out_index) -{ - const auto response = db(handle).get_low_indexed_leaf(tree(tree_id), FF::serialize_from_buffer(key)); - *out_is_present = response.is_already_present ? 1 : 0; - *out_index = response.index; -} - -void bb_wsref_get_leaf_value(void* handle, uint8_t tree_id, uint64_t leaf_index, uint8_t* out) -{ - FF::serialize_to_buffer(db(handle).get_leaf_value(tree(tree_id), leaf_index), out); -} - -void bb_wsref_get_nullifier_preimage( - void* handle, uint64_t leaf_index, uint8_t* out_nullifier, uint64_t* out_next_index, uint8_t* out_next_key) -{ - const auto preimage = db(handle).get_leaf_preimage_nullifier_tree(leaf_index); - FF::serialize_to_buffer(preimage.leaf.nullifier, out_nullifier); - *out_next_index = preimage.nextIndex; - FF::serialize_to_buffer(preimage.nextKey, out_next_key); -} - -void bb_wsref_get_public_data_preimage(void* handle, - uint64_t leaf_index, - uint8_t* out_slot, - uint8_t* out_value, - uint64_t* out_next_index, - uint8_t* out_next_key) -{ - const auto preimage = db(handle).get_leaf_preimage_public_data_tree(leaf_index); - FF::serialize_to_buffer(preimage.leaf.slot, out_slot); - FF::serialize_to_buffer(preimage.leaf.value, out_value); - *out_next_index = preimage.nextIndex; - FF::serialize_to_buffer(preimage.nextKey, out_next_key); -} - -void bb_wsref_insert_nullifier(void* handle, const uint8_t* nullifier) -{ - db(handle).insert_indexed_leaves_nullifier_tree( - bb::crypto::merkle_tree::NullifierLeafValue(FF::serialize_from_buffer(nullifier))); -} - -void bb_wsref_insert_public_data(void* handle, const uint8_t* slot, const uint8_t* value) -{ - db(handle).insert_indexed_leaves_public_data_tree(bb::crypto::merkle_tree::PublicDataLeafValue( - FF::serialize_from_buffer(slot), FF::serialize_from_buffer(value))); -} - -// `leaves` is count*32 canonical bytes. -void bb_wsref_append_leaves(void* handle, uint8_t tree_id, const uint8_t* leaves, size_t count) -{ - std::vector values; - values.reserve(count); - for (size_t i = 0; i < count; ++i) { - values.push_back(FF::serialize_from_buffer(leaves + (i * 32))); - } - db(handle).append_leaves(tree(tree_id), values); -} - -void bb_wsref_pad_tree(void* handle, uint8_t tree_id, size_t num_leaves) -{ - db(handle).pad_tree(tree(tree_id), num_leaves); -} - -void bb_wsref_create_checkpoint(void* handle) -{ - db(handle).create_checkpoint(); -} - -void bb_wsref_commit_checkpoint(void* handle) -{ - db(handle).commit_checkpoint(); -} - -void bb_wsref_revert_checkpoint(void* handle) -{ - db(handle).revert_checkpoint(); -} - -uint32_t bb_wsref_get_checkpoint_id(void* handle) -{ - return db(handle).get_checkpoint_id(); -} - -} // extern "C" diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt index 04f3e86824f..659246711cd 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/CMakeLists.txt @@ -1,7 +1,9 @@ # wsdb-ref: the in-memory reference world state (world_state_reference) as a service over the node # world state's contract, wsdb_schema.jsonc here. wsdb-ref owns the schema: the AVM's wsdb client in # vm2_wsdb and native-packages/wsdb's aztec-wsdb are generated from it too. The generated dispatch -# runs the handlers here; the generated FFI entry (wsdb_ipc_ffi_entry) runs the dispatch in-process. +# runs the handlers here; the generated FFI entry (wsdb_ref_ipc_ffi_entry) runs the dispatch in-process. +# The FFI symbols carry this implementation's name, not the schema's service (which names the wire +# protocol aztec-wsdb serves too), so the two can be linked into one binary. # Three artifacts carry it to @aztec-foundation/wsdb-ref (barretenberg/ts/wsdb-ref), one per transport: # - the wsdb-ref binary (native builds), the dispatch served over IPC, for its process transports # (and for a downstream world state's conformance test, which runs it beside its own server); @@ -35,6 +37,7 @@ add_custom_command( --server --ffi --napi + --ffi-name wsdb_ref --cpp-namespace bb::wsdb_ref --cpp-include-dir barretenberg/wsdb_ref/generated DEPENDS ${WSDB_REF_SCHEMA} ${IPC_CODEGEN_SRC} diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp index ffdf5b44e7c..c1b5afd51d2 100644 --- a/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_ref.test.cpp @@ -62,9 +62,9 @@ template Result call_ffi(const char* name, const auto& cmd auto request = encode_request(name, cmd); uint8_t* output = nullptr; size_t output_len = 0; - wsdb_ipc_ffi_entry(request.data(), request.size(), &output, &output_len); + wsdb_ref_ipc_ffi_entry(request.data(), request.size(), &output, &output_len); std::vector frame(output, output + output_len); - wsdb_ipc_ffi_free(output); + wsdb_ref_ipc_ffi_free(output); return decode_response(frame, expected); } @@ -193,9 +193,9 @@ TEST(WsdbRef, FfiEntryAnswersBadRequestsWithErrors) pk.pack_map(0); uint8_t* output = nullptr; size_t output_len = 0; - wsdb_ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &output, &output_len); + wsdb_ref_ipc_ffi_entry(reinterpret_cast(buf.data()), buf.size(), &output, &output_len); std::vector frame(output, output + output_len); - wsdb_ipc_ffi_free(output); + wsdb_ref_ipc_ffi_free(output); EXPECT_EQ(error_of(decode_response(frame, "")), "unknown command: WsdbNoSuchCommand"); diff --git a/barretenberg/ts/bootstrap.sh b/barretenberg/ts/bootstrap.sh index d0f260864b9..c47e3bc67c5 100755 --- a/barretenberg/ts/bootstrap.sh +++ b/barretenberg/ts/bootstrap.sh @@ -93,6 +93,7 @@ function generate_wsdb_ref_package { --schema "$ROOT/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc" \ --lang ts \ --package "$ROOT/barretenberg/ts/wsdb-ref" \ + --ffi-name wsdb_ref \ --package-name "$WSDB_REF_PACKAGE" \ --package-transports uds,shm,napi,wasm \ --package-ipc-path-args 'msgpack,run,--input,{path}' \ diff --git a/ipc-codegen/SCHEMA_SPEC.md b/ipc-codegen/SCHEMA_SPEC.md index 213964104d2..fa5dbcbbe89 100644 --- a/ipc-codegen/SCHEMA_SPEC.md +++ b/ipc-codegen/SCHEMA_SPEC.md @@ -224,10 +224,13 @@ void* _ipc_ffi_alloc(size_t size); void _ipc_ffi_free(void* ptr); ``` -`` is the schema's `service` in snake_case (`Bb` → `bb_ipc_ffi_entry`; -a positional schema uses the generator's type prefix, or the bare names when -there is none). The prefix is what lets several services be linked into one -binary. `input` is exactly the request payload above — no framing envelope, +`` is the implementation's FFI name: by default the schema's `service` in +snake_case (`Bb` → `bb_ipc_ffi_entry`; a positional schema uses the generator's +type prefix, or the bare names when there is none). The prefix is what lets +several services be linked into one binary. The schema's `service` names the wire +protocol, so when several implementations serve one schema each passes its own +`--ffi-name` (`wsdb_ref_ipc_ffi_entry`, `wsdb_labs_ipc_ffi_entry`) and they can +be linked together. `input` is exactly the request payload above — no framing envelope, since an in-process call has no transport — and `*output` receives the response payload in a buffer from `_ipc_ffi_alloc()` that the caller releases with `_ipc_ffi_free()` (both malloc/free-compatible). Input buffers the caller diff --git a/ipc-codegen/src/cpp_codegen.ts b/ipc-codegen/src/cpp_codegen.ts index 6eaf7ac137d..503ca1634eb 100644 --- a/ipc-codegen/src/cpp_codegen.ts +++ b/ipc-codegen/src/cpp_codegen.ts @@ -32,6 +32,8 @@ export interface CppCodegenOptions { * and the server dispatch deserializes into wire types then converts to domain types. */ wireNamespace?: string; + /** The name the FFI symbols carry (`_ipc_ffi_entry`); defaults to the prefix in snake_case. */ + ffiName?: string; } export class CppCodegen { @@ -700,12 +702,13 @@ void serve(const std::string& input_path, Ctx& ctx) * symbols, and the one hook the service defines to hand over its dispatcher. */ /** - * Exported symbol names carry the service (`bb_ipc_ffi_entry`), so libraries of several services - * can be linked into one binary. A schema with no service/prefix keeps the bare names. + * Exported symbol names carry the implementation's FFI name (`bb_ipc_ffi_entry`), so libraries of + * several services, or several implementations of one schema, can be linked into one binary. A + * schema with no service/prefix and no FFI name keeps the bare names. */ private ffiSymbol(name: string): string { - const prefix = this.opts.prefix ? `${toSnakeCase(this.opts.prefix)}_` : ""; - return `${prefix}${name}`; + const ffiName = this.opts.ffiName ?? toSnakeCase(this.opts.prefix); + return ffiName ? `${ffiName}_${name}` : name; } generateFfiHeader(): string { diff --git a/ipc-codegen/src/generate.ts b/ipc-codegen/src/generate.ts index e98ee591998..dfd3c951391 100644 --- a/ipc-codegen/src/generate.ts +++ b/ipc-codegen/src/generate.ts @@ -86,6 +86,7 @@ interface Args { uds: boolean; ffi: boolean; napi: boolean; + ffiName: string; stripMethodPrefix: boolean; stripTypePrefix: boolean; } @@ -142,6 +143,10 @@ Optional: --ffi In-process FFI (rust, zig, cpp). With --client: the FFI client backend. With --server: the exported FFI entry (_ipc_ffi_entry) over the dispatch + --ffi-name The name the FFI symbols carry (_ipc_ffi_entry, and the + wasm export a TS package's wasm transport calls). Defaults + to the schema's service in snake_case; set it when several + implementations serve one schema, so each exports its own --napi With --server --ffi (cpp): also emit _napi.cpp, a Node-API addon over the FFI entry, for a TS package's napi transport (--package-transports napi) @@ -179,6 +184,7 @@ function parseArgs(argv: string[]): Args { uds: false, ffi: false, napi: false, + ffiName: "", stripMethodPrefix: false, stripTypePrefix: false, }; @@ -269,6 +275,9 @@ function parseArgs(argv: string[]): Args { case "--napi": args.napi = true; break; + case "--ffi-name": + args.ffiName = takeValue(); + break; case "--strip-method-prefix": args.stripMethodPrefix = true; break; @@ -513,6 +522,9 @@ function generate(args: Args) { // the bare command. Positional schemas keep the legacy --prefix/--strip flags. const prefix = service || args.prefix || detectPrefix(compiled); const stripMethodPrefix = service ? true : args.stripMethodPrefix; + // The schema's service names the wire protocol (it prefixes every tag), so it is shared by every + // implementation of the schema. The FFI symbols name one implementation. + const ffiName = args.ffiName || toSnakeCase(prefix); console.log( `Schema: ${absSchema} (${compiled.commands.length} commands, prefix=${prefix})`, @@ -660,6 +672,7 @@ function generate(args: Args) { } const packageGen = new TypeScriptPackageCodegen({ prefix, + ffiName, packageName, binaryName, binaryEnvVar: binaryName @@ -715,6 +728,7 @@ function generate(args: Args) { case "rust": { const gen = new RustCodegen({ prefix, + ffiName, stripMethodPrefix: stripMethodPrefix, stripTypePrefix: args.stripTypePrefix, }); @@ -753,6 +767,7 @@ function generate(args: Args) { case "zig": { const gen = new ZigCodegen({ prefix, + ffiName, clientName: `${prefix}Client`, stripMethodPrefix: stripMethodPrefix, }); @@ -796,6 +811,7 @@ function generate(args: Args) { const gen = new CppCodegen({ namespace: ns, prefix, + ffiName, wireNamespace: wireNs, generatedIncludeDir: args.cppIncludeDir, stripMethodPrefix: stripMethodPrefix, diff --git a/ipc-codegen/src/rust_codegen.ts b/ipc-codegen/src/rust_codegen.ts index 305546b1bd3..41e5eb9e732 100644 --- a/ipc-codegen/src/rust_codegen.ts +++ b/ipc-codegen/src/rust_codegen.ts @@ -30,6 +30,8 @@ export interface RustCodegenOptions { typesDocComment?: string; /** Module doc comment for api file */ apiDocComment?: string; + /** The name the FFI symbols carry (`_ipc_ffi_entry`); defaults to the prefix in snake_case. */ + ffiName?: string; } export class RustCodegen { @@ -60,6 +62,7 @@ export class RustCodegen { prefix, stripMethodPrefix: options?.stripMethodPrefix ?? false, stripTypePrefix: options?.stripTypePrefix ?? false, + ffiName: options?.ffiName ?? toSnakeCase(prefix), apiStructName: options?.apiStructName ?? `${name}Api`, backendImport: options?.backendImport ?? "super::backend::Backend", errorImport: options?.errorImport ?? `super::error::{IpcError, Result}`, @@ -927,8 +930,7 @@ pub fn handle_request(handler: &mut dyn Handler, request_bytes: &[u8]) -> Vec_ipc_ffi_entry`. */ + ffiName: string; packageName: string; binaryName: string; binaryEnvVar: string; @@ -798,8 +800,8 @@ export class ${svc}Sync extends SyncApi { : "undefined"; const wide = (key: string, m: PackageWasmModule | undefined) => m ? `\n ${key}: ${moduleEntry(m)},` : ""; - // The module's FFI symbols carry the service name (see the FFI entry in SCHEMA_SPEC.md). - const sym = prefix ? `${toSnakeCase(prefix)}_` : ""; + // The module's FFI symbols carry the implementation's FFI name (see the FFI entry in SCHEMA_SPEC.md). + const sym = this.opts.ffiName ? `${this.opts.ffiName}_` : ""; const ffiExports = ` entry: '${sym}ipc_ffi_entry',`; return `import { diff --git a/ipc-codegen/src/zig_codegen.ts b/ipc-codegen/src/zig_codegen.ts index 4032266451f..96c39ade5ff 100644 --- a/ipc-codegen/src/zig_codegen.ts +++ b/ipc-codegen/src/zig_codegen.ts @@ -29,6 +29,8 @@ export interface ZigCodegenOptions { stripMethodPrefix?: boolean; /** Client struct name (e.g., 'WsdbClient') */ clientName?: string; + /** The name the FFI symbols carry (`_ipc_ffi_entry`); defaults to the prefix in snake_case. */ + ffiName?: string; } export class ZigCodegen { @@ -40,6 +42,7 @@ export class ZigCodegen { prefix: options?.prefix ?? "", clientName: options?.clientName ?? "Client", stripMethodPrefix: options?.stripMethodPrefix ?? false, + ffiName: options?.ffiName ?? toSnakeCase(options?.prefix ?? ""), }; } @@ -48,7 +51,7 @@ export class ZigCodegen { * carry the service (`bb_ipc_ffi_entry`) so several services can be linked into one binary. */ generateFfiBackend(): string { - const sym = this.opts.prefix ? `${toSnakeCase(this.opts.prefix)}_` : ""; + const sym = this.opts.ffiName ? `${this.opts.ffiName}_` : ""; return `//! AUTOGENERATED - DO NOT EDIT //! FFI client backend for ${this.opts.prefix || "service"}: calls the service's in-process FFI //! entry directly (\`${sym}ipc_ffi_entry\`), with no process and no IPC. Link the library @@ -93,7 +96,7 @@ pub const FfiBackend = struct { */ generateFfi(): string { const { prefix } = this.opts; - const sym = prefix ? `${toSnakeCase(prefix)}_` : ""; + const sym = this.opts.ffiName ? `${this.opts.ffiName}_` : ""; const serverFile = `${toSnakeCase(prefix)}_server.zig`; return `//! AUTOGENERATED - DO NOT EDIT //! In-process FFI entry for ${prefix || "service"}: the ipc-codegen FFI backend contract, with the diff --git a/native-packages/wsdb/.gitignore b/native-packages/wsdb/.gitignore index c44a028d9a2..60aa617d1ad 100644 --- a/native-packages/wsdb/.gitignore +++ b/native-packages/wsdb/.gitignore @@ -1,5 +1,6 @@ ts/ cpp/build/ +cpp/build-*/ # ipc-codegen emits the server dispatch into a generated/ subdir at build time. **/generated/ .yarn/* diff --git a/native-packages/wsdb/.rebuild_patterns b/native-packages/wsdb/.rebuild_patterns index e5227bf5c51..28d60043a40 100644 --- a/native-packages/wsdb/.rebuild_patterns +++ b/native-packages/wsdb/.rebuild_patterns @@ -1,8 +1,6 @@ ^native-packages/wsdb/bootstrap\.sh$ ^native-packages/wsdb/package\.json$ ^native-packages/wsdb/\.rebuild_patterns$ -^barretenberg/cpp/src/barretenberg/wsdb_ref/ -^barretenberg/cpp/src/barretenberg/world_state_reference/ ^native-packages/wsdb/cpp/ ^native-packages/lmdblib/ ^native-packages/wsdb/foundation\.pin$ diff --git a/native-packages/wsdb/README.md b/native-packages/wsdb/README.md index 824562deed8..85f37935334 100644 --- a/native-packages/wsdb/README.md +++ b/native-packages/wsdb/README.md @@ -6,18 +6,31 @@ client package (`@aztec-foundation/wsdb`) that launches the server binary. ## Dependencies -wsdb compiles no barretenberg header. Everything it needs from outside this directory, except the -schema, comes from one aztec-packages release, pinned by tag in `foundation.pin` (`cpp/CMakeLists.txt`): +wsdb compiles no barretenberg header. While it lives in aztec-packages it builds against this +checkout; only the protocol constants come from the release pinned by tag in `foundation.pin` +(`cpp/CMakeLists.txt`): | Dependency | Source | |---|---| -| barretenberg (`bb_poseidon2_hash*`, and the reference world state's `bb_wsref_*` C ABI for the conformance test) | `barretenberg-static--.tar.gz` from the barretenberg GitHub release of the tag | -| ipc-runtime (C++, compiled in) and ipc-codegen (the generated dispatch) | a sparse partial clone of aztec-packages at the tag, holding only `ipc-runtime/cpp` and `ipc-codegen` (a few MB) | +| barretenberg's `libwsdb-ref.a`: `bb_poseidon2_hash*`, and wsdb-ref behind its FFI entry (`wsdb_ref_ipc_ffi_entry`) for the differential test. Only those C symbols are global in it, so barretenberg's C++ cannot collide with this package's | the working tree's barretenberg build, `barretenberg/cpp/build/lib/libwsdb-ref.a` (`build-` for a cross build) | +| ipc-runtime (C++, compiled in) and ipc-codegen (the generated dispatch, FFI entry and client) | the working tree | +| the wire contract (`wsdb_schema.jsonc`) | the working tree, `barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc` | | protocol constants (`aztec_constants.hpp`) | `npx @aztec-foundation/constants-codegen@`, which embeds its release's `constants.nr` | -| the `wsdb-ref` binary (the differential test, `wsdb_ref_diff.test.cpp`, runs it beside `aztec-wsdb`) | the working tree's barretenberg build, `barretenberg/cpp/build/bin/wsdb-ref`; the test skips without it. When native-packages leaves aztec-packages, take it from the release instead (see the NOTE in `cpp/CMakeLists.txt`) | -| the wire contract (`wsdb_schema.jsonc`) | the working tree, `barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc`. When native-packages leaves aztec-packages, take it from the sparse clone instead (see the NOTE in `cpp/CMakeLists.txt`) | -The lmdblib sibling package (`../lmdblib`) is always built from source. The TypeScript client is -generated by `bootstrap.sh` from the same ipc-codegen. +When native-packages leaves aztec-packages, all of these come from the pinned release again (see the +NOTE in `cpp/CMakeLists.txt`). + +aztec-wsdb serves the schema over IPC (the `aztec-wsdb` binary) and in-process through its FFI entry, +`wsdb_labs_ipc_ffi_entry`, opened with `wsdb_labs_ffi_open` (`wsdb_ffi_service.hpp`). The differential +test, `wsdb_ref_diff.test.cpp`, drives aztec-wsdb and wsdb-ref through their FFI entries with the +generated typed client and requires both to answer every request alike. + +The lmdblib sibling package (`../lmdblib`) is built from source as part of this build. The +TypeScript client is generated by `bootstrap.sh` from the same ipc-codegen. + +The release publishes aztec-wsdb for linux-x64, linux-arm64, darwin-x64 and darwin-arm64. The host +binary is `build_native`; the other three are `build_cross `, zig cross builds from +`cpp/CMakePresets.json` that link barretenberg's cross build of `libwsdb-ref.a` for that platform +(`barretenberg/cpp/build-/lib/`). `foundation.pin` is the tag and nothing else; edit it to move the pin. diff --git a/native-packages/wsdb/bootstrap.sh b/native-packages/wsdb/bootstrap.sh index 6d6ba73d7de..21288f8b293 100755 --- a/native-packages/wsdb/bootstrap.sh +++ b/native-packages/wsdb/bootstrap.sh @@ -10,17 +10,13 @@ WSDB_BINARY=aztec-wsdb # from once native-packages leaves aztec-packages. WSDB_SCHEMA="$ROOT/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc" -# Everything outside this package comes from the release pinned in foundation.pin (see -# cpp/CMakeLists.txt), so the build's identity is this package plus the lmdblib sibling. -hash=$(hash_str $(../lmdblib/bootstrap.sh hash) $(cache_content_hash .rebuild_patterns)) +# While wsdb lives in aztec-packages it builds against this checkout's barretenberg (its static +# library), ipc-runtime and ipc-codegen (see cpp/CMakeLists.txt), so the build follows their hashes. +hash=$(hash_str $(../lmdblib/bootstrap.sh hash) $(../../barretenberg/cpp/bootstrap.sh hash) \ + $(../../ipc-codegen/bootstrap.sh hash) $(cache_content_hash .rebuild_patterns)) -# ipc-codegen from the sparse clone cpp/CMakeLists.txt makes at configure time. -IPC_CODEGEN_DIR="$PKG/cpp/build/_deps/aztec-packages-src/ipc-codegen" +IPC_CODEGEN_DIR="$ROOT/ipc-codegen" -# Note this runs the codegen from the pinned release, not the working tree, so its flags must be -# what that release accepts. --client and --out are implied by --package in later versions and are -# still accepted there, but the pinned one requires --out; do not drop them until foundation.pin -# moves past the release that made them optional. function generate_ts_package { node --experimental-strip-types --no-warnings \ "$IPC_CODEGEN_DIR/src/generate.ts" \ @@ -35,7 +31,7 @@ function generate_ts_package { --package-ipc-path-args 'msgpack,run,--input,{path}' } -# Build the standalone aztec-wsdb binary against the release pinned in foundation.pin. +# Build the standalone aztec-wsdb binary against this checkout's barretenberg build. function build_native { local build_dir="cpp/build" CC=$(which clang) CXX=$(which clang++) cmake -S cpp -B "$build_dir" -G Ninja >/dev/null @@ -45,6 +41,16 @@ function build_native { cp "$build_dir/bin/$WSDB_BINARY" "$target_dir/$WSDB_BINARY" } +# Cross-compile aztec-wsdb for a non-host release platform with zig (cpp/CMakePresets.json), against +# barretenberg's build of libwsdb-ref.a for that platform. +function build_cross { + local platform=$1 + echo_header "wsdb build_cross $platform" + (cd cpp && cmake --preset "$platform" >/dev/null && cmake --build --preset "$platform") + mkdir -p "ts/build/$platform" + cp "cpp/build-$platform/bin/$WSDB_BINARY" "ts/build/$platform/$WSDB_BINARY" +} + function build { echo_header "wsdb build" build_native @@ -82,11 +88,14 @@ function test { } function clean { - rm -rf ts node_modules cpp/build + rm -rf ts node_modules cpp/build cpp/build-* } function release { build_native + for platform in arm64-linux amd64-macos arm64-macos; do + build_cross $platform + done generate_ts_package npm_install_deps yarn build @@ -97,7 +106,7 @@ function release { (cd ts && retry "deploy_npm ${REF_NAME#v}") } -export -f generate_ts_package build_native build test_cmds test bench_cmds bench clean release +export -f generate_ts_package build_native build_cross build test_cmds test bench_cmds bench clean release case "$cmd" in "") diff --git a/native-packages/wsdb/cpp/CMakeLists.txt b/native-packages/wsdb/cpp/CMakeLists.txt index 8ecd82ebdb6..bfc76474e6f 100644 --- a/native-packages/wsdb/cpp/CMakeLists.txt +++ b/native-packages/wsdb/cpp/CMakeLists.txt @@ -1,6 +1,11 @@ cmake_minimum_required(VERSION 3.24) -# aztec-wsdb: the world-state DB service. Compiles no barretenberg header; everything it needs from -# outside this directory comes from one aztec-packages release, pinned by tag in ../foundation.pin. +# aztec-wsdb: the world-state DB service. Compiles no barretenberg header. What it needs from outside +# this directory (barretenberg's libwsdb-ref.a, ipc-runtime, ipc-codegen and the schema) comes from +# this aztec-packages checkout; only the protocol constants come from the release pinned by tag in +# ../foundation.pin. +# NOTE: when native-packages moves out of aztec-packages, take all of it from the pinned release +# again: libwsdb-ref.a from the release's barretenberg-static--.tar.gz, and ipc-runtime/cpp, +# ipc-codegen and the schema from a sparse clone of aztec-packages at the tag. project(aztec_wsdb CXX C) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -13,27 +18,27 @@ set(pkg "${CMAKE_CURRENT_SOURCE_DIR}/..") set(deps "${CMAKE_BINARY_DIR}/_deps") # --- Dependencies ------------------------------------------------------------------------------- -# barretenberg's static library (the poseidon2 c_bind and the reference world state's C ABI) from -# the GitHub release of the pinned tag, and a sparse clone of aztec-packages at that tag for -# ipc-runtime/cpp (compiled in) and ipc-codegen (run with node). +include(FetchContent) +# barretenberg's libwsdb-ref.a: the poseidon2 C bindings, and wsdb-ref (the reference world state) +# behind its FFI entry, which the differential test drives. It exports only those C symbols, so +# barretenberg's C++ (its own msgpack-c among it) cannot collide with this package's. bb-cpp-native +# builds it. file(READ "${pkg}/foundation.pin" tag) string(STRIP "${tag}" tag) string(REGEX REPLACE "^v" "" version "${tag}") -string(TOLOWER "${CMAKE_SYSTEM_NAME}" os) -string(REPLACE "x86_64" "amd64" arch "${CMAKE_SYSTEM_PROCESSOR}") -string(REPLACE "aarch64" "arm64" arch "${arch}") -include(FetchContent) -FetchContent_Declare(aztec-packages - DOWNLOAD_COMMAND git clone --quiet --filter=blob:none --depth 1 --sparse --branch "${tag}" - https://github.com/AztecProtocol/aztec-packages.git - COMMAND git -C sparse-checkout set ipc-runtime/cpp ipc-codegen) -FetchContent_Declare(barretenberg - URL "https://github.com/AztecProtocol/barretenberg/releases/download/${tag}/barretenberg-static-${arch}-${os}.tar.gz") -FetchContent_MakeAvailable(aztec-packages barretenberg) -set(foundation ${aztec-packages_SOURCE_DIR}) -set(bb_archive ${barretenberg_SOURCE_DIR}/libbb-external.a) -# The lmdblib sibling package: the lmdb C++ wrapper plus the lmdb and msgpack-c sources it fetched. +set(foundation "${pkg}/../..") +set(bb_archive "${foundation}/barretenberg/cpp/build/lib/libwsdb-ref.a" CACHE FILEPATH + "barretenberg's libwsdb-ref.a") +if(NOT EXISTS "${bb_archive}") + message(FATAL_ERROR "No libwsdb-ref.a at ${bb_archive}: build barretenberg/cpp first") +endif() +option(WSDB_BUILD_TESTS "Build wsdb_tests and wsdb_bench" ON) +# The lmdblib sibling package (the lmdb C++ wrapper, with the lmdb and msgpack-c sources it fetches), +# built here for this toolchain and target. set(lmdblib "${pkg}/../lmdblib/cpp") +set(LMDBLIB_BUILD_TESTS OFF CACHE BOOL "" FORCE) +add_subdirectory(${lmdblib} ${deps}/lmdblib) +FetchContent_GetProperties(msgpack) # --- Build -------------------------------------------------------------------------------------- # Match barretenberg's archive build (-march=skylake on x86_64, libc++, Release). add_compile_definitions(MSGPACK_NO_BOOST MSGPACK_USE_STD_VARIANT_ADAPTOR NO_PAR_ALGOS) @@ -43,66 +48,62 @@ endif() add_compile_options(-fconstexpr-steps=100000000 -fbracket-depth=1024 -stdlib=libc++ -Wno-deprecated-declarations -Wno-missing-field-initializers) add_link_options(-stdlib=libc++ -static-libstdc++ -static-libgcc) -include_directories(src ${lmdblib}/src ${lmdblib}/build/_deps/msgpack-src/include - ${lmdblib}/build/_deps/lmdb-src/libraries/liblmdb) +include_directories(src ${lmdblib}/src ${msgpack_SOURCE_DIR}/include) set(IPC_RUNTIME_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(IPC_RUNTIME_BUILD_NAPI OFF CACHE BOOL "" FORCE) add_subdirectory(${foundation}/ipc-runtime/cpp ${deps}/ipc-runtime) # The wire contract is wsdb_schema.jsonc, owned by barretenberg's wsdb-ref (the reference -# implementation). It is the one input read from the working tree rather than the pinned release. -# NOTE: when native-packages moves out of aztec-packages, take the schema from the sparse clone -# instead: add barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc to its sparse-checkout -# above and set wsdb_schema to ${foundation}/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc -# (bootstrap.sh's WSDB_SCHEMA and .rebuild_patterns follow). -set(wsdb_schema "${pkg}/../../barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc") +# implementation). +set(wsdb_schema "${foundation}/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc") -# The generated server dispatch and wire types from that schema, and the protocol constants from -# @aztec-foundation/constants-codegen at the pinned version; both are regenerated whenever configure -# runs, which their inputs below trigger. +# The generated server dispatch, wire types, in-process FFI entry (wsdb_labs_ipc_ffi_entry; its symbols +# carry this implementation's name, as wsdb-ref's carry wsdb_ref) and typed client from that schema, and +# the protocol constants from @aztec-foundation/constants-codegen at the pinned version; both are +# regenerated whenever configure runs, which their inputs below trigger. execute_process( COMMAND node --experimental-strip-types --no-warnings ${foundation}/ipc-codegen/src/generate.ts --schema ${wsdb_schema} --lang cpp --out ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/generated - --server --cpp-namespace azteclabs::wsdb --cpp-include-dir wsdb/generated + --server --client --ffi --ffi-name wsdb_labs --cpp-namespace azteclabs::wsdb + --cpp-include-dir wsdb/generated COMMAND_ERROR_IS_FATAL ANY) execute_process( COMMAND "${CMAKE_COMMAND}" -E env "WSDB_FOUNDATION_VERSION=${version}" "${pkg}/scripts/remake-constants.sh" COMMAND_ERROR_IS_FATAL ANY) set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${wsdb_schema} ${pkg}/foundation.pin ${pkg}/scripts/constants-codegen/cpp.json) +file(GLOB_RECURSE ipc_codegen_sources CONFIGURE_DEPENDS ${foundation}/ipc-codegen/src/*.ts + ${foundation}/ipc-codegen/templates/cpp/*) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${ipc_codegen_sources}) set(CLI11_BUILD_TESTS OFF CACHE INTERNAL "") set(CLI11_BUILD_EXAMPLES OFF CACHE INTERNAL "") set(CLI11_BUILD_DOCS OFF CACHE INTERNAL "") FetchContent_Declare(cli11 GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git GIT_TAG v2.4.2 GIT_SHALLOW TRUE) -set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) -FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.13.0 GIT_SHALLOW TRUE) -set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) -set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE) -FetchContent_Declare(benchmark GIT_REPOSITORY https://github.com/google/benchmark.git GIT_TAG v1.8.3 GIT_SHALLOW TRUE) -FetchContent_MakeAvailable(cli11 googletest benchmark) +FetchContent_MakeAvailable(cli11) file(GLOB_RECURSE sources CONFIGURE_DEPENDS src/*.cpp) file(GLOB_RECURSE tests CONFIGURE_DEPENDS src/*.test.cpp) file(GLOB_RECURSE benches CONFIGURE_DEPENDS src/*.bench.cpp) list(REMOVE_ITEM sources ${tests} ${benches}) -set(libs ${bb_archive} ${lmdblib}/build/lib/liblmdblib.a ipc_runtime ${lmdblib}/build/lib/liblmdb.a pthread) +set(libs ${bb_archive} lmdblib ipc_runtime lmdb pthread) add_executable(aztec-wsdb ${sources}) target_link_libraries(aztec-wsdb PRIVATE ${libs} CLI11::CLI11) -list(REMOVE_ITEM sources ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/main.cpp) -add_executable(wsdb_tests ${sources} ${tests}) -target_link_libraries(wsdb_tests PRIVATE ${libs} CLI11::CLI11 gmock_main gmock gtest) -# wsdb_ref_diff.test.cpp runs the server built alongside next to barretenberg's wsdb-ref binary, taken -# from the working tree's barretenberg build (the test skips when it is not built). -# NOTE: when native-packages moves out of aztec-packages, take wsdb-ref from the pinned release instead -# (its @aztec-foundation/wsdb-ref- package ships the binary), as for the schema above. -set(WSDB_REF_BIN "${pkg}/../../barretenberg/cpp/build/bin/wsdb-ref" CACHE FILEPATH "wsdb-ref binary") -add_dependencies(wsdb_tests aztec-wsdb) -target_compile_definitions(wsdb_tests PRIVATE AZTEC_WSDB_BIN="$" - WSDB_REF_BIN="${WSDB_REF_BIN}") +if(WSDB_BUILD_TESTS) + set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) + FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.13.0 GIT_SHALLOW TRUE) + set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) + set(BENCHMARK_ENABLE_GTEST_TESTS OFF CACHE BOOL "" FORCE) + FetchContent_Declare(benchmark GIT_REPOSITORY https://github.com/google/benchmark.git GIT_TAG v1.8.3 GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(googletest benchmark) -add_executable(wsdb_bench ${sources} ${benches}) -target_link_libraries(wsdb_bench PRIVATE ${libs} CLI11::CLI11 benchmark::benchmark_main) + list(REMOVE_ITEM sources ${CMAKE_CURRENT_SOURCE_DIR}/src/wsdb/main.cpp) + add_executable(wsdb_tests ${sources} ${tests}) + target_link_libraries(wsdb_tests PRIVATE ${libs} CLI11::CLI11 gmock_main gmock gtest) + + add_executable(wsdb_bench ${sources} ${benches}) + target_link_libraries(wsdb_bench PRIVATE ${libs} CLI11::CLI11 benchmark::benchmark_main) +endif() diff --git a/native-packages/wsdb/cpp/CMakePresets.json b/native-packages/wsdb/cpp/CMakePresets.json new file mode 100644 index 00000000000..cc3e58abe09 --- /dev/null +++ b/native-packages/wsdb/cpp/CMakePresets.json @@ -0,0 +1,93 @@ +{ + "version": 5, + "cmakeMinimumRequired": { + "major": 3, + "minor": 24, + "patch": 0 + }, + "configurePresets": [ + { + "name": "cross-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-${presetName}", + "environment": { + "CC": "zig cc", + "CXX": "zig c++", + "CFLAGS": "-g0", + "CXXFLAGS": "-g0" + }, + "cacheVariables": { + "CMAKE_AR": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ar.sh", + "CMAKE_RANLIB": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ranlib.sh", + "CMAKE_BUILD_TYPE": "Release", + "WSDB_BUILD_TESTS": "OFF", + "bb_archive": "${sourceDir}/../../../barretenberg/cpp/build-${presetName}/lib/libwsdb-ref.a" + } + }, + { + "name": "arm64-linux", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target aarch64-linux-gnu.2.35", + "CXX": "zig c++ -target aarch64-linux-gnu.2.35", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Linux" + } + }, + { + "name": "amd64-macos", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target x86_64-macos -mcpu=baseline", + "CXX": "zig c++ -target x86_64-macos -mcpu=baseline", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "x86_64" + } + }, + { + "name": "arm64-macos", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target aarch64-macos -mcpu=apple_a14", + "CXX": "zig c++ -target aarch64-macos -mcpu=apple_a14", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "aarch64" + } + } + ], + "buildPresets": [ + { + "name": "arm64-linux", + "configurePreset": "arm64-linux", + "inheritConfigureEnvironment": true, + "targets": [ + "aztec-wsdb" + ] + }, + { + "name": "amd64-macos", + "configurePreset": "amd64-macos", + "inheritConfigureEnvironment": true, + "targets": [ + "aztec-wsdb" + ] + }, + { + "name": "arm64-macos", + "configurePreset": "arm64-macos", + "inheritConfigureEnvironment": true, + "targets": [ + "aztec-wsdb" + ] + } + ] +} diff --git a/native-packages/wsdb/cpp/src/wsdb/cli.cpp b/native-packages/wsdb/cpp/src/wsdb/cli.cpp index b655b74e9d1..12a1cb375da 100644 --- a/native-packages/wsdb/cpp/src/wsdb/cli.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/cli.cpp @@ -5,6 +5,7 @@ #include "world_state/world_state.hpp" #include "wsdb/generated/wsdb_ipc_server.hpp" #include "wsdb/wsdb_ipc_server.hpp" +#include "wsdb/wsdb_service.hpp" #include #include @@ -36,42 +37,8 @@ int parse_and_run_wsdb(int argc, char* argv[]) msgpack_run_command->add_option( "-i,--input", input_path, "IPC socket/shm path (.sock for UDS, .shm for shared memory)"); - std::string data_dir; - msgpack_run_command->add_option("-d,--data-dir", data_dir, "Data directory for LMDB stores")->required(); - - // Tree heights (JSON map: treeId -> height) - std::string tree_heights_json; - msgpack_run_command->add_option("--tree-heights", tree_heights_json, "Tree heights as JSON: {0:40,1:32,...}"); - - // Tree prefill sizes - std::string tree_prefill_json; - msgpack_run_command->add_option( - "--tree-prefill", tree_prefill_json, "Tree prefill sizes as JSON: {0:128,2:128,...}"); - - // Map sizes (KB) - std::string map_sizes_json; - msgpack_run_command->add_option("--map-sizes", map_sizes_json, "LMDB map sizes in KB as JSON: {0:1024,...}"); - - uint32_t threads = 16; - msgpack_run_command->add_option("-t,--threads", threads, "Thread pool size (default: 16)") - ->check(CLI::PositiveNumber); - - uint32_t initial_header_generator_point = 0; - msgpack_run_command->add_option( - "--initial-header-generator-point", initial_header_generator_point, "Header generator point (default: 0)"); - - // Prefilled public data as JSON array of [slot_hex, value_hex] pairs - std::string prefilled_public_data_json; - msgpack_run_command->add_option( - "--prefilled-public-data", prefilled_public_data_json, "Prefilled public data as JSON array"); - - // Prefilled nullifiers as JSON array of nullifier_hex strings - std::string prefilled_nullifiers_json; - msgpack_run_command->add_option( - "--prefilled-nullifiers", prefilled_nullifiers_json, "Prefilled genesis nullifiers as JSON array"); - - uint64_t genesis_timestamp = 0; - msgpack_run_command->add_option("--genesis-timestamp", genesis_timestamp, "Genesis block timestamp (default: 0)"); + WsdbConfig config; + add_wsdb_config_options(*msgpack_run_command, config); size_t request_ring_size = 1024 * 1024; msgpack_run_command @@ -95,18 +62,7 @@ int parse_and_run_wsdb(int argc, char* argv[]) try { if (msgpack_run_command->parsed()) { - return execute_wsdb_server(input_path, - data_dir, - tree_heights_json, - tree_prefill_json, - map_sizes_json, - threads, - initial_header_generator_point, - prefilled_public_data_json, - prefilled_nullifiers_json, - genesis_timestamp, - request_ring_size, - response_ring_size); + return execute_wsdb_server(input_path, config, request_ring_size, response_ring_size); } } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << '\n'; diff --git a/native-packages/wsdb/cpp/src/wsdb/memory_merkle_db.test.cpp b/native-packages/wsdb/cpp/src/wsdb/memory_merkle_db.test.cpp deleted file mode 100644 index e783063bebc..00000000000 --- a/native-packages/wsdb/cpp/src/wsdb/memory_merkle_db.test.cpp +++ /dev/null @@ -1,313 +0,0 @@ -#include "common/aztec_constants.hpp" -#include "field/field_element.hpp" -#include "merkle_tree/fixtures.hpp" -#include "merkle_tree/indexed_leaf.hpp" -#include "merkle_tree/response.hpp" -#include "world_state/types.hpp" -#include "world_state/world_state.hpp" -#include "wsdb/reference_merkle_db.hpp" -#include -#include -#include -#include -#include -#include - -// This test is the conformance gate for this package's WorldState against barretenberg's -// in-memory reference world state (world_state_reference), reached through its flat C ABI -// (see reference_merkle_db.hpp) so no barretenberg headers are compiled here. The reference -// is the spec for the AVM-facing tree semantics: both sides are driven through an identical -// sequence of operations and must agree, after every step, on roots, sibling paths, low-leaf -// lookups and indexed-leaf preimages. Any divergence here is a conformance bug in this -// package (or a spec change in the reference that this package has not caught up with). - -using namespace azteclabs::wsdb::world_state; -using namespace azteclabs::wsdb::merkle_tree; -using azteclabs::wsdb::fr; -using azteclabs::wsdb::ReferenceMerkleDB; - -namespace { - -constexpr size_t NULLIFIER_PREFILL = 128; -constexpr size_t PUBLIC_DATA_PREFILL = 128; - -class ReferenceConformanceTest : public ::testing::Test { - protected: - void SetUp() override - { - data_dir = random_temp_directory(); - std::filesystem::create_directories(data_dir); - - std::unordered_map tree_heights{ - { MerkleTreeId::NULLIFIER_TREE, NULLIFIER_TREE_HEIGHT }, - { MerkleTreeId::NOTE_HASH_TREE, NOTE_HASH_TREE_HEIGHT }, - { MerkleTreeId::PUBLIC_DATA_TREE, PUBLIC_DATA_TREE_HEIGHT }, - { MerkleTreeId::L1_TO_L2_MESSAGE_TREE, L1_TO_L2_MSG_TREE_HEIGHT }, - { MerkleTreeId::ARCHIVE, ARCHIVE_HEIGHT }, - }; - std::unordered_map tree_prefill{ - { MerkleTreeId::NULLIFIER_TREE, NULLIFIER_PREFILL }, - { MerkleTreeId::PUBLIC_DATA_TREE, PUBLIC_DATA_PREFILL }, - }; - - ws = std::make_unique(/*thread_pool_size=*/1, - data_dir, - /*map_size=*/10240, - tree_heights, - tree_prefill, - DOM_SEP__BLOCK_HEADER_HASH); - - ref = std::make_unique(NULLIFIER_PREFILL, PUBLIC_DATA_PREFILL); - } - - void TearDown() override - { - ws.reset(); - std::filesystem::remove_all(data_dir); - } - - static WorldStateRevision revision() { return uncommitted_revision(); } - - void expect_roots_equal() - { - for (MerkleTreeId tree_id : { MerkleTreeId::NULLIFIER_TREE, - MerkleTreeId::NOTE_HASH_TREE, - MerkleTreeId::PUBLIC_DATA_TREE, - MerkleTreeId::L1_TO_L2_MESSAGE_TREE }) { - const auto snapshot = ref->get_snapshot(tree_id); - const auto info = ws->get_tree_info(revision(), tree_id); - EXPECT_EQ(snapshot.root, info.meta.root) << "root mismatch for tree " << static_cast(tree_id); - EXPECT_EQ(snapshot.next_available_leaf_index, info.meta.size) - << "size mismatch for tree " << static_cast(tree_id); - } - } - - void expect_sibling_path_equal(MerkleTreeId tree_id, index_t leaf_index) - { - EXPECT_EQ(ref->get_sibling_path(tree_id, leaf_index), ws->get_sibling_path(revision(), tree_id, leaf_index)) - << "sibling path mismatch, tree " << static_cast(tree_id) << " index " << leaf_index; - } - - void expect_leaf_value_equal(MerkleTreeId tree_id, index_t leaf_index) - { - auto ws_leaf = ws->get_leaf(revision(), tree_id, leaf_index); - ASSERT_TRUE(ws_leaf.has_value()); - EXPECT_EQ(ref->get_leaf_value(tree_id, leaf_index), ws_leaf.value()) - << "leaf value mismatch, tree " << static_cast(tree_id) << " index " << leaf_index; - } - - void expect_low_leaf_equal(MerkleTreeId tree_id, const fr& key) - { - EXPECT_EQ(ref->get_low_indexed_leaf(tree_id, key), ws->find_low_leaf_index(revision(), tree_id, key)); - } - - void expect_nullifier_preimage_equal(index_t leaf_index) - { - auto ws_leaf = ws->get_indexed_leaf(revision(), MerkleTreeId::NULLIFIER_TREE, leaf_index); - ASSERT_TRUE(ws_leaf.has_value()); - EXPECT_EQ(ref->get_nullifier_preimage(leaf_index), ws_leaf.value()) - << "nullifier preimage mismatch at index " << leaf_index; - } - - void expect_public_data_preimage_equal(index_t leaf_index) - { - auto ws_leaf = - ws->get_indexed_leaf(revision(), MerkleTreeId::PUBLIC_DATA_TREE, leaf_index); - ASSERT_TRUE(ws_leaf.has_value()); - EXPECT_EQ(ref->get_public_data_preimage(leaf_index), ws_leaf.value()) - << "public data preimage mismatch at index " << leaf_index; - } - - std::string data_dir; - std::unique_ptr ws; - std::unique_ptr ref; -}; - -// Genesis state must already match: the indexed trees are prefilled with an ascending linked chain of -// padding leaves, the append-only trees start empty. -TEST_F(ReferenceConformanceTest, GenesisMatches) -{ - expect_roots_equal(); - - // Indexed-tree genesis preimages and sibling paths. (get_leaf_value is only ever called on the - // append-only trees in production, so we don't cross-check leaf values for the indexed trees.) - for (index_t i : { index_t(0), index_t(1), index_t(63), index_t(127) }) { - expect_nullifier_preimage_equal(i); - expect_public_data_preimage_equal(i); - expect_sibling_path_equal(MerkleTreeId::NULLIFIER_TREE, i); - expect_sibling_path_equal(MerkleTreeId::PUBLIC_DATA_TREE, i); - } - - // Append-only genesis (empty trees) sibling paths. - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, 0); - expect_sibling_path_equal(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, 0); - - // Low-leaf lookups over the genesis chain (present and absent keys). - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(1)); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(127)); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(500)); - expect_low_leaf_equal(MerkleTreeId::PUBLIC_DATA_TREE, fr(42)); - expect_low_leaf_equal(MerkleTreeId::PUBLIC_DATA_TREE, fr(5000)); -} - -TEST_F(ReferenceConformanceTest, AppendNoteHashes) -{ - std::vector note_hashes{ fr(111), fr(222), fr(333) }; - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - ref->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - - expect_roots_equal(); - for (index_t i = 0; i < 4; ++i) { - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, i); - } - for (index_t i = 0; i < 3; ++i) { - expect_leaf_value_equal(MerkleTreeId::NOTE_HASH_TREE, i); - } - - // Append to L1->L2 as well. - std::vector msgs{ fr(7), fr(8) }; - ws->append_leaves(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, msgs); - ref->append_leaves(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, msgs); - expect_roots_equal(); - expect_sibling_path_equal(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, 0); - expect_sibling_path_equal(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, 1); - expect_leaf_value_equal(MerkleTreeId::L1_TO_L2_MESSAGE_TREE, 0); -} - -TEST_F(ReferenceConformanceTest, PadNoteHashTree) -{ - std::vector note_hashes{ fr(111), fr(222), fr(333) }; - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - ref->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - - // The AVM pads the note-hash tree to a multiple of MAX_NOTE_HASHES_PER_TX; on the world state that - // is an append of zero leaves, on the reference a pad_tree. - size_t padding = MAX_NOTE_HASHES_PER_TX - (note_hashes.size() % MAX_NOTE_HASHES_PER_TX); - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector(padding, fr(0))); - ref->pad_tree(MerkleTreeId::NOTE_HASH_TREE, padding); - - expect_roots_equal(); - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, 2); - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, padding + 2); -} - -TEST_F(ReferenceConformanceTest, InsertNullifiers) -{ - // Keys must exceed the genesis padding range [0, 128) so each is a genuine insertion; nullifiers - // are not updateable, so re-inserting an existing key would (faithfully) throw on both sides. - for (const fr& nullifier : { fr(1000), fr(500), fr(1000000), fr(750) }) { - // The world state's low-leaf witness captures the pre-insertion state; check the reference - // agrees on it before mutating. - const auto low = ref->get_low_indexed_leaf(MerkleTreeId::NULLIFIER_TREE, nullifier); - const index_t new_index = ref->get_snapshot(MerkleTreeId::NULLIFIER_TREE).next_available_leaf_index; - - auto ws_result = ws->insert_indexed_leaves(MerkleTreeId::NULLIFIER_TREE, { { nullifier } }); - ref->insert_nullifier(nullifier); - - ASSERT_FALSE(ws_result.lowLeafWitnessData.empty()); - EXPECT_EQ(low.index, ws_result.lowLeafWitnessData[0].index); - ASSERT_FALSE(ws_result.insertionWitnessData.empty()); - EXPECT_EQ(new_index, ws_result.insertionWitnessData[0].index); - - expect_roots_equal(); - // The newly inserted leaf and the (mutated) low leaf. - expect_nullifier_preimage_equal(new_index); - expect_nullifier_preimage_equal(low.index); - expect_sibling_path_equal(MerkleTreeId::NULLIFIER_TREE, new_index); - } - - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(500)); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(600)); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(1000)); -} - -TEST_F(ReferenceConformanceTest, InsertAndUpdatePublicData) -{ - // First insertion of a fresh slot. - const index_t new_index = ref->get_snapshot(MerkleTreeId::PUBLIC_DATA_TREE).next_available_leaf_index; - ws->insert_indexed_leaves(MerkleTreeId::PUBLIC_DATA_TREE, { { fr(900), fr(11) } }); - ref->insert_public_data(fr(900), fr(11)); - expect_roots_equal(); - expect_public_data_preimage_equal(new_index); - expect_sibling_path_equal(MerkleTreeId::PUBLIC_DATA_TREE, new_index); - - // Update of the same slot (public-data leaves are updateable; this goes through the "already present" - // branch and mutates the existing leaf in place rather than appending). - ws->insert_indexed_leaves(MerkleTreeId::PUBLIC_DATA_TREE, { { fr(900), fr(99) } }); - ref->insert_public_data(fr(900), fr(99)); - expect_roots_equal(); - expect_public_data_preimage_equal(new_index); - expect_low_leaf_equal(MerkleTreeId::PUBLIC_DATA_TREE, fr(900)); -} - -// Exercises the full checkpoint protocol (create / commit / revert) and confirms the reference tracks -// roots in lockstep with the world state across nested checkpoints. -TEST_F(ReferenceConformanceTest, Checkpoints) -{ - EXPECT_EQ(ref->get_checkpoint_id(), 0u); - - // Outer checkpoint, then a mutation. - ws->checkpoint(CANONICAL_FORK_ID); - ref->create_checkpoint(); - EXPECT_EQ(ref->get_checkpoint_id(), 1u); - - ws->insert_indexed_leaves(MerkleTreeId::NULLIFIER_TREE, { { fr(4242) } }); - ref->insert_nullifier(fr(4242)); - expect_roots_equal(); - - // Nested checkpoint, mutate, then revert it: state returns to the post-insert snapshot. - ws->checkpoint(CANONICAL_FORK_ID); - ref->create_checkpoint(); - EXPECT_EQ(ref->get_checkpoint_id(), 2u); - - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ fr(7) }); - ref->append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector{ fr(7) }); - expect_roots_equal(); - - ws->revert_checkpoint(CANONICAL_FORK_ID); - ref->revert_checkpoint(); - EXPECT_EQ(ref->get_checkpoint_id(), 1u); - expect_roots_equal(); - // The note-hash append was rolled back. - EXPECT_EQ(ref->get_snapshot(MerkleTreeId::NOTE_HASH_TREE).next_available_leaf_index, 0u); - - // Commit the outer checkpoint: the nullifier insertion is kept. - ws->commit_checkpoint(CANONICAL_FORK_ID); - ref->commit_checkpoint(); - EXPECT_EQ(ref->get_checkpoint_id(), 0u); - expect_roots_equal(); - expect_low_leaf_equal(MerkleTreeId::NULLIFIER_TREE, fr(4242)); -} - -// A combined sequence mirroring the AVM fuzzer's genesis seeding, asserting equality at every step. -TEST_F(ReferenceConformanceTest, MixedSequence) -{ - // Register-contract-style nullifier inserts. - for (const fr& nullifier : { fr(0x1111), fr(0x2222) }) { - ws->insert_indexed_leaves(MerkleTreeId::NULLIFIER_TREE, { { nullifier } }); - ref->insert_nullifier(nullifier); - expect_roots_equal(); - } - - // Fee-payer / public-data writes. - ws->insert_indexed_leaves(MerkleTreeId::PUBLIC_DATA_TREE, { { fr(0xABCD), fr(123456) } }); - ref->insert_public_data(fr(0xABCD), fr(123456)); - expect_roots_equal(); - - // Note hashes + padding. - std::vector note_hashes{ fr(1), fr(2) }; - size_t padding = MAX_NOTE_HASHES_PER_TX - (note_hashes.size() % MAX_NOTE_HASHES_PER_TX); - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - ws->append_leaves(MerkleTreeId::NOTE_HASH_TREE, std::vector(padding, fr(0))); - ref->append_leaves(MerkleTreeId::NOTE_HASH_TREE, note_hashes); - ref->pad_tree(MerkleTreeId::NOTE_HASH_TREE, padding); - expect_roots_equal(); - - expect_sibling_path_equal(MerkleTreeId::NULLIFIER_TREE, NULLIFIER_PREFILL); - expect_sibling_path_equal(MerkleTreeId::PUBLIC_DATA_TREE, PUBLIC_DATA_PREFILL); - expect_sibling_path_equal(MerkleTreeId::NOTE_HASH_TREE, 0); - expect_nullifier_preimage_equal(NULLIFIER_PREFILL); - expect_public_data_preimage_equal(PUBLIC_DATA_PREFILL); -} - -} // namespace diff --git a/native-packages/wsdb/cpp/src/wsdb/reference_merkle_db.hpp b/native-packages/wsdb/cpp/src/wsdb/reference_merkle_db.hpp deleted file mode 100644 index ee81b013a38..00000000000 --- a/native-packages/wsdb/cpp/src/wsdb/reference_merkle_db.hpp +++ /dev/null @@ -1,169 +0,0 @@ -#pragma once - -#include "common/aztec_constants.hpp" -#include "field/field_element.hpp" -#include "merkle_tree/indexed_leaf.hpp" -#include "merkle_tree/merkle_tree_id.hpp" -#include "merkle_tree/response.hpp" -#include -#include -#include -#include - -// Flat C ABI into barretenberg's in-memory reference world state (defined in -// libbarretenberg.a, world_state_reference/memory_merkle_db_capi.cpp). The reference is -// the conformance spec for the AVM-facing tree semantics: roots, sibling paths, low-leaf -// lookups, indexed-leaf preimages and checkpointing. Field elements cross as 32 canonical -// bytes; tree ids are the shared MerkleTreeId values. -extern "C" { -void* bb_wsref_create(size_t nullifier_tree_prefill, size_t public_data_tree_prefill); -void bb_wsref_destroy(void* handle); -void bb_wsref_get_snapshot(void* handle, uint8_t tree_id, uint8_t* out_root, uint64_t* out_next_index); -size_t bb_wsref_get_sibling_path(void* handle, uint8_t tree_id, uint64_t leaf_index, uint8_t* out); -void bb_wsref_get_low_indexed_leaf( - void* handle, uint8_t tree_id, const uint8_t* key, uint8_t* out_is_present, uint64_t* out_index); -void bb_wsref_get_leaf_value(void* handle, uint8_t tree_id, uint64_t leaf_index, uint8_t* out); -void bb_wsref_get_nullifier_preimage( - void* handle, uint64_t leaf_index, uint8_t* out_nullifier, uint64_t* out_next_index, uint8_t* out_next_key); -void bb_wsref_get_public_data_preimage(void* handle, - uint64_t leaf_index, - uint8_t* out_slot, - uint8_t* out_value, - uint64_t* out_next_index, - uint8_t* out_next_key); -void bb_wsref_insert_nullifier(void* handle, const uint8_t* nullifier); -void bb_wsref_insert_public_data(void* handle, const uint8_t* slot, const uint8_t* value); -void bb_wsref_append_leaves(void* handle, uint8_t tree_id, const uint8_t* leaves, size_t count); -void bb_wsref_pad_tree(void* handle, uint8_t tree_id, size_t num_leaves); -void bb_wsref_create_checkpoint(void* handle); -void bb_wsref_commit_checkpoint(void* handle); -void bb_wsref_revert_checkpoint(void* handle); -uint32_t bb_wsref_get_checkpoint_id(void* handle); -} - -namespace azteclabs::wsdb { - -/** - * @brief RAII wrapper over barretenberg's reference merkle DB, in wsdb's own types. - * - * Used by the conformance test to drive the reference and this package's WorldState - * through identical operations and compare results, without compiling any barretenberg - * headers (field elements cross the C ABI as 32 canonical bytes, which is FieldElement's - * own representation). - */ -class ReferenceMerkleDB { - public: - struct Snapshot { - fr root; - uint64_t next_available_leaf_index = 0; - bool operator==(const Snapshot&) const = default; - }; - - ReferenceMerkleDB(size_t nullifier_tree_prefill, size_t public_data_tree_prefill) - : handle_(bb_wsref_create(nullifier_tree_prefill, public_data_tree_prefill)) - {} - ~ReferenceMerkleDB() { bb_wsref_destroy(handle_); } - ReferenceMerkleDB(const ReferenceMerkleDB&) = delete; - ReferenceMerkleDB& operator=(const ReferenceMerkleDB&) = delete; - - Snapshot get_snapshot(merkle_tree::MerkleTreeId tree_id) const - { - Snapshot snapshot; - bb_wsref_get_snapshot(handle_, id(tree_id), snapshot.root.data(), &snapshot.next_available_leaf_index); - return snapshot; - } - - merkle_tree::fr_sibling_path get_sibling_path(merkle_tree::MerkleTreeId tree_id, uint64_t leaf_index) const - { - std::vector buffer(static_cast(tree_height(tree_id)) * 32); - size_t count = bb_wsref_get_sibling_path(handle_, id(tree_id), leaf_index, buffer.data()); - merkle_tree::fr_sibling_path path(count); - for (size_t i = 0; i < count; ++i) { - std::memcpy(path[i].data(), buffer.data() + (i * 32), 32); - } - return path; - } - - merkle_tree::GetLowIndexedLeafResponse get_low_indexed_leaf(merkle_tree::MerkleTreeId tree_id, const fr& key) const - { - uint8_t is_present = 0; - uint64_t index = 0; - bb_wsref_get_low_indexed_leaf(handle_, id(tree_id), key.data(), &is_present, &index); - return { is_present != 0, index }; - } - - fr get_leaf_value(merkle_tree::MerkleTreeId tree_id, uint64_t leaf_index) const - { - fr value; - bb_wsref_get_leaf_value(handle_, id(tree_id), leaf_index, value.data()); - return value; - } - - merkle_tree::IndexedLeaf get_nullifier_preimage(uint64_t leaf_index) const - { - merkle_tree::NullifierLeafValue leaf; - uint64_t next_index = 0; - fr next_key; - bb_wsref_get_nullifier_preimage(handle_, leaf_index, leaf.nullifier.data(), &next_index, next_key.data()); - return { leaf, next_index, next_key }; - } - - merkle_tree::IndexedLeaf get_public_data_preimage(uint64_t leaf_index) const - { - merkle_tree::PublicDataLeafValue leaf; - uint64_t next_index = 0; - fr next_key; - bb_wsref_get_public_data_preimage( - handle_, leaf_index, leaf.slot.data(), leaf.value.data(), &next_index, next_key.data()); - return { leaf, next_index, next_key }; - } - - void insert_nullifier(const fr& nullifier) { bb_wsref_insert_nullifier(handle_, nullifier.data()); } - - void insert_public_data(const fr& slot, const fr& value) - { - bb_wsref_insert_public_data(handle_, slot.data(), value.data()); - } - - void append_leaves(merkle_tree::MerkleTreeId tree_id, const std::vector& leaves) - { - std::vector buffer(leaves.size() * 32); - for (size_t i = 0; i < leaves.size(); ++i) { - std::memcpy(buffer.data() + (i * 32), leaves[i].data(), 32); - } - bb_wsref_append_leaves(handle_, id(tree_id), buffer.data(), leaves.size()); - } - - void pad_tree(merkle_tree::MerkleTreeId tree_id, size_t num_leaves) - { - bb_wsref_pad_tree(handle_, id(tree_id), num_leaves); - } - - void create_checkpoint() { bb_wsref_create_checkpoint(handle_); } - void commit_checkpoint() { bb_wsref_commit_checkpoint(handle_); } - void revert_checkpoint() { bb_wsref_revert_checkpoint(handle_); } - uint32_t get_checkpoint_id() const { return bb_wsref_get_checkpoint_id(handle_); } - - private: - static uint8_t id(merkle_tree::MerkleTreeId tree_id) { return static_cast(tree_id); } - - static uint32_t tree_height(merkle_tree::MerkleTreeId tree_id) - { - switch (tree_id) { - case merkle_tree::MerkleTreeId::NULLIFIER_TREE: - return NULLIFIER_TREE_HEIGHT; - case merkle_tree::MerkleTreeId::NOTE_HASH_TREE: - return NOTE_HASH_TREE_HEIGHT; - case merkle_tree::MerkleTreeId::PUBLIC_DATA_TREE: - return PUBLIC_DATA_TREE_HEIGHT; - case merkle_tree::MerkleTreeId::L1_TO_L2_MESSAGE_TREE: - return L1_TO_L2_MSG_TREE_HEIGHT; - default: - return 0; - } - } - - void* handle_; -}; - -} // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.cpp new file mode 100644 index 00000000000..aef02ab9b1d --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.cpp @@ -0,0 +1,79 @@ +// The in-process aztec-wsdb behind the generated FFI entry (wsdb_labs_ipc_ffi_entry): the same WsdbService the IPC server runs, +// called on the caller's thread with no transport. The caller opens it once with the `msgpack run` options. +#include "wsdb/wsdb_ffi_service.hpp" +#include "wsdb/wsdb_service.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace azteclabs::wsdb { + +namespace { +std::unique_ptr& ffi_service() +{ + static std::unique_ptr service; + return service; +} +} // namespace + +AsyncDispatchHandler& ipc_ffi_dispatcher() +{ + // The FFI entry takes its response before it returns, but handlers may respond later from a pool thread, so the + // caller waits here. The caller's thread does the IPC reactor's job: reads run inline on it when idle. + static AsyncDispatchHandler handler = [](std::span request, RawRespond respond) { + auto& service = ffi_service(); + if (!service) { + respond(detail::make_error("aztec-wsdb FFI: call wsdb_labs_ffi_open first")); + return; + } + std::mutex mtx; + std::condition_variable cv; + std::optional> response; + service->handler()(request, [&](std::vector frame) { + std::lock_guard lock(mtx); + response = std::move(frame); + cv.notify_one(); + }); + std::unique_lock lock(mtx); + cv.wait(lock, [&] { return response.has_value(); }); + respond(std::move(*response)); + }; + return handler; +} + +} // namespace azteclabs::wsdb + +IPC_FFI_EXPORT int wsdb_labs_ffi_open(int argc, const char* const* args) +{ + using namespace azteclabs::wsdb; + if (ffi_service()) { + std::cerr << "wsdb_labs_ffi_open: already open" << '\n'; + return 1; + } + CLI::App app{ "wsdb FFI" }; + WsdbConfig config; + add_wsdb_config_options(app, config); + std::vector argv{ "wsdb_ffi" }; + argv.insert(argv.end(), args, args + argc); + try { + app.parse(static_cast(argv.size()), argv.data()); + ffi_service() = std::make_unique(config, []() { return false; }); + } catch (const CLI::ParseError& e) { + return app.exit(e); + } catch (const std::exception& e) { + std::cerr << "wsdb_labs_ffi_open: " << e.what() << '\n'; + return 1; + } + return 0; +} + +IPC_FFI_EXPORT void wsdb_labs_ffi_close() +{ + azteclabs::wsdb::ffi_service().reset(); +} diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.hpp new file mode 100644 index 00000000000..8cd6e0993b6 --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ffi_service.hpp @@ -0,0 +1,16 @@ +#pragma once +/** + * @file wsdb_ffi_service.hpp + * @brief Opening and closing the in-process aztec-wsdb behind its generated FFI entry (wsdb_labs_ipc_ffi_entry). + */ +#include "wsdb/generated/wsdb_ffi.hpp" + +/** + * Opens the in-process wsdb. args are the `aztec-wsdb msgpack run` options other than the transport ones + * (e.g. "-d", "", "--tree-heights", "{0:42,...}", "-t", "4"). Returns 0 on success; prints and returns non-zero + * on bad options or if it is already open. + */ +IPC_FFI_EXPORT int wsdb_labs_ffi_open(int argc, const char* const* args); + +/** Closes the in-process wsdb, waiting for in-flight work. */ +IPC_FFI_EXPORT void wsdb_labs_ffi_close(); diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp index e4f09b236ab..c53d827f9d6 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.cpp @@ -1,200 +1,19 @@ #include "wsdb/wsdb_ipc_server.hpp" -#include "common/log.hpp" -#include "common/thread_pool.hpp" #include "ipc_runtime/ipc_server.hpp" #include "ipc_runtime/serve_helper.hpp" #include "ipc_runtime/signal_handlers.hpp" -#include "merkle_tree/indexed_leaf.hpp" -#include "world_state/world_state.hpp" -#include "wsdb/generated/wsdb_ipc_server.hpp" -#include "wsdb/wsdb_handlers.hpp" -#include "wsdb/wsdb_request.hpp" -#include "wsdb/wsdb_scheduler.hpp" -#include +#include "wsdb/wsdb_service.hpp" -#include -#include -#include #include -#include -#include #include -#include -#include namespace azteclabs::wsdb { -using namespace azteclabs::wsdb::world_state; -using namespace azteclabs::wsdb::merkle_tree; - -// --------------------------------------------------------------------------- -// Simple JSON-like parsing for config maps -// Parses "{0:1024,1:2048,...}" into unordered_map -// --------------------------------------------------------------------------- - -static std::unordered_map parse_tree_uint64_map(const std::string& json) -{ - std::unordered_map result; - if (json.empty()) { - return result; - } - std::string cleaned; - for (char c : json) { - if (c != '{' && c != '}' && c != ' ') { - cleaned += c; - } - } - std::istringstream ss(cleaned); - std::string pair; - while (std::getline(ss, pair, ',')) { - auto colon_pos = pair.find(':'); - if (colon_pos != std::string::npos) { - auto key = static_cast(std::stoi(pair.substr(0, colon_pos))); - auto value = static_cast(std::stoull(pair.substr(colon_pos + 1))); - result[key] = value; - } - } - return result; -} - -static std::unordered_map parse_tree_uint32_map(const std::string& json) -{ - std::unordered_map result; - if (json.empty()) { - return result; - } - auto u64_map = parse_tree_uint64_map(json); - for (const auto& [k, v] : u64_map) { - result[k] = static_cast(v); - } - return result; -} - -static std::unordered_map parse_tree_index_map(const std::string& json) -{ - std::unordered_map result; - if (json.empty()) { - return result; - } - auto u64_map = parse_tree_uint64_map(json); - for (const auto& [k, v] : u64_map) { - result[k] = static_cast(v); - } - return result; -} - -// --------------------------------------------------------------------------- -// Parse prefilled public data from JSON: [["slot_hex","value_hex"],...] -// Each hex string is a 64-char (32-byte) hex-encoded field element. -// --------------------------------------------------------------------------- - -static fr hex_to_fr(const std::string& hex) -{ - std::string cleaned = hex; - if (cleaned.size() >= 2 && cleaned[0] == '0' && (cleaned[1] == 'x' || cleaned[1] == 'X')) { - cleaned = cleaned.substr(2); - } - return fr(cleaned); -} - -static std::vector parse_prefilled_public_data(const std::string& json) -{ - std::vector result; - if (json.empty() || json == "[]") { - return result; - } - - // Simple state-machine parser for [["hex","hex"],["hex","hex"],...] - std::vector hex_values; - std::string current; - bool in_string = false; - - for (char c : json) { - if (c == '"') { - in_string = !in_string; - } else if (in_string) { - current += c; - } else if ((c == ',' || c == ']') && !current.empty()) { - hex_values.push_back(std::move(current)); - current.clear(); - } - } - - // hex_values should have pairs: slot, value, slot, value, ... - if (hex_values.size() % 2 != 0) { - std::cerr << "Warning: odd number of hex values in prefilled public data, ignoring last" << '\n'; - } - for (size_t i = 0; i + 1 < hex_values.size(); i += 2) { - result.emplace_back(hex_to_fr(hex_values[i]), hex_to_fr(hex_values[i + 1])); - } - return result; -} - -// --------------------------------------------------------------------------- -// Parse prefilled nullifiers from JSON: ["nullifier_hex",...] -// Each hex string is a 64-char (32-byte) hex-encoded field element. -// --------------------------------------------------------------------------- - -static std::vector parse_prefilled_nullifiers(const std::string& json) -{ - std::vector result; - if (json.empty() || json == "[]") { - return result; - } - - std::string current; - bool in_string = false; - - for (char c : json) { - if (c == '"') { - in_string = !in_string; - } else if (in_string) { - current += c; - } else if ((c == ',' || c == ']') && !current.empty()) { - result.push_back(hex_to_fr(current)); - current.clear(); - } - } - return result; -} - -// --------------------------------------------------------------------------- -// IPC server execution -// --------------------------------------------------------------------------- - int execute_wsdb_server(const std::string& input_path, - const std::string& data_dir, - const std::string& tree_heights_json, - const std::string& tree_prefill_json, - const std::string& map_sizes_json, - uint32_t threads, - uint32_t initial_header_generator_point, - const std::string& prefilled_public_data_json, - const std::string& prefilled_nullifiers_json, - uint64_t genesis_timestamp, + const WsdbConfig& config, size_t request_ring_size, size_t response_ring_size) { - const uint64_t DEFAULT_MAP_SIZE = 1024UL * 1024; - - // Parse config - auto tree_height = parse_tree_uint32_map(tree_heights_json); - auto tree_prefill = parse_tree_index_map(tree_prefill_json); - - std::unordered_map map_size{ - { world_state::MerkleTreeId::ARCHIVE, DEFAULT_MAP_SIZE }, - { world_state::MerkleTreeId::NULLIFIER_TREE, DEFAULT_MAP_SIZE }, - { world_state::MerkleTreeId::NOTE_HASH_TREE, DEFAULT_MAP_SIZE }, - { world_state::MerkleTreeId::PUBLIC_DATA_TREE, DEFAULT_MAP_SIZE }, - { world_state::MerkleTreeId::L1_TO_L2_MESSAGE_TREE, DEFAULT_MAP_SIZE }, - }; - if (!map_sizes_json.empty()) { - auto parsed = parse_tree_uint64_map(map_sizes_json); - for (const auto& [k, v] : parsed) { - map_size[k] = v; - } - } - // Create the IPC server and listen BEFORE the (potentially slow) WorldState // construction: LMDB open and genesis prefill can take arbitrarily long on // a loaded machine. With the socket already listening, clients connect into @@ -227,63 +46,13 @@ int execute_wsdb_server(const std::string& input_path, } std::cerr << "aztec-wsdb listening on " << input_path << '\n'; - // Parse prefilled public data: JSON array of ["slot_hex","value_hex"] pairs - std::vector prefilled_public_data; - if (!prefilled_public_data_json.empty()) { - prefilled_public_data = parse_prefilled_public_data(prefilled_public_data_json); - std::cerr << "Parsed " << prefilled_public_data.size() << " prefilled public data entries" << '\n'; - } - - // Parse prefilled nullifiers: JSON array of "nullifier_hex" strings. The caller (TS world-state) passes the same - // canonical genesis nullifiers it seeds via the napi path, so the IPC genesis nullifier-tree root matches. - std::vector prefilled_nullifiers; - if (!prefilled_nullifiers_json.empty()) { - prefilled_nullifiers = parse_prefilled_nullifiers(prefilled_nullifiers_json); - std::cerr << "Parsed " << prefilled_nullifiers.size() << " prefilled nullifiers" << '\n'; - } - - // Create WorldState - std::cerr << "Creating WorldState at " << data_dir << " with " << threads << " threads" << '\n'; - auto ws = std::make_unique(threads, - data_dir, - map_size, - tree_height, - tree_prefill, - prefilled_public_data, - prefilled_nullifiers, - initial_header_generator_point, - genesis_timestamp); - - WsdbRequest request{ .world_state = *ws }; - + WsdbService service(config, [&server]() { return server->has_pending_request(); }); std::cerr << "aztec-wsdb IPC server ready" << '\n'; - // Dispatch pool: services requests concurrently so parallel reads aren't - // serialized through the single reactor thread. It is deliberately DISTINCT - // from WorldState's own intra-op pool (the `threads` arg above): mutating - // handlers enqueue subtasks onto that pool and wait() on them, so dispatching - // those handlers onto the SAME pool could deadlock (azteclabs::wsdb::ThreadPool is - // blocking and non-work-stealing). - // - // Sized from the caller-provided `threads` budget (the same value used for - // the WorldState pool), NOT std::thread::hardware_concurrency() — the latter - // ignores cgroup CPU limits and reports the host core count (e.g. 192 in a - // 2-CPU CI container), which would spawn a huge pool per wsdb process and - // exhaust the per-UID thread limit (pthread_create EAGAIN). - uint32_t dispatch_threads = std::max(2, threads); - azteclabs::wsdb::ThreadPool dispatch_pool(dispatch_threads); - - // Server-side ordering: the database, not the client, guarantees per-fork - // read/write consistency. Each handler hands its work to this scheduler via - // schedule_read / schedule_write (which run reads concurrently and serialize - // writes per fork — see WsdbScheduler), so the context carries it. - auto scheduler = std::make_shared(dispatch_pool, *server); - request.scheduler = scheduler.get(); - // Async dispatch: the reactor reads each request and hands it to the // generated handler with a respond callback; the handler decodes, schedules // its work, and responds when done (possibly from a pool thread). - auto handler = make_wsdb_handler(request); + auto& handler = service.handler(); server->run_reactor([&handler](int /*client_id*/, std::span raw, ipc::IpcServer::Respond respond) { handler(raw, std::move(respond)); }); diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp index bf49b818487..cb51b4aedd8 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ipc_server.hpp @@ -1,27 +1,19 @@ #pragma once #include -#include #include namespace azteclabs::wsdb { +struct WsdbConfig; + /** * @brief Start the aztec-wsdb IPC server. * - * Creates a WorldState instance and runs the IPC server loop, dispatching - * incoming msgpack commands via the WsdbCommand NamedUnion. + * Listens on input_path, builds the WsdbService from config and runs the IPC reactor until shutdown. */ int execute_wsdb_server(const std::string& input_path, - const std::string& data_dir, - const std::string& tree_heights_json, - const std::string& tree_prefill_json, - const std::string& map_sizes_json, - uint32_t threads, - uint32_t initial_header_generator_point, - const std::string& prefilled_public_data_json, - const std::string& prefilled_nullifiers_json, - uint64_t genesis_timestamp, + const WsdbConfig& config, size_t request_ring_size, size_t response_ring_size); diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp index 0a0508729a8..fec192377d1 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_ref_diff.test.cpp @@ -2,41 +2,45 @@ * @file wsdb_ref_diff.test.cpp * @brief aztec-wsdb against wsdb-ref, barretenberg's reference world state, request for request. * - * Both serve wsdb_schema.jsonc, so one encoded request goes to both, each a child process reached over its - * socket: aztec-wsdb, and barretenberg's wsdb-ref binary (so no barretenberg code is compiled or linked - * here). A seeded random run builds blocks the way a node does (effects on a - * fork, then SyncBlock), interleaved with reads at every revision, writes and checkpoints on forks, unwinds, - * finalization and history removal. A request must fail on both or on neither, and every success must be - * byte-identical, apart from the store statistics a full status carries. Error messages are counted, not + * Both serve wsdb_schema.jsonc and both are linked in, each behind its generated FFI entry: aztec-wsdb's + * (wsdb_labs_ipc_ffi_entry) from this package's sources, wsdb-ref's (wsdb_ref_ipc_ffi_entry) from barretenberg's + * static library. The test drives each through the generated typed client, WsdbApi. A seeded random run builds blocks + * the way a node does (effects on a fork, then SyncBlock), interleaved with reads at every revision, writes and + * checkpoints on forks, unwinds, finalization and history removal. A request must fail on both or on neither, and + * every success must be equal, apart from the store statistics a full status carries. Error messages are counted, not * compared. * - * Skipped when the wsdb-ref binary (WSDB_REF_BIN, see cpp/CMakeLists.txt) is not built. WSDB_DIFF_SEEDS (default 4) and - * WSDB_DIFF_STEPS (default 400) size the run. + * WSDB_DIFF_SEEDS (default 4) and WSDB_DIFF_STEPS (default 400) size the run; WSDB_DIFF_TRACE prints every request. */ #include "common/aztec_constants.hpp" #include "field/field_element.hpp" -#include "ipc_runtime/ipc_client.hpp" #include "world_state/genesis_protocol_nullifiers.hpp" #include "wsdb/generated/ipc_codegen/msgpack_adaptor.hpp" +#include "wsdb/generated/wsdb_client.hpp" +#include "wsdb/generated/wsdb_ffi_backend.hpp" #include "wsdb/generated/wsdb_types.hpp" +#include "wsdb/wsdb_ffi_service.hpp" #include #include -#include #include #include +#include #include -#include -#include +#include #include #include -#include -#include +#include #include +#include #include -extern char** environ; +// wsdb-ref's FFI entry, in barretenberg's static library. +extern "C" { +void wsdb_ref_ipc_ffi_entry(const uint8_t* input, size_t input_len, uint8_t** output, size_t* output_len); +void wsdb_ref_ipc_ffi_free(void* ptr); +} using namespace azteclabs::wsdb; @@ -44,6 +48,23 @@ namespace { constexpr uint32_t LATEST = std::numeric_limits::max(); +/** An ipc_codegen::Backend over wsdb-ref's FFI entry, as the generated FfiBackend is over aztec-wsdb's. */ +class WsdbRefBackend { + public: + std::vector call(std::span request) + { + uint8_t* out = nullptr; + size_t out_len = 0; + wsdb_ref_ipc_ffi_entry(request.data(), request.size(), &out, &out_len); + std::vector response(out, out + out_len); + wsdb_ref_ipc_ffi_free(out); + return response; + } +}; + +using RefApi = WsdbApi; +using LabsApi = WsdbApi; + template W bytes_of(const fr& value) { W w{}; @@ -56,45 +77,21 @@ fr fr_of(const auto& w) return fr::serialize_from_buffer(w.data()); } -std::vector encode_request(const std::string& name, const auto& cmd) -{ - msgpack::sbuffer buf; - msgpack::packer pk(buf); - pk.pack_array(1); - pk.pack_array(2); - pk.pack(name); - pk.pack(cmd); - return { buf.data(), buf.data() + buf.size() }; -} - -/** A response frame: its tag, and the payload decoded when it is not an error. */ -struct Frame { - std::vector bytes; - std::string tag; - std::string error; - bool is_error() const { return tag == "WsdbErrorResponse"; } -}; - -Frame parse(std::vector bytes) +/** A message or response as msgpack text, for failure messages and traces. */ +template std::string dump(const T& value) { - Frame frame{ .bytes = std::move(bytes), .tag = {}, .error = {} }; - auto unpacked = msgpack::unpack(reinterpret_cast(frame.bytes.data()), frame.bytes.size()); - auto obj = unpacked.get(); - frame.tag.assign(obj.via.array.ptr[0].via.str.ptr, obj.via.array.ptr[0].via.str.size); - if (frame.is_error()) { - wire::WsdbErrorResponse error; - obj.via.array.ptr[1].convert(error); - frame.error = error.message; + if constexpr (std::is_same_v) { + return "{}"; + } else { + msgpack::sbuffer buf; + msgpack::packer pk(buf); + pk.pack(value); + auto unpacked = msgpack::unpack(buf.data(), buf.size()); + std::ostringstream out; + out << unpacked.get(); + std::string text = out.str(); + return text.size() > 1500 ? text.substr(0, 1500) + "..." : text; } - return frame; -} - -template Resp decode(const Frame& frame) -{ - auto unpacked = msgpack::unpack(reinterpret_cast(frame.bytes.data()), frame.bytes.size()); - Resp resp; - unpacked.get().via.array.ptr[1].convert(resp); - return resp; } /** The indexed trees' initial sizes, prefill included: a run's one varied part of the genesis. */ @@ -103,13 +100,10 @@ struct InitialSizes { uint64_t public_data; }; -/** aztec-wsdb's command line: this test's tree shapes and genesis, served on `socket`. */ -std::vector aztec_wsdb_command(const std::string& binary, - const std::filesystem::path& dir, - const std::string& socket, - const InitialSizes& sizes) +/** aztec-wsdb's open options: this test's tree shapes and genesis, its stores in `data_dir`. */ +std::vector aztec_wsdb_options(const std::filesystem::path& data_dir, const InitialSizes& sizes) { - std::filesystem::create_directories(dir / "data"); + std::filesystem::create_directories(data_dir); std::ostringstream nullifiers; nullifiers << "["; auto seeds = world_state::genesis_protocol_nullifiers(); @@ -123,13 +117,8 @@ std::vector aztec_wsdb_command(const std::string& binary, heights << "{0:" << NULLIFIER_TREE_HEIGHT << ",1:" << NOTE_HASH_TREE_HEIGHT << ",2:" << PUBLIC_DATA_TREE_HEIGHT << ",3:" << L1_TO_L2_MSG_TREE_HEIGHT << ",4:" << ARCHIVE_HEIGHT << "}"; return { - binary, - "msgpack", - "run", - "-i", - socket, "-d", - (dir / "data").string(), + data_dir.string(), "-t", "4", "--tree-heights", @@ -145,62 +134,39 @@ std::vector aztec_wsdb_command(const std::string& binary, }; } -/** A wsdb server in a child process, reached over its socket. */ -class WsdbProcess { +/** aztec-wsdb in-process, open for one run: closed, with its stores, when the run ends. */ +class AztecWsdb { public: - WsdbProcess(std::vector args, std::string socket_path) - : socket_path_(std::move(socket_path)) + explicit AztecWsdb(const std::vector& options) { - const std::string binary = args[0]; - std::vector argv; - for (auto& arg : args) { - argv.push_back(arg.data()); - } - argv.push_back(nullptr); - if (posix_spawn(&pid_, binary.c_str(), nullptr, nullptr, argv.data(), environ) != 0) { - throw std::runtime_error("failed to spawn " + binary); + std::vector args; + for (const auto& option : options) { + args.push_back(option.c_str()); } - client_ = ipc::IpcClient::create_socket(socket_path_); - for (int attempt = 0; attempt < 200 && !client_->connect(); ++attempt) { - std::this_thread::sleep_for(std::chrono::milliseconds(25)); + if (wsdb_labs_ffi_open(static_cast(args.size()), args.data()) != 0) { + throw std::runtime_error("wsdb_labs_ffi_open failed"); } } - - ~WsdbProcess() - { - client_->close(); - kill(pid_, SIGTERM); - int status = 0; - waitpid(pid_, &status, 0); - } - - std::vector call(const std::vector& request) - { - if (!client_->send(request.data(), request.size(), 0)) { - throw std::runtime_error("send to " + socket_path_ + " failed"); - } - auto response = client_->receive(0); - if (response.data() == nullptr) { - throw std::runtime_error("receive from " + socket_path_ + " failed"); - } - std::vector out(response.begin(), response.end()); - client_->release(response.size()); - return out; - } - - private: - std::string socket_path_; - pid_t pid_ = 0; - std::unique_ptr client_; + ~AztecWsdb() { wsdb_labs_ffi_close(); } + AztecWsdb(const AztecWsdb&) = delete; + AztecWsdb& operator=(const AztecWsdb&) = delete; + AztecWsdb(AztecWsdb&&) = delete; + AztecWsdb& operator=(AztecWsdb&&) = delete; }; +// Sends one command to both: `BOTH(get_tree_info, cmd)` calls api.get_tree_info(cmd) on each, building the command +// once so both get the same request. +#define BOTH(method, ...) \ + both( \ + #method, \ + [](auto& api, auto&&... args) { return api.method(std::forward(args)...); } __VA_OPT__(, ) \ + __VA_ARGS__) + /** One differential run: a random but node-shaped workload, every request answered by both. */ class DiffRun { public: - DiffRun(WsdbProcess& ref, WsdbProcess& native, const InitialSizes& sizes, uint64_t seed) - : ref_(ref) - , native_(native) - , sizes_(sizes) + DiffRun(const InitialSizes& sizes, uint64_t seed) + : sizes_(sizes) , rng_(seed) {} @@ -209,43 +175,54 @@ class DiffRun { size_t failed_writes_reverted = 0; std::vector message_mismatches; - /** Sends `cmd` to both; returns the reference's frame. */ - template Frame both(const std::string& name, const Cmd& cmd) + /** + * Sends one command to both through `call(api, cmd...)` and compares the answers. Returns the reference's + * response, or nullopt when it failed; a command answering nothing returns std::monostate. + */ + template auto both(const std::string& name, Call call, const Cmd&... cmd) { - auto request = encode_request(name, cmd); - Frame ref = parse(ref_.call(request)); - Frame nat = parse(native_.call(request)); + using Resp = decltype(call(ref_, Cmd(cmd)...)); + using Value = std::conditional_t, std::monostate, Resp>; + auto attempt = [&](auto& api) -> std::variant { + try { + if constexpr (std::is_void_v) { + call(api, Cmd(cmd)...); + return Value{}; + } else { + return call(api, Cmd(cmd)...); + } + } catch (const std::exception& e) { + return std::string(e.what()); + } + }; + auto ref = attempt(ref_); + auto labs = attempt(labs_); ++requests; + auto request = [&]() { return name + " " + (std::string() + ... + dump(cmd)); }; + auto* ref_value = std::get_if(&ref); + auto* labs_value = std::get_if(&labs); if (std::getenv("WSDB_DIFF_TRACE") != nullptr) { - std::cout << "step " << step_ << " " << dump_request(request).substr(0, 600) - << (ref.is_error() - ? " -> error: " + ref.error - : (name == "WsdbCreateFork" || name == "WsdbGetStatus" ? " -> " + dump(ref) : "")) + std::cout << "step " << step_ << " " << request().substr(0, 600) + << (ref_value == nullptr ? " -> error: " + std::get(ref) + : name == "create_fork" || name == "get_status" ? " -> " + dump(*ref_value) + : "") << "\n"; } - if (ref.is_error() != nat.is_error()) { - ADD_FAILURE() << name << " step " << step_ << " " << dump_request(request) << ": wsdb-ref " - << (ref.is_error() ? "failed: " + ref.error : "succeeded") << ", aztec-wsdb " - << (nat.is_error() ? "failed: " + nat.error : "succeeded"); - return ref; - } - if (ref.is_error()) { + if ((ref_value == nullptr) != (labs_value == nullptr)) { + ADD_FAILURE() << "step " << step_ << " " << request() << ": wsdb-ref " + << (ref_value == nullptr ? "failed: " + std::get(ref) : "succeeded") + << ", aztec-wsdb " + << (labs_value == nullptr ? "failed: " + std::get(labs) : "succeeded"); + } else if (ref_value == nullptr) { ++errors; - if (ref.error != nat.error) { - message_mismatches.push_back(name + ": '" + ref.error + "' vs '" + nat.error + "'"); + if (std::get(ref) != std::get(labs)) { + message_mismatches.push_back(name + ": '" + std::get(ref) + "' vs '" + + std::get(labs) + "'"); } - return ref; - } - if (has_full_status(name)) { - compare_status(name, ref, nat); - } else if (name == "WsdbGetStateReference" || name == "WsdbGetInitialStateReference") { - // A node builds a state reference from an unordered map: the entries match, in any order. - EXPECT_EQ(sorted_state(ref), sorted_state(nat)) << name << " step " << step_ << ": state references differ"; - } else if (ref.bytes != nat.bytes) { - ADD_FAILURE() << name << " step " << step_ << ": responses differ\n request: " << dump_request(request) - << "\n wsdb-ref: " << dump(ref) << "\n aztec-wsdb: " << dump(nat); + } else { + compare(*ref_value, *labs_value, request); } - return ref; + return ref_value == nullptr ? std::optional() : std::optional(std::move(*ref_value)); } void run(size_t steps) @@ -275,8 +252,8 @@ class DiffRun { } private: - WsdbProcess& ref_; - WsdbProcess& native_; + RefApi ref_; + LabsApi labs_; InitialSizes sizes_; std::mt19937_64 rng_; size_t step_ = 0; @@ -285,28 +262,8 @@ class DiffRun { std::vector known_note_hashes_; std::vector slots_; - static std::string dump(const Frame& frame) - { - auto unpacked = msgpack::unpack(reinterpret_cast(frame.bytes.data()), frame.bytes.size()); - std::ostringstream out; - out << unpacked.get(); - std::string text = out.str(); - return text.size() > 1500 ? text.substr(0, 1500) + "..." : text; - } - - static std::string dump_request(const std::vector& request) - { - auto unpacked = msgpack::unpack(reinterpret_cast(request.data()), request.size()); - std::ostringstream out; - out << unpacked.get(); - std::string text = out.str(); - return text.size() > 1500 ? text.substr(0, 1500) + "..." : text; - } - - static std::vector> sorted_state(const Frame& frame) + static auto sorted_state(const std::vector& state) { - // Both responses carry `state` as their only field. - auto state = decode(frame).state; std::vector> out; for (const auto& tree : state) { out.emplace_back(tree.treeId, fr_of(tree.root), tree.size); @@ -315,21 +272,23 @@ class DiffRun { return out; } - static bool has_full_status(const std::string& name) + template void compare(const T& ref, const T& labs, const auto& request) { - return name == "WsdbCommit" || name == "WsdbSyncBlock" || name == "WsdbUnwindBlocks" || - name == "WsdbRemoveHistoricalBlocks"; + if constexpr (requires { ref.status.meta; }) { + compare_status(ref.status, labs.status, request); + } else if constexpr (requires { ref.state; }) { + // A node builds a state reference from an unordered map: the entries match, in any order. + EXPECT_EQ(sorted_state(ref.state), sorted_state(labs.state)) + << "step " << step_ << " " << request() << ": state references differ"; + } else if (!(ref == labs)) { + ADD_FAILURE() << "step " << step_ << ": responses differ\n request: " << request() + << "\n wsdb-ref: " << dump(ref) << "\n aztec-wsdb: " << dump(labs); + } } - void compare_status(const std::string& name, const Frame& ref, const Frame& nat) + void compare_status(const wire::WorldStateStatusFull& r, const wire::WorldStateStatusFull& n, const auto& request) { // Store statistics are a node's LMDB sizes; the summary and every tree's meta must match. - auto summary = [](const wire::WorldStateStatusFull& s) { - return std::make_tuple(s.summary.unfinalizedBlockNumber, - s.summary.finalizedBlockNumber, - s.summary.oldestHistoricalBlock, - s.summary.treesAreSynched); - }; auto meta = [](const wire::TreeMeta& m) { return std::make_tuple(m.depth, m.size, @@ -341,28 +300,13 @@ class DiffRun { m.unfinalizedBlockHeight, m.finalizedBlockHeight); }; - auto statuses = [&]() -> std::pair { - if (name == "WsdbCommit") { - return { decode(ref).status, decode(nat).status }; - } - if (name == "WsdbSyncBlock") { - return { decode(ref).status, - decode(nat).status }; - } - if (name == "WsdbUnwindBlocks") { - return { decode(ref).status, - decode(nat).status }; - } - return { decode(ref).status, - decode(nat).status }; - }(); - const auto& [r, n] = statuses; - EXPECT_EQ(summary(r), summary(n)) << name << " step " << step_ << ": status summaries differ"; - EXPECT_EQ(meta(r.meta.nullifierTreeMeta), meta(n.meta.nullifierTreeMeta)) << name << " nullifier meta"; - EXPECT_EQ(meta(r.meta.noteHashTreeMeta), meta(n.meta.noteHashTreeMeta)) << name << " note hash meta"; - EXPECT_EQ(meta(r.meta.publicDataTreeMeta), meta(n.meta.publicDataTreeMeta)) << name << " public data meta"; - EXPECT_EQ(meta(r.meta.messageTreeMeta), meta(n.meta.messageTreeMeta)) << name << " message meta"; - EXPECT_EQ(meta(r.meta.archiveTreeMeta), meta(n.meta.archiveTreeMeta)) << name << " archive meta"; + auto what = [&]() { return "step " + std::to_string(step_) + " " + request(); }; + EXPECT_EQ(r.summary, n.summary) << what() << ": status summaries differ"; + EXPECT_EQ(meta(r.meta.nullifierTreeMeta), meta(n.meta.nullifierTreeMeta)) << what() << ": nullifier meta"; + EXPECT_EQ(meta(r.meta.noteHashTreeMeta), meta(n.meta.noteHashTreeMeta)) << what() << ": note hash meta"; + EXPECT_EQ(meta(r.meta.publicDataTreeMeta), meta(n.meta.publicDataTreeMeta)) << what() << ": public data meta"; + EXPECT_EQ(meta(r.meta.messageTreeMeta), meta(n.meta.messageTreeMeta)) << what() << ": message meta"; + EXPECT_EQ(meta(r.meta.archiveTreeMeta), meta(n.meta.archiveTreeMeta)) << what() << ": archive meta"; } uint64_t uniform(uint64_t lo, uint64_t hi) { return std::uniform_int_distribution(lo, hi)(rng_); } @@ -396,8 +340,7 @@ class DiffRun { uint32_t latest_block() { - auto status = decode(both("WsdbGetStatus", wire::WsdbGetStatus{})); - return static_cast(status.status.unfinalizedBlockNumber); + return static_cast(BOTH(get_status).value().status.unfinalizedBlockNumber); } wire::WorldStateRevision some_revision() @@ -432,10 +375,10 @@ class DiffRun { for (const auto& seed : world_state::genesis_protocol_nullifiers()) { genesis.prefilledNullifiers.push_back(bytes_of(seed)); } - auto request = encode_request("WsdbInit", genesis); - ASSERT_FALSE(parse(ref_.call(request)).is_error()); + // aztec-wsdb takes its genesis when it is opened; wsdb-ref, from Init, which replaces any earlier state. + ref_.init(genesis); slots_.push_back(fr(1000)); - both("WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}); + BOTH(get_initial_state_reference); } void read() @@ -444,34 +387,34 @@ class DiffRun { uint32_t tree = static_cast(uniform(0, 4)); switch (uniform(0, 11)) { case 0: - both("WsdbGetTreeInfo", wire::WsdbGetTreeInfo{ .treeId = tree, .revision = revision }); + BOTH(get_tree_info, wire::WsdbGetTreeInfo{ .treeId = tree, .revision = revision }); break; case 1: - both("WsdbGetStateReference", wire::WsdbGetStateReference{ .revision = revision }); + BOTH(get_state_reference, wire::WsdbGetStateReference{ .revision = revision }); break; case 2: - both("WsdbGetLeafValue", + BOTH(get_leaf_value, wire::WsdbGetLeafValue{ .treeId = coin(80) ? 1u : 4u, .revision = revision, .leafIndex = uniform(0, 300) }); break; case 3: - both("WsdbGetNullifierLeafPreimage", + BOTH(get_nullifier_leaf_preimage, wire::WsdbGetNullifierLeafPreimage{ .revision = revision, .leafIndex = uniform(0, 400) }); - both("WsdbGetNullifierLeafValue", + BOTH(get_nullifier_leaf_value, wire::WsdbGetNullifierLeafValue{ .revision = revision, .leafIndex = uniform(0, 400) }); break; case 4: - both("WsdbGetPublicDataLeafPreimage", + BOTH(get_public_data_leaf_preimage, wire::WsdbGetPublicDataLeafPreimage{ .revision = revision, .leafIndex = uniform(0, 300) }); - both("WsdbGetPublicDataLeafValue", + BOTH(get_public_data_leaf_value, wire::WsdbGetPublicDataLeafValue{ .revision = revision, .leafIndex = uniform(0, 300) }); break; case 5: - both("WsdbGetSiblingPath", + BOTH(get_sibling_path, wire::WsdbGetSiblingPath{ .treeId = tree, .revision = revision, .leafIndex = uniform(0, 400) }); break; case 6: - both("WsdbFindLeafIndices", + BOTH(find_leaf_indices, wire::WsdbFindLeafIndices{ .treeId = 1, .revision = revision, @@ -479,13 +422,13 @@ class DiffRun { .startIndex = uniform(0, 20) }); break; case 7: - both("WsdbFindNullifierLeafIndices", + BOTH(find_nullifier_leaf_indices, wire::WsdbFindNullifierLeafIndices{ .revision = revision, .leaves = { { .nullifier = bytes_of(some_key()) }, { .nullifier = bytes_of(fr(uniform(1, 127))) } }, .startIndex = uniform(0, 140) }); - both("WsdbFindPublicDataLeafIndices", + BOTH(find_public_data_leaf_indices, wire::WsdbFindPublicDataLeafIndices{ .revision = revision, .leaves = { { .slot = bytes_of(some_slot()), @@ -493,32 +436,32 @@ class DiffRun { .startIndex = 0 }); break; case 8: - both("WsdbFindLowLeaf", + BOTH(find_low_leaf, wire::WsdbFindLowLeaf{ .treeId = coin(50) ? 0u : 2u, .revision = revision, .key = bytes_of(coin(50) ? some_key() : some_slot()) }); break; case 9: - both("WsdbFindSiblingPaths", + BOTH(find_sibling_paths, wire::WsdbFindSiblingPaths{ .treeId = 1, .revision = revision, .leaves = { bytes_of(some_note_hash()) } }); - both("WsdbFindNullifierSiblingPaths", + BOTH(find_nullifier_sibling_paths, wire::WsdbFindNullifierSiblingPaths{ .revision = revision, .leaves = { { .nullifier = bytes_of(some_key()) } } }); - both("WsdbFindPublicDataSiblingPaths", + BOTH(find_public_data_sibling_paths, wire::WsdbFindPublicDataSiblingPaths{ .revision = revision, .leaves = { { .slot = bytes_of(some_slot()), .value = bytes_of(fr(0)) } } }); break; case 10: - both("WsdbGetBlockNumbersForLeafIndices", + BOTH(get_block_numbers_for_leaf_indices, wire::WsdbGetBlockNumbersForLeafIndices{ .treeId = tree, .revision = revision, .leafIndices = { 0, uniform(0, 140), uniform(0, 400) } }); break; default: - both("WsdbGetStatus", wire::WsdbGetStatus{}); - both("WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{}); + BOTH(get_status); + BOTH(get_initial_state_reference); } } @@ -586,8 +529,8 @@ class DiffRun { { std::vector others(forks_.begin() + 1, forks_.end()); if (others.empty() || coin(15)) { - auto created = both("WsdbCreateFork", wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }); - forks_.push_back(decode(created).forkId); + auto created = BOTH(create_fork, wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }); + forks_.push_back(created.value().forkId); return forks_.back(); } return others[uniform(0, others.size() - 1)]; @@ -598,87 +541,86 @@ class DiffRun { uint64_t fork = coin(25) ? 0 : other_fork(); if (fork == 0 && coin(50)) { // Writes that cannot fail, so the canonical fork keeps growing. - both("WsdbAppendLeaves", + BOTH(append_leaves, wire::WsdbAppendLeaves{ .treeId = coin(70) ? 1u : 3u, .leaves = some_note_hashes(uniform(0, 5)), .forkId = 0 }); - both("WsdbSequentialInsertNullifier", + BOTH(sequential_insert_nullifier, wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(uniform(0, 3), true, true), .forkId = 0 }); - both("WsdbBatchInsertPublicData", + BOTH(batch_insert_public_data, wire::WsdbBatchInsertPublicData{ .leaves = distinct_public_data(4), .subtreeDepth = 2, .forkId = 0 }); return; } // As the public processor does with each tx: a checkpoint around the write, reverted if the write fails. bool guarded = coin(50); if (guarded) { - both("WsdbCreateCheckpoint", wire::WsdbCreateCheckpoint{ .forkId = fork }); + BOTH(create_checkpoint, wire::WsdbCreateCheckpoint{ .forkId = fork }); } - Frame result; + bool failed = false; switch (uniform(0, 6)) { case 0: - both("WsdbAppendLeaves", + BOTH(append_leaves, wire::WsdbAppendLeaves{ .treeId = coin(70) ? 1u : 3u, .leaves = some_note_hashes(uniform(0, 5)), .forkId = fork }); break; case 1: - result = both("WsdbSequentialInsertNullifier", + failed = !BOTH(sequential_insert_nullifier, wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(uniform(0, 4), true, false), - .forkId = fork }); + .forkId = fork }).has_value(); break; case 2: - result = both("WsdbSequentialInsertPublicData", + failed = !BOTH(sequential_insert_public_data, wire::WsdbSequentialInsertPublicData{ .leaves = some_public_data(uniform(0, 4), true), - .forkId = fork }); + .forkId = fork }).has_value(); break; case 3: - result = both("WsdbBatchInsertNullifier", + failed = !BOTH(batch_insert_nullifier, wire::WsdbBatchInsertNullifier{ .leaves = some_nullifiers(4, true, false), .subtreeDepth = static_cast(uniform(0, 2)), - .forkId = fork }); + .forkId = fork }).has_value(); break; case 4: - result = both("WsdbBatchInsertPublicData", + failed = !BOTH(batch_insert_public_data, wire::WsdbBatchInsertPublicData{ .leaves = some_public_data(4, true), .subtreeDepth = static_cast(uniform(0, 2)), - .forkId = fork }); + .forkId = fork }).has_value(); break; case 5: - result = both("WsdbAppendNullifierLeaves", + failed = !BOTH(append_nullifier_leaves, wire::WsdbAppendNullifierLeaves{ .leaves = some_nullifiers(uniform(1, 3), true, false), - .forkId = fork }); - if (result.is_error()) { + .forkId = fork }).has_value(); + if (failed) { break; } - result = both( - "WsdbAppendPublicDataLeaves", - wire::WsdbAppendPublicDataLeaves{ .leaves = some_public_data(uniform(1, 3), true), .forkId = fork }); + failed = !BOTH(append_public_data_leaves, + wire::WsdbAppendPublicDataLeaves{ .leaves = some_public_data(uniform(1, 3), true), .forkId = fork }).has_value(); break; default: { - auto state = both("WsdbGetStateReference", + auto state = BOTH(get_state_reference, wire::WsdbGetStateReference{ .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }); - if (state.is_error()) { + if (!state) { break; } - auto block_state = decode(state).state; + auto block_state = state->state; if (coin(20)) { - block_state = decode( - both("WsdbGetInitialStateReference", wire::WsdbGetInitialStateReference{})) - .state; + block_state = BOTH(get_initial_state_reference).value().state; } - both( - "WsdbUpdateArchive", + BOTH(update_archive, wire::WsdbUpdateArchive{ .blockStateRef = block_state, .blockHeaderHash = bytes_of(fr(uniform(1, 1ULL << 62))), .forkId = fork }); } } if (guarded) { - if (result.is_error()) { + if (failed) { failed_writes_reverted++; } - both(result.is_error() || coin(50) ? "WsdbRevertCheckpoint" : "WsdbCommitCheckpoint", - wire::WsdbRevertCheckpoint{ .forkId = fork }); + if (failed || coin(50)) { + BOTH(revert_checkpoint, wire::WsdbRevertCheckpoint{ .forkId = fork }); + } else { + BOTH(commit_checkpoint, wire::WsdbCommitCheckpoint{ .forkId = fork }); + } } } @@ -688,17 +630,20 @@ class DiffRun { switch (uniform(0, 4)) { case 0: case 1: - both("WsdbCreateCheckpoint", wire::WsdbCreateCheckpoint{ .forkId = fork }); + BOTH(create_checkpoint, wire::WsdbCreateCheckpoint{ .forkId = fork }); break; case 2: - both(coin(50) ? "WsdbCommitCheckpoint" : "WsdbRevertCheckpoint", - wire::WsdbCommitCheckpoint{ .forkId = fork }); + if (coin(50)) { + BOTH(commit_checkpoint, wire::WsdbCommitCheckpoint{ .forkId = fork }); + } else { + BOTH(revert_checkpoint, wire::WsdbRevertCheckpoint{ .forkId = fork }); + } break; case 3: - both("WsdbCommitAllCheckpoints", wire::WsdbCommitAllCheckpoints{ .forkId = fork }); + BOTH(commit_all_checkpoints, wire::WsdbCommitAllCheckpoints{ .forkId = fork }); break; default: - both("WsdbRevertAllCheckpoints", wire::WsdbRevertAllCheckpoints{ .forkId = fork }); + BOTH(revert_all_checkpoints, wire::WsdbRevertAllCheckpoints{ .forkId = fork }); } } @@ -706,16 +651,16 @@ class DiffRun { { if (coin(60) || forks_.size() < 2) { bool latest = coin(70); - auto frame = - both("WsdbCreateFork", + auto created = + BOTH(create_fork, wire::WsdbCreateFork{ .latest = latest, .blockNumber = static_cast(uniform(0, latest_block() + 1)) }); - if (!frame.is_error()) { - forks_.push_back(decode(frame).forkId); + if (created) { + forks_.push_back(created->forkId); } } else { size_t at = uniform(0, forks_.size() - 1); - both("WsdbDeleteFork", wire::WsdbDeleteFork{ .forkId = forks_[at] }); + BOTH(delete_fork, wire::WsdbDeleteFork{ .forkId = forks_[at] }); if (forks_[at] != 0) { forks_.erase(forks_.begin() + static_cast(at)); } @@ -725,13 +670,13 @@ class DiffRun { void canonical_op() { // Direct writes to the canonical fork, then a commit or a rollback. - both("WsdbAppendLeaves", wire::WsdbAppendLeaves{ .treeId = 1, .leaves = some_note_hashes(2), .forkId = 0 }); - both("WsdbSequentialInsertNullifier", + BOTH(append_leaves, wire::WsdbAppendLeaves{ .treeId = 1, .leaves = some_note_hashes(2), .forkId = 0 }); + BOTH(sequential_insert_nullifier, wire::WsdbSequentialInsertNullifier{ .leaves = some_nullifiers(1, false, true), .forkId = 0 }); if (coin(50)) { - both("WsdbCommit", wire::WsdbCommit{}); + BOTH(commit); } else { - both("WsdbRollback", wire::WsdbRollback{}); + BOTH(rollback); } } @@ -740,15 +685,15 @@ class DiffRun { uint32_t latest = latest_block(); switch (uniform(0, 2)) { case 0: - both("WsdbFinalizeBlocks", + BOTH(finalize_blocks, wire::WsdbFinalizeBlocks{ .toBlockNumber = static_cast(uniform(0, latest + 1)) }); break; case 1: - both("WsdbUnwindBlocks", + BOTH(unwind_blocks, wire::WsdbUnwindBlocks{ .toBlockNumber = static_cast(uniform(0, latest)) }); break; default: - both("WsdbRemoveHistoricalBlocks", + BOTH(remove_historical_blocks, wire::WsdbRemoveHistoricalBlocks{ .toBlockNumber = static_cast(uniform(0, latest + 1)) }); } // Forks a chain operation dropped are gone on both; forget them. @@ -756,58 +701,61 @@ class DiffRun { if (fork == 0) { return false; } - return parse(ref_.call(encode_request( - "WsdbGetTreeInfo", - wire::WsdbGetTreeInfo{ - .treeId = 0, - .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }))) - .is_error(); + try { + ref_.get_tree_info(wire::WsdbGetTreeInfo{ + .treeId = 0, .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }); + return false; + } catch (const std::exception&) { + return true; + } }); } /** Builds a block as a node does: its effects on a fork at the tip, then the same effects synced to fork 0. */ void build_block() { - auto created = both("WsdbCreateFork", wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }); - uint64_t fork = decode(created).forkId; + auto created = BOTH(create_fork, wire::WsdbCreateFork{ .latest = true, .blockNumber = 0 }); + uint64_t fork = created.value().forkId; auto notes = some_note_hashes(coin(30) ? 64 : 8); auto messages = coin(30) ? some_note_hashes(4) : std::vector{}; auto nullifiers = some_nullifiers(coin(30) ? 64 : 8, true, true); auto public_data = some_public_data(uniform(0, 4), false); - both("WsdbAppendLeaves", wire::WsdbAppendLeaves{ .treeId = 1, .leaves = notes, .forkId = fork }); - both("WsdbAppendLeaves", wire::WsdbAppendLeaves{ .treeId = 3, .leaves = messages, .forkId = fork }); + BOTH(append_leaves, wire::WsdbAppendLeaves{ .treeId = 1, .leaves = notes, .forkId = fork }); + BOTH(append_leaves, wire::WsdbAppendLeaves{ .treeId = 3, .leaves = messages, .forkId = fork }); auto inserted = - both("WsdbBatchInsertNullifier", + BOTH(batch_insert_nullifier, wire::WsdbBatchInsertNullifier{ .leaves = nullifiers, .subtreeDepth = nullifiers.size() == 64 ? 6u : 3u, .forkId = fork }); - both("WsdbSequentialInsertPublicData", + BOTH(sequential_insert_public_data, wire::WsdbSequentialInsertPublicData{ .leaves = public_data, .forkId = fork }); - auto state = decode( - both("WsdbGetStateReference", - wire::WsdbGetStateReference{ - .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } })); + auto state = BOTH(get_state_reference, + wire::WsdbGetStateReference{ + .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }) + .value(); fr header_hash(uniform(1, 1ULL << 62)); - both("WsdbUpdateArchive", + BOTH(update_archive, wire::WsdbUpdateArchive{ .blockStateRef = state.state, .blockHeaderHash = bytes_of(header_hash), .forkId = fork }); - auto archive = decode( - both("WsdbGetTreeInfo", - wire::WsdbGetTreeInfo{ - .treeId = 4, .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } })); - auto previous = decode( - both("WsdbGetTreeInfo", - wire::WsdbGetTreeInfo{ - .treeId = 4, .revision = { .forkId = 0, .blockNumber = LATEST, .includeUncommitted = false } })); - both("WsdbDeleteFork", wire::WsdbDeleteFork{ .forkId = fork }); - if (inserted.is_error()) { + auto archive = BOTH(get_tree_info, + wire::WsdbGetTreeInfo{ + .treeId = 4, + .revision = { .forkId = fork, .blockNumber = LATEST, .includeUncommitted = true } }) + .value(); + auto previous = BOTH(get_tree_info, + wire::WsdbGetTreeInfo{ + .treeId = 4, + .revision = { .forkId = 0, .blockNumber = LATEST, .includeUncommitted = false } }) + .value(); + BOTH(delete_fork, wire::WsdbDeleteFork{ .forkId = fork }); + if (!inserted) { return; } // Sometimes a block whose claimed state is wrong, which both must reject without committing. if (coin(10)) { state.state[0].size += 1; } - both("WsdbSyncBlock", + BOTH(sync_block, wire::WsdbSyncBlock{ .blockNumber = 0, .blockStateRef = state.state, .blockHeaderHash = bytes_of(header_hash), @@ -825,22 +773,16 @@ class DiffRun { TEST(WsdbRefDiff, MatchesWsdbRefRequestForRequest) { - if (!std::filesystem::exists(WSDB_REF_BIN)) { - GTEST_SKIP() << "no wsdb-ref binary at " << WSDB_REF_BIN << " (build barretenberg's wsdb-ref target)"; - } size_t seeds = std::getenv("WSDB_DIFF_SEEDS") ? std::stoul(std::getenv("WSDB_DIFF_SEEDS")) : 4; size_t steps = std::getenv("WSDB_DIFF_STEPS") ? std::stoul(std::getenv("WSDB_DIFF_STEPS")) : 400; for (uint64_t seed = 1; seed <= seeds && !HasFailure(); ++seed) { auto dir = std::filesystem::temp_directory_path() / ("wsdb-ref-diff-" + std::to_string(getpid()) + "-" + std::to_string(seed)); { - auto native_socket = (dir / "aztec-wsdb.sock").string(); - auto ref_socket = (dir / "wsdb-ref.sock").string(); // Odd seeds run the protocol's initial sizes, even ones others. InitialSizes sizes = seed % 2 == 1 ? InitialSizes{ 128, 128 } : InitialSizes{ 64, 256 }; - WsdbProcess native(aztec_wsdb_command(AZTEC_WSDB_BIN, dir, native_socket, sizes), native_socket); - WsdbProcess ref({ WSDB_REF_BIN, "msgpack", "run", "--input", ref_socket }, ref_socket); - DiffRun run(ref, native, sizes, seed); + AztecWsdb aztec_wsdb(aztec_wsdb_options(dir / "data", sizes)); + DiffRun run(sizes, seed); run.run(steps); std::cout << "seed " << seed << ": " << run.requests << " requests, " << run.errors << " answered with errors on both, " << run.message_mismatches.size() diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_scheduler.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_scheduler.hpp index 1a77644b3df..b24acd9f6b3 100644 --- a/native-packages/wsdb/cpp/src/wsdb/wsdb_scheduler.hpp +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_scheduler.hpp @@ -29,7 +29,6 @@ */ #include "common/thread_pool.hpp" -#include "ipc_runtime/ipc_server.hpp" #include #include @@ -44,9 +43,13 @@ namespace azteclabs::wsdb { class WsdbScheduler : public std::enable_shared_from_this { public: - WsdbScheduler(azteclabs::wsdb::ThreadPool& pool, ipc::IpcServer& server) + /** + * @param has_pending_request Whether another request is already waiting to be read; a read runs inline only + * when this is false and nothing is in flight. Called only on the reactor thread. + */ + WsdbScheduler(azteclabs::wsdb::ThreadPool& pool, std::function has_pending_request) : pool_(pool) - , server_(server) + , has_pending_request_(std::move(has_pending_request)) {} // A read on `fork`. Committed reads (independent snapshots) run concurrently @@ -94,7 +97,7 @@ class WsdbScheduler : public std::enable_shared_from_this { // nothing in flight this request is alone and ordering is moot. bool run_inline_if_idle(std::function& work) { - if (inflight_.load(std::memory_order_acquire) == 0 && !server_.has_pending_request()) { + if (inflight_.load(std::memory_order_acquire) == 0 && !has_pending_request_()) { work(); return true; } @@ -161,7 +164,7 @@ class WsdbScheduler : public std::enable_shared_from_this { } azteclabs::wsdb::ThreadPool& pool_; - ipc::IpcServer& server_; + std::function has_pending_request_; std::mutex mtx_; std::unordered_map lanes_; // per fork; references stable (never erased) std::atomic inflight_{ 0 }; // submitted-but-not-completed ops (queued + running) diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_service.cpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_service.cpp new file mode 100644 index 00000000000..99f16e35912 --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_service.cpp @@ -0,0 +1,235 @@ +#include "wsdb/wsdb_service.hpp" +#include "merkle_tree/indexed_leaf.hpp" +#include "wsdb/wsdb_handlers.hpp" +#include "wsdb/wsdb_scheduler.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace azteclabs::wsdb { + +using namespace azteclabs::wsdb::world_state; +using namespace azteclabs::wsdb::merkle_tree; + +// --------------------------------------------------------------------------- +// Simple JSON-like parsing for config maps +// Parses "{0:1024,1:2048,...}" into unordered_map +// --------------------------------------------------------------------------- + +static std::unordered_map parse_tree_uint64_map(const std::string& json) +{ + std::unordered_map result; + if (json.empty()) { + return result; + } + std::string cleaned; + for (char c : json) { + if (c != '{' && c != '}' && c != ' ') { + cleaned += c; + } + } + std::istringstream ss(cleaned); + std::string pair; + while (std::getline(ss, pair, ',')) { + auto colon_pos = pair.find(':'); + if (colon_pos != std::string::npos) { + auto key = static_cast(std::stoi(pair.substr(0, colon_pos))); + auto value = static_cast(std::stoull(pair.substr(colon_pos + 1))); + result[key] = value; + } + } + return result; +} + +static std::unordered_map parse_tree_uint32_map(const std::string& json) +{ + std::unordered_map result; + if (json.empty()) { + return result; + } + auto u64_map = parse_tree_uint64_map(json); + for (const auto& [k, v] : u64_map) { + result[k] = static_cast(v); + } + return result; +} + +static std::unordered_map parse_tree_index_map(const std::string& json) +{ + std::unordered_map result; + if (json.empty()) { + return result; + } + auto u64_map = parse_tree_uint64_map(json); + for (const auto& [k, v] : u64_map) { + result[k] = static_cast(v); + } + return result; +} + +// --------------------------------------------------------------------------- +// Parse prefilled public data from JSON: [["slot_hex","value_hex"],...] +// Each hex string is a 64-char (32-byte) hex-encoded field element. +// --------------------------------------------------------------------------- + +static fr hex_to_fr(const std::string& hex) +{ + std::string cleaned = hex; + if (cleaned.size() >= 2 && cleaned[0] == '0' && (cleaned[1] == 'x' || cleaned[1] == 'X')) { + cleaned = cleaned.substr(2); + } + return fr(cleaned); +} + +static std::vector parse_prefilled_public_data(const std::string& json) +{ + std::vector result; + if (json.empty() || json == "[]") { + return result; + } + + // Simple state-machine parser for [["hex","hex"],["hex","hex"],...] + std::vector hex_values; + std::string current; + bool in_string = false; + + for (char c : json) { + if (c == '"') { + in_string = !in_string; + } else if (in_string) { + current += c; + } else if ((c == ',' || c == ']') && !current.empty()) { + hex_values.push_back(std::move(current)); + current.clear(); + } + } + + // hex_values should have pairs: slot, value, slot, value, ... + if (hex_values.size() % 2 != 0) { + std::cerr << "Warning: odd number of hex values in prefilled public data, ignoring last" << '\n'; + } + for (size_t i = 0; i + 1 < hex_values.size(); i += 2) { + result.emplace_back(hex_to_fr(hex_values[i]), hex_to_fr(hex_values[i + 1])); + } + return result; +} + +// --------------------------------------------------------------------------- +// Parse prefilled nullifiers from JSON: ["nullifier_hex",...] +// Each hex string is a 64-char (32-byte) hex-encoded field element. +// --------------------------------------------------------------------------- + +static std::vector parse_prefilled_nullifiers(const std::string& json) +{ + std::vector result; + if (json.empty() || json == "[]") { + return result; + } + + std::string current; + bool in_string = false; + + for (char c : json) { + if (c == '"') { + in_string = !in_string; + } else if (in_string) { + current += c; + } else if ((c == ',' || c == ']') && !current.empty()) { + result.push_back(hex_to_fr(current)); + current.clear(); + } + } + return result; +} + + +void add_wsdb_config_options(CLI::App& app, WsdbConfig& config) +{ + app.add_option("-d,--data-dir", config.data_dir, "Data directory for LMDB stores")->required(); + app.add_option("--tree-heights", config.tree_heights_json, "Tree heights as JSON: {0:40,1:32,...}"); + app.add_option("--tree-prefill", config.tree_prefill_json, "Tree prefill sizes as JSON: {0:128,2:128,...}"); + app.add_option("--map-sizes", config.map_sizes_json, "LMDB map sizes in KB as JSON: {0:1024,...}"); + app.add_option("-t,--threads", config.threads, "Thread pool size (default: 16)")->check(CLI::PositiveNumber); + app.add_option("--initial-header-generator-point", + config.initial_header_generator_point, + "Header generator point (default: 0)"); + app.add_option( + "--prefilled-public-data", config.prefilled_public_data_json, "Prefilled public data as JSON array"); + app.add_option( + "--prefilled-nullifiers", config.prefilled_nullifiers_json, "Prefilled genesis nullifiers as JSON array"); + app.add_option("--genesis-timestamp", config.genesis_timestamp, "Genesis block timestamp (default: 0)"); +} + +static std::unique_ptr make_world_state(const WsdbConfig& config) +{ + const uint64_t DEFAULT_MAP_SIZE = 1024UL * 1024; + + auto tree_height = parse_tree_uint32_map(config.tree_heights_json); + auto tree_prefill = parse_tree_index_map(config.tree_prefill_json); + + std::unordered_map map_size{ + { world_state::MerkleTreeId::ARCHIVE, DEFAULT_MAP_SIZE }, + { world_state::MerkleTreeId::NULLIFIER_TREE, DEFAULT_MAP_SIZE }, + { world_state::MerkleTreeId::NOTE_HASH_TREE, DEFAULT_MAP_SIZE }, + { world_state::MerkleTreeId::PUBLIC_DATA_TREE, DEFAULT_MAP_SIZE }, + { world_state::MerkleTreeId::L1_TO_L2_MESSAGE_TREE, DEFAULT_MAP_SIZE }, + }; + for (const auto& [k, v] : parse_tree_uint64_map(config.map_sizes_json)) { + map_size[k] = v; + } + + std::vector prefilled_public_data; + if (!config.prefilled_public_data_json.empty()) { + prefilled_public_data = parse_prefilled_public_data(config.prefilled_public_data_json); + std::cerr << "Parsed " << prefilled_public_data.size() << " prefilled public data entries" << '\n'; + } + + // The caller (TS world-state) passes the same canonical genesis nullifiers it seeds via the napi path, so the IPC + // genesis nullifier-tree root matches. + std::vector prefilled_nullifiers; + if (!config.prefilled_nullifiers_json.empty()) { + prefilled_nullifiers = parse_prefilled_nullifiers(config.prefilled_nullifiers_json); + std::cerr << "Parsed " << prefilled_nullifiers.size() << " prefilled nullifiers" << '\n'; + } + + std::cerr << "Creating WorldState at " << config.data_dir << " with " << config.threads << " threads" << '\n'; + return std::make_unique(config.threads, + config.data_dir, + map_size, + tree_height, + tree_prefill, + prefilled_public_data, + prefilled_nullifiers, + config.initial_header_generator_point, + config.genesis_timestamp); +} + +WsdbService::WsdbService(const WsdbConfig& config, std::function has_pending_request) + : world_state_(make_world_state(config)) + // Sized from the caller-provided `threads` budget, NOT std::thread::hardware_concurrency(): the latter ignores + // cgroup CPU limits and reports the host core count (e.g. 192 in a 2-CPU CI container), which would spawn a huge + // pool per wsdb process and exhaust the per-UID thread limit (pthread_create EAGAIN). + , dispatch_pool_(std::make_unique(std::max(2, config.threads))) + , request_{ .world_state = *world_state_ } + // Server-side ordering: the database, not the client, guarantees per-fork read/write consistency. Each handler + // hands its work to the scheduler via schedule_read / schedule_write, which run reads concurrently and serialize + // writes per fork. + , scheduler_(std::make_shared(*dispatch_pool_, std::move(has_pending_request))) +{ + request_.scheduler = scheduler_.get(); + handler_ = make_wsdb_handler(request_); +} + +WsdbService::~WsdbService() +{ + // Join the pool before the world state its in-flight work uses goes away. + dispatch_pool_.reset(); +} + +} // namespace azteclabs::wsdb diff --git a/native-packages/wsdb/cpp/src/wsdb/wsdb_service.hpp b/native-packages/wsdb/cpp/src/wsdb/wsdb_service.hpp new file mode 100644 index 00000000000..d87c71cea1f --- /dev/null +++ b/native-packages/wsdb/cpp/src/wsdb/wsdb_service.hpp @@ -0,0 +1,76 @@ +#pragma once +/** + * @file wsdb_service.hpp + * @brief The wsdb service without a transport: the world state, the dispatch pool, the per-fork scheduler and the + * generated async handler over them. + * + * The IPC server feeds the handler from its reactor; the in-process FFI entry (wsdb_ffi_service.cpp) feeds it from the + * caller's thread. Both build it from the same WsdbConfig, which the `msgpack run` CLI and the FFI open call parse + * with the same options. + */ +#include "common/thread_pool.hpp" +#include "world_state/world_state.hpp" +#include "wsdb/generated/wsdb_dispatch.hpp" +#include "wsdb/wsdb_request.hpp" + +#include +#include +#include +#include + +namespace CLI { +class App; +} + +namespace azteclabs::wsdb { + +class WsdbScheduler; + +struct WsdbConfig { + std::string data_dir; + // Maps as "{treeId:value,...}". + std::string tree_heights_json; + std::string tree_prefill_json; + // LMDB map sizes in KB. + std::string map_sizes_json; + uint32_t threads = 16; + uint32_t initial_header_generator_point = 0; + // [["slot_hex","value_hex"],...] + std::string prefilled_public_data_json; + // ["nullifier_hex",...] + std::string prefilled_nullifiers_json; + uint64_t genesis_timestamp = 0; +}; + +/** Registers the WsdbConfig options on a CLI (sub)command. */ +void add_wsdb_config_options(CLI::App& app, WsdbConfig& config); + +class WsdbService { + public: + /** + * @param has_pending_request Whether another request is already waiting; reads run inline on the submitting + * thread only when this is false (see WsdbScheduler). + */ + WsdbService(const WsdbConfig& config, std::function has_pending_request); + ~WsdbService(); + + WsdbService(const WsdbService&) = delete; + WsdbService& operator=(const WsdbService&) = delete; + WsdbService(WsdbService&&) = delete; + WsdbService& operator=(WsdbService&&) = delete; + + /** Decodes one request, schedules its work and responds when done, possibly from a pool thread. Submissions + * must come from one thread at a time. */ + AsyncDispatchHandler& handler() { return handler_; } + + private: + std::unique_ptr world_state_; + // Distinct from WorldState's own pool: mutating handlers enqueue subtasks there and wait on them, so dispatching + // them onto the same blocking, non-work-stealing pool could deadlock. + std::unique_ptr dispatch_pool_; + WsdbRequest request_; + std::shared_ptr scheduler_; + AsyncDispatchHandler handler_; +}; + +} // namespace azteclabs::wsdb From b2ac2103abb9f522adf7cfc3648b913b0efe780f Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:14:43 +0000 Subject: [PATCH 16/20] chore(bb): clang-format poseidon2_capi.cpp Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- .../cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp index 33e960c5061..da8f95c6717 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp +++ b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp @@ -31,7 +31,10 @@ __attribute__((visibility("default"))) void bb_poseidon2_hash(const uint8_t* ele // Hash { separator, lhs, rhs } — matches Poseidon2HashPolicy::hash_pair_with_separator, // the domain-separated internal-node hash used by the Aztec merkle trees. -__attribute__((visibility("default"))) void bb_poseidon2_hash_pair_with_separator(uint64_t separator, const uint8_t* lhs, const uint8_t* rhs, uint8_t* out) +__attribute__((visibility("default"))) void bb_poseidon2_hash_pair_with_separator(uint64_t separator, + const uint8_t* lhs, + const uint8_t* rhs, + uint8_t* out) { std::vector inputs{ FF(separator), FF::serialize_from_buffer(lhs), FF::serialize_from_buffer(rhs) }; FF::serialize_to_buffer(Poseidon2Bn254::hash(inputs), out); From bfc25428287d1bfc33294fd123735a7988020127 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:46:15 +0000 Subject: [PATCH 17/20] fix(wsdb): stage the arch packages through the generated package's prepare_arch_packages script This tree's ipc-codegen no longer writes ts/scripts/prepare_arch_packages.sh; the package's prepare_arch_packages script runs ipc-runtime's instead, as barretenberg/ts's packages do. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- native-packages/wsdb/bootstrap.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/native-packages/wsdb/bootstrap.sh b/native-packages/wsdb/bootstrap.sh index 21288f8b293..67aa456d31f 100755 --- a/native-packages/wsdb/bootstrap.sh +++ b/native-packages/wsdb/bootstrap.sh @@ -57,7 +57,7 @@ function build { generate_ts_package npm_install_deps yarn build - (cd ts && ./scripts/prepare_arch_packages.sh "$(arch)-$(os)=build/$(arch)-$(os)/$WSDB_BINARY") + (cd ts && yarn prepare_arch_packages "$(arch)-$(os)=build/$(arch)-$(os)/$WSDB_BINARY") } # Emit test commands for the CI test engine. @@ -99,7 +99,7 @@ function release { generate_ts_package npm_install_deps yarn build - (cd ts && ./scripts/prepare_arch_packages.sh) + (cd ts && yarn prepare_arch_packages) for package_dir in ts/packages/*; do (cd "$package_dir" && retry "deploy_npm ${REF_NAME#v}") done From fdda20bd2ea842add218a4725b52b9bb8104034e Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Fri, 13 Nov 2026 23:34:39 +0000 Subject: [PATCH 18/20] fix(kvdb): cross-build the addon so the release publishes all four platform packages kvdb's release built only the release host's addon, so the linux-arm64 and darwin packages went out without a binary. It now builds lmdblib as a subdirectory (for the target's toolchain) and cross-compiles the addon with zig for arm64-linux, amd64-macos and arm64-macos, as wsdb does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- Makefile | 2 +- native-packages/kvdb/.gitignore | 1 + native-packages/kvdb/bootstrap.sh | 20 ++++- native-packages/kvdb/cpp/CMakeLists.txt | 45 ++++------- native-packages/kvdb/cpp/CMakePresets.json | 91 ++++++++++++++++++++++ 5 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 native-packages/kvdb/cpp/CMakePresets.json diff --git a/Makefile b/Makefile index 70b69775d1e..9c1f2bd5c37 100644 --- a/Makefile +++ b/Makefile @@ -427,7 +427,7 @@ ipc-runtime-cross: ipc-runtime ipc-runtime-cross-arm64-linux ipc-runtime-cross-a lmdblib: $(call build,$@,native-packages/lmdblib) -kvdb: lmdblib +kvdb: $(call build,$@,native-packages/kvdb) # wsdb links the working tree's barretenberg static library (see native-packages/wsdb/README.md). diff --git a/native-packages/kvdb/.gitignore b/native-packages/kvdb/.gitignore index a0e6495b6eb..c7dd2d6e11c 100644 --- a/native-packages/kvdb/.gitignore +++ b/native-packages/kvdb/.gitignore @@ -1,4 +1,5 @@ cpp/build/ +cpp/build-*/ ts/dest/ ts/build/ ts/packages/ diff --git a/native-packages/kvdb/bootstrap.sh b/native-packages/kvdb/bootstrap.sh index a5efdcce92a..2afe3350211 100755 --- a/native-packages/kvdb/bootstrap.sh +++ b/native-packages/kvdb/bootstrap.sh @@ -10,8 +10,8 @@ hash=$(hash_str \ $(../lmdblib/bootstrap.sh hash) \ $(cache_content_hash .rebuild_patterns)) -# Build the NAPI addon. kvdb is barretenberg-free: it links only the lmdblib -# sibling package's archives (+ lmdb, node-addon-api). lmdblib is assumed prebuilt. +# Build the NAPI addon. kvdb is barretenberg-free: it builds and links only the lmdblib +# sibling package (+ lmdb, node-addon-api). function build_native { local build_dir="cpp/build" CC=$(which clang) CXX=$(which clang++) cmake -S cpp -B "$build_dir" -G Ninja >/dev/null @@ -21,6 +21,15 @@ function build_native { cp "$build_dir/lib/$NAPI_BINARY" "$target_dir/$NAPI_BINARY" } +# Cross-compile the addon for another platform with zig (cpp/CMakePresets.json). +function build_cross { + local platform=$1 + echo_header "kvdb build_cross $platform" + (cd cpp && cmake --preset "$platform" >/dev/null && cmake --build --preset "$platform") + mkdir -p "ts/build/$platform" + cp "cpp/build-$platform/lib/$NAPI_BINARY" "ts/build/$platform/$NAPI_BINARY" +} + function build { echo_header "kvdb build" build_native @@ -46,11 +55,14 @@ function test { } function clean { - rm -rf ts/dest ts/build ts/packages ts/node_modules node_modules cpp/build + rm -rf ts/dest ts/build ts/packages ts/node_modules node_modules cpp/build cpp/build-* } function release { build_native + for platform in arm64-linux amd64-macos arm64-macos; do + build_cross $platform + done npm_install_deps yarn build (cd ts && ./scripts/prepare_arch_packages.sh) @@ -60,7 +72,7 @@ function release { (cd ts && retry "deploy_npm ${REF_NAME#v}") } -export -f build_native build test_cmds test clean release +export -f build_native build_cross build test_cmds test clean release case "$cmd" in "") diff --git a/native-packages/kvdb/cpp/CMakeLists.txt b/native-packages/kvdb/cpp/CMakeLists.txt index d3ceeda004c..755202d977c 100644 --- a/native-packages/kvdb/cpp/CMakeLists.txt +++ b/native-packages/kvdb/cpp/CMakeLists.txt @@ -1,8 +1,8 @@ # Standalone build for the kvdb Node NAPI addon (nodejs_module.node). # # This package owns the kv-store native backend (the LMDBStore NAPI wrapper) that -# barretenberg no longer hosts. It is barretenberg-free: it links only the lmdblib -# sibling package's archives + lmdb + the Node addon runtime. It uses msgpack-c +# barretenberg no longer hosts. It is barretenberg-free: it builds and links only the +# lmdblib sibling package + lmdb + the Node addon runtime. It uses msgpack-c # directly (no bb serialization framework) and vendors the small message header / # dispatcher it needs under src/messaging. @@ -14,35 +14,21 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -# Default to Release (-O3 -DNDEBUG) to match the prebuilt liblmdblib.a it links: the -# shared lmdblib templated/inline headers must be compiled the same way in both, or -# their inline codegen diverges (ODR). +# Default to Release (-O3 -DNDEBUG), as lmdblib does: the shared lmdblib templated/inline +# headers must be compiled the same way in both, or their inline codegen diverges (ODR). if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() add_definitions(-DNAPI_VERSION=9) -# --------------------------------------------------------------------------- -# lmdblib sibling package build outputs (the only native dependency). -# --------------------------------------------------------------------------- -get_filename_component(REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) -set(LMDBLIB_PKG "${REPO_ROOT}/native-packages/lmdblib") -set(LMDBLIB_SRC "${LMDBLIB_PKG}/cpp/src") -set(LMDBLIB_LIB "${LMDBLIB_PKG}/cpp/build/lib/liblmdblib.a") -set(LMDB_INCLUDE "${LMDBLIB_PKG}/cpp/build/_deps/lmdb-src/libraries/liblmdb") -set(LMDB_LIB "${LMDBLIB_PKG}/cpp/build/lib/liblmdb.a") -# msgpack-c headers (header-only) come transitively from lmdblib's build: lmdblib fetches -# msgpack-c and compiles liblmdblib.a against it, so the wire ABI is single-sourced rather -# than re-fetched here. (Matches how the wsdb package consumes msgpack.) -set(MSGPACK_INCLUDE "${LMDBLIB_PKG}/cpp/build/_deps/msgpack-src/include") - -foreach(artifact "${LMDBLIB_LIB}" "${LMDB_LIB}" "${MSGPACK_INCLUDE}") - if(NOT EXISTS "${artifact}") - message(FATAL_ERROR "Missing prebuilt artifact: ${artifact}\n" - "Build lmdblib first (cd native-packages/lmdblib && ./bootstrap.sh).") - endif() -endforeach() +# The lmdblib sibling package (the lmdb C++ wrapper, with the lmdb and msgpack-c sources it fetches), +# built here for this toolchain and target. +include(FetchContent) +get_filename_component(LMDBLIB "${CMAKE_CURRENT_SOURCE_DIR}/../../lmdblib/cpp" ABSOLUTE) +set(LMDBLIB_BUILD_TESTS OFF CACHE BOOL "" FORCE) +add_subdirectory(${LMDBLIB} ${CMAKE_BINARY_DIR}/_deps/lmdblib) +FetchContent_GetProperties(msgpack) # --------------------------------------------------------------------------- # Node addon-api / node-api headers (resolved from this package's node_modules). @@ -64,7 +50,7 @@ execute_process( string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR}) string(REGEX REPLACE "[\r\n\"]" "" NODE_API_HEADERS_DIR ${NODE_API_HEADERS_DIR}) -# libc++ and the x86_64 Skylake target match lmdblib's archive. Static +# libc++ and the x86_64 Skylake target match lmdblib's. Static # libc++/libgcc keep the addon self-contained (no libc++.so.1 runtime dependency). add_compile_definitions(MSGPACK_NO_BOOST MSGPACK_USE_STD_VARIANT_ADAPTOR NO_PAR_ALGOS) if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") @@ -77,9 +63,8 @@ add_link_options(-stdlib=libc++ -static-libstdc++ -static-libgcc) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/src - ${LMDBLIB_SRC} - ${MSGPACK_INCLUDE} - ${LMDB_INCLUDE} + ${LMDBLIB}/src + ${msgpack_SOURCE_DIR}/include ) set(KVDB_SOURCES @@ -91,7 +76,7 @@ add_library(nodejs_module SHARED ${KVDB_SOURCES}) set_target_properties(nodejs_module PROPERTIES PREFIX "" SUFFIX ".node") target_include_directories(nodejs_module PRIVATE ${NODE_API_HEADERS_DIR} ${NODE_ADDON_API_DIR}) -target_link_libraries(nodejs_module PRIVATE ${LMDBLIB_LIB} ${LMDB_LIB} pthread) +target_link_libraries(nodejs_module PRIVATE lmdblib lmdb pthread) # On macOS, Node.js N-API symbols are provided by the runtime, not at link time. if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") diff --git a/native-packages/kvdb/cpp/CMakePresets.json b/native-packages/kvdb/cpp/CMakePresets.json new file mode 100644 index 00000000000..03bfc94362b --- /dev/null +++ b/native-packages/kvdb/cpp/CMakePresets.json @@ -0,0 +1,91 @@ +{ + "version": 5, + "cmakeMinimumRequired": { + "major": 3, + "minor": 24, + "patch": 0 + }, + "configurePresets": [ + { + "name": "cross-base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build-${presetName}", + "environment": { + "CC": "zig cc", + "CXX": "zig c++", + "CFLAGS": "-g0", + "CXXFLAGS": "-g0" + }, + "cacheVariables": { + "CMAKE_AR": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ar.sh", + "CMAKE_RANLIB": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ranlib.sh", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "arm64-linux", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target aarch64-linux-gnu.2.35", + "CXX": "zig c++ -target aarch64-linux-gnu.2.35", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Linux" + } + }, + { + "name": "amd64-macos", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target x86_64-macos -mcpu=baseline", + "CXX": "zig c++ -target x86_64-macos -mcpu=baseline", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "x86_64" + } + }, + { + "name": "arm64-macos", + "inherits": "cross-base", + "environment": { + "CC": "zig cc -target aarch64-macos -mcpu=apple_a14", + "CXX": "zig c++ -target aarch64-macos -mcpu=apple_a14", + "LDFLAGS": "-s" + }, + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "Darwin", + "CMAKE_SYSTEM_PROCESSOR": "aarch64" + } + } + ], + "buildPresets": [ + { + "name": "arm64-linux", + "configurePreset": "arm64-linux", + "inheritConfigureEnvironment": true, + "targets": [ + "nodejs_module" + ] + }, + { + "name": "amd64-macos", + "configurePreset": "amd64-macos", + "inheritConfigureEnvironment": true, + "targets": [ + "nodejs_module" + ] + }, + { + "name": "arm64-macos", + "configurePreset": "arm64-macos", + "inheritConfigureEnvironment": true, + "targets": [ + "nodejs_module" + ] + } + ] +} From 06cdc23fceafa3e811d8a3715c5c456f2ed78415 Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Sat, 14 Nov 2026 02:09:08 +0000 Subject: [PATCH 19/20] refactor(bb): use upstream msgpack-c 7.0.0 instead of the fork; drop a stray circuit include The AztecProtocol/msgpack-c fork existed to build without exceptions, which barretenberg no longer does. barretenberg now fetches upstream cpp-7.0.0 with FetchContent, the release lmdblib, wsdb and kvdb already use, as a system include (its headers do not build cleanly under -Wconversion). The fork's other change, an explicit in cpp_config_decl.hpp, is needed by wasi-sdk's libc++, so ipc-codegen's msgpack_include.hpp includes it first. lmdblib declares msgpack-c the same way, so a build that includes both shares one. crypto/merkle_tree/indexed_leaf.hpp included stdlib/primitives/field/field.hpp without using it, which pulled the circuit builders, honk and eccvm headers into everything that includes it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- barretenberg/cpp/cmake/module.cmake | 12 --------- barretenberg/cpp/cmake/msgpack.cmake | 27 ++++++++----------- .../cpp/scripts/rename_private_symbols.sh | 27 ------------------- .../acir_components_check/CMakeLists.txt | 1 - .../crypto/merkle_tree/indexed_leaf.hpp | 1 - .../crypto/poseidon2/poseidon2_capi.cpp | 10 +++---- .../dsl/acir_format/acir_null_deref.test.cpp | 2 +- .../src/barretenberg/vm2_stub/CMakeLists.txt | 1 - .../cpp/ipc_codegen/msgpack_include.hpp | 3 +++ native-packages/lmdblib/cpp/CMakeLists.txt | 12 ++++----- 10 files changed, 24 insertions(+), 72 deletions(-) delete mode 100755 barretenberg/cpp/scripts/rename_private_symbols.sh diff --git a/barretenberg/cpp/cmake/module.cmake b/barretenberg/cpp/cmake/module.cmake index c558af81b00..0b4ac6fd017 100644 --- a/barretenberg/cpp/cmake/module.cmake +++ b/barretenberg/cpp/cmake/module.cmake @@ -100,10 +100,6 @@ function(barretenberg_module_with_sources MODULE_NAME) endif() endif() - # enable msgpack downloading via dependency (solves race condition) - add_dependencies(${MODULE_NAME} msgpack-c) - add_dependencies(${MODULE_NAME}_objects msgpack-c) - # enable lmdb downloading via dependency (solves race condition) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32" AND NOT BB_LITE) add_dependencies(${MODULE_NAME} lmdb_repo) @@ -193,10 +189,6 @@ function(barretenberg_module_with_sources MODULE_NAME) ${TBB_IMPORTED_TARGETS} ) - # enable msgpack downloading via dependency (solves race condition) - add_dependencies(${MODULE_NAME}_test_objects msgpack-c) - add_dependencies(${MODULE_NAME}_tests msgpack-c) - # enable lmdb downloading via dependency (solves race condition) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32" AND NOT BB_LITE) add_dependencies(${MODULE_NAME}_test_objects lmdb_repo) @@ -307,10 +299,6 @@ function(barretenberg_module_with_sources MODULE_NAME) ) endif() - # enable msgpack downloading via dependency (solves race condition) - add_dependencies(${BENCHMARK_NAME}_bench_objects msgpack-c) - add_dependencies(${BENCHMARK_NAME}_bench msgpack-c) - # enable lmdb downloading via dependency (solves race condition) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32" AND NOT BB_LITE) add_dependencies(${BENCHMARK_NAME}_bench_objects lmdb_repo) diff --git a/barretenberg/cpp/cmake/msgpack.cmake b/barretenberg/cpp/cmake/msgpack.cmake index db8f05ac6d3..38f217b51b7 100644 --- a/barretenberg/cpp/cmake/msgpack.cmake +++ b/barretenberg/cpp/cmake/msgpack.cmake @@ -1,18 +1,13 @@ -include(ExternalProject) +include(FetchContent) -# External project: Download msgpack-c from GitHub -set(MSGPACK_PREFIX "${CMAKE_BINARY_DIR}/_deps/msgpack-c") -set(MSGPACK_INCLUDE "${MSGPACK_PREFIX}/src/msgpack-c/include") - -ExternalProject_Add( - msgpack-c - PREFIX ${MSGPACK_PREFIX} - # We need to go through some hoops to do a shallow clone of a fixed commit (as opposed to a tag). - DOWNLOAD_COMMAND - sh -c "mkdir -p ${MSGPACK_PREFIX}/src/msgpack-c && cd ${MSGPACK_PREFIX}/src/msgpack-c && git init --quiet && (git remote add origin https://github.com/AztecProtocol/msgpack-c.git 2>/dev/null || true) && git fetch --depth 1 origin --quiet c0334576ed657fb3b3c49e8e61402989fb84146d && git reset --quiet --hard FETCH_HEAD" - SOURCE_DIR ${MSGPACK_PREFIX}/src/msgpack-c - CONFIGURE_COMMAND "" # No configure step - BUILD_COMMAND "" # No build step - INSTALL_COMMAND "" # No install step - UPDATE_COMMAND "" # No update step +# msgpack-c is used header-only. SOURCE_SUBDIR names a directory without a CMakeLists.txt, so +# FetchContent_MakeAvailable only fetches it and never configures msgpack-c's own targets. +FetchContent_Declare( + msgpack + GIT_REPOSITORY https://github.com/msgpack/msgpack-c.git + GIT_TAG cpp-7.0.0 + GIT_SHALLOW TRUE + SOURCE_SUBDIR include ) +FetchContent_MakeAvailable(msgpack) +set(MSGPACK_INCLUDE "${msgpack_SOURCE_DIR}/include") diff --git a/barretenberg/cpp/scripts/rename_private_symbols.sh b/barretenberg/cpp/scripts/rename_private_symbols.sh deleted file mode 100755 index a41cf5cea92..00000000000 --- a/barretenberg/cpp/scripts/rename_private_symbols.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -# Usage: rename_private_symbols.sh ... -# -# Archives the objects listed in (one per line) into with every symbol they -# define renamed under a private prefix, except the s and the replaceable operator new and -# delete. A consumer's own C++ (its msgpack-c, its libc++ instantiations) then cannot be merged with -# barretenberg's, while the archive keeps ordinary lazy member semantics. The Mach-O counterpart of -# the ELF relocatable link in src/CMakeLists.txt, which ld64.lld cannot do. -set -euo pipefail -nm=$1 objcopy=$2 ar=$3 out=$4 rsp=$5 -shift 5 -mapfile -t objects < "$rsp" -dir=$(mktemp -d "$out.XXXXXX") -trap 'rm -rf "$dir"' EXIT - -printf '%s\n' "$@" > "$dir/exports" -"$nm" --defined-only --extern-only --just-symbol-name "${objects[@]}" 2>/dev/null | - grep -v -e '^$' -e ':$' | sort -u | - grep -vxF -f "$dir/exports" | grep -vE '^__Z(nw|na|dl|da)' | - awk '{ print $1 " _wsdb_ref." $1 }' > "$dir/renames" - -i=0 -for object in "${objects[@]}"; do - "$objcopy" --redefine-syms="$dir/renames" "$object" "$dir/$((i++)).o" -done -rm -f "$out" -(cd "$dir" && "$ar" qc "$out" $(ls *.o | sort -n)) diff --git a/barretenberg/cpp/src/barretenberg/acir_components_check/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/acir_components_check/CMakeLists.txt index 0ad01500fa3..8f6ebdc3927 100644 --- a/barretenberg/cpp/src/barretenberg/acir_components_check/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/acir_components_check/CMakeLists.txt @@ -17,7 +17,6 @@ target_link_libraries( vm2_stub libdeflate::libdeflate_static ) -add_dependencies(acir_components_check_lib msgpack-c) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32" AND NOT BB_LITE) add_dependencies(acir_components_check_lib lmdb_repo) endif() diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_leaf.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_leaf.hpp index dc0096d070a..c8541f7dc42 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_leaf.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_leaf.hpp @@ -4,7 +4,6 @@ #include "barretenberg/crypto/merkle_tree/types.hpp" #include "barretenberg/ecc/curves/bn254/fr.hpp" #include "barretenberg/serialize/msgpack.hpp" -#include "barretenberg/stdlib/primitives/field/field.hpp" namespace bb::crypto::merkle_tree { diff --git a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp index da8f95c6717..56560c9253b 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp +++ b/barretenberg/cpp/src/barretenberg/crypto/poseidon2/poseidon2_capi.cpp @@ -9,8 +9,7 @@ // frequency but want no dependency on barretenberg's C++ headers, field type, // stdlib, or build flags (e.g. the standalone world-state DB service). Field // elements cross as 32 canonical bytes (barretenberg's field serialization); -// the montgomery form stays private to barretenberg. Default visibility, as an exported C ABI: the -// libwsdb-ref.a link keeps only exported symbols global (see src/CMakeLists.txt). +// the montgomery form stays private to barretenberg. namespace { using FF = bb::fr; using Poseidon2Bn254 = bb::crypto::Poseidon2; @@ -19,7 +18,7 @@ using Poseidon2Bn254 = bb::crypto::Poseidon2 inputs; inputs.reserve(n); @@ -31,10 +30,7 @@ __attribute__((visibility("default"))) void bb_poseidon2_hash(const uint8_t* ele // Hash { separator, lhs, rhs } — matches Poseidon2HashPolicy::hash_pair_with_separator, // the domain-separated internal-node hash used by the Aztec merkle trees. -__attribute__((visibility("default"))) void bb_poseidon2_hash_pair_with_separator(uint64_t separator, - const uint8_t* lhs, - const uint8_t* rhs, - uint8_t* out) +void bb_poseidon2_hash_pair_with_separator(uint64_t separator, const uint8_t* lhs, const uint8_t* rhs, uint8_t* out) { std::vector inputs{ FF(separator), FF::serialize_from_buffer(lhs), FF::serialize_from_buffer(rhs) }; FF::serialize_to_buffer(Poseidon2Bn254::hash(inputs), out); diff --git a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_null_deref.test.cpp b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_null_deref.test.cpp index f605b36ce70..c9360621eff 100644 --- a/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_null_deref.test.cpp +++ b/barretenberg/cpp/src/barretenberg/dsl/acir_format/acir_null_deref.test.cpp @@ -9,7 +9,7 @@ * * Attack vector: An attacker crafts raw ACIR bytecode (bypassing the Noir compiler) * containing a BlackBoxFuncCall opcode where a fixed-size array field is encoded as - * msgpack NIL (0xc0). Without the fix, the AztecProtocol/msgpack-c fork silently + * msgpack NIL (0xc0). Without the fix, msgpack-c's shared_ptr adaptor silently * converts NIL to a null shared_ptr, which is then dereferenced unconditionally. * With the fix, deserialization rejects NIL for required fields and throws. */ diff --git a/barretenberg/cpp/src/barretenberg/vm2_stub/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/vm2_stub/CMakeLists.txt index 571e621b445..6902222f4ab 100644 --- a/barretenberg/cpp/src/barretenberg/vm2_stub/CMakeLists.txt +++ b/barretenberg/cpp/src/barretenberg/vm2_stub/CMakeLists.txt @@ -5,5 +5,4 @@ file(GLOB_RECURSE SOURCE_FILES *.cpp) list(FILTER SOURCE_FILES EXCLUDE REGEX ".*\\.(fuzzer|test|bench)\\.cpp$") add_library(vm2_stub OBJECT ${SOURCE_FILES}) -add_dependencies(vm2_stub msgpack-c) target_link_libraries(vm2_stub PUBLIC barretenberg_headers) diff --git a/ipc-codegen/templates/cpp/ipc_codegen/msgpack_include.hpp b/ipc-codegen/templates/cpp/ipc_codegen/msgpack_include.hpp index 1ac19615fb5..2ccff505295 100644 --- a/ipc-codegen/templates/cpp/ipc_codegen/msgpack_include.hpp +++ b/ipc-codegen/templates/cpp/ipc_codegen/msgpack_include.hpp @@ -12,6 +12,9 @@ */ #include "throw.hpp" +// msgpack-c 7.0.0 uses std::remove_volatile and friends without including ; libc++ in +// wasi-sdk does not provide it transitively. +#include #ifdef BB_NO_EXCEPTIONS #define try if (true) diff --git a/native-packages/lmdblib/cpp/CMakeLists.txt b/native-packages/lmdblib/cpp/CMakeLists.txt index 61d611fcc86..f000c0b08d8 100644 --- a/native-packages/lmdblib/cpp/CMakeLists.txt +++ b/native-packages/lmdblib/cpp/CMakeLists.txt @@ -40,17 +40,17 @@ set(LMDB_DIR ${lmdb_SOURCE_DIR}/libraries/liblmdb) add_library(lmdb STATIC ${LMDB_DIR}/mdb.c ${LMDB_DIR}/midl.c) target_include_directories(lmdb PUBLIC ${LMDB_DIR}) -# msgpack-c: header-only. Fetch the source and use its include directory directly -# (we don't build msgpack-c's own CMake targets/tests). +# msgpack-c: header-only. SOURCE_SUBDIR names a directory without a CMakeLists.txt, so this only +# fetches it (never msgpack-c's own targets/tests). Declared as barretenberg's is, so a build that +# includes both shares one. FetchContent_Declare( msgpack GIT_REPOSITORY https://github.com/msgpack/msgpack-c.git GIT_TAG cpp-7.0.0 + GIT_SHALLOW TRUE + SOURCE_SUBDIR include ) -FetchContent_GetProperties(msgpack) -if(NOT msgpack_POPULATED) - FetchContent_Populate(msgpack) -endif() +FetchContent_MakeAvailable(msgpack) set(MSGPACK_INCLUDE ${msgpack_SOURCE_DIR}/include) # msgpack-c is used header-only with exceptions (no boost). Compiled with libc++ and From c01acdda9a4c46f8f6d46e9bd96dfd491a138d0f Mon Sep 17 00:00:00 2001 From: Charlie <5764343+charlielye@users.noreply.github.com> Date: Sat, 14 Nov 2026 02:09:08 +0000 Subject: [PATCH 20/20] refactor(wsdb): build wsdb-ref from source instead of linking libwsdb-ref.a wsdb-ref and the poseidon2 C bindings need ten barretenberg sources (the rest is header-only field arithmetic), so barretenberg/cpp/src/barretenberg/wsdb_ref/standalone is a small CMake project that compiles just those, and aztec-wsdb adds it with FetchContent for whatever it is building. With one msgpack-c across both, nothing needs hiding: the prelinked libwsdb-ref.a, its Mach-O symbol renaming and the poseidon2 visibility change are gone, and wsdb no longer waits on barretenberg's native or cross builds. wsdb sets its toolchain flags before adding its dependencies, so they are built the same way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015HXzMEGRnmFYs5yXRcAWCb --- Makefile | 4 +- barretenberg/cpp/CMakePresets.json | 8 +- barretenberg/cpp/bootstrap.sh | 8 +- barretenberg/cpp/src/CMakeLists.txt | 67 +---------------- .../wsdb_ref/standalone/CMakeLists.txt | 75 +++++++++++++++++++ native-packages/wsdb/README.md | 10 +-- native-packages/wsdb/bootstrap.sh | 9 +-- native-packages/wsdb/cpp/CMakeLists.txt | 46 ++++++------ native-packages/wsdb/cpp/CMakePresets.json | 3 +- 9 files changed, 119 insertions(+), 111 deletions(-) create mode 100644 barretenberg/cpp/src/barretenberg/wsdb_ref/standalone/CMakeLists.txt diff --git a/Makefile b/Makefile index 9c1f2bd5c37..c9ae79621ff 100644 --- a/Makefile +++ b/Makefile @@ -430,8 +430,8 @@ lmdblib: kvdb: $(call build,$@,native-packages/kvdb) -# wsdb links the working tree's barretenberg static library (see native-packages/wsdb/README.md). -wsdb: bb-cpp-native +# wsdb builds barretenberg's wsdb-ref sources itself (see native-packages/wsdb/README.md), so it needs no bb build. +wsdb: $(call build,$@,native-packages/wsdb) # Native-package tests. lmdblib and wsdb are self-contained gtest binaries. kvdb has no diff --git a/barretenberg/cpp/CMakePresets.json b/barretenberg/cpp/CMakePresets.json index 2bb7d773c9e..ef0cfde18ac 100644 --- a/barretenberg/cpp/CMakePresets.json +++ b/barretenberg/cpp/CMakePresets.json @@ -804,25 +804,25 @@ "name": "amd64-linux", "configurePreset": "amd64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi", "libwsdb-ref.a"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "arm64-linux", "configurePreset": "arm64-linux", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi", "libwsdb-ref.a"] + "targets": ["bb", "bb-avm", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "amd64-macos", "configurePreset": "amd64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi", "libwsdb-ref.a"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "arm64-macos", "configurePreset": "arm64-macos", "inheritConfigureEnvironment": true, - "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi", "libwsdb-ref.a"] + "targets": ["bb", "bb-external", "bb-avm-sim", "wsdb-ref", "wsdb_ref_napi"] }, { "name": "amd64-windows", diff --git a/barretenberg/cpp/bootstrap.sh b/barretenberg/cpp/bootstrap.sh index 6a5f578cab6..72cc79b8d1f 100755 --- a/barretenberg/cpp/bootstrap.sh +++ b/barretenberg/cpp/bootstrap.sh @@ -274,10 +274,10 @@ function build_release_dir { release_tar build-release/barretenberg-avm-arm64-linux.tar.gz build-arm64-linux/bin bb-avm # Package static libraries for FFI bindings (stripped at build time via CMake POST_BUILD). - tar -czf build-release/barretenberg-static-amd64-linux.tar.gz -C $native_build_dir/lib libbb-external.a libwsdb-ref.a - tar -czf build-release/barretenberg-static-arm64-linux.tar.gz -C build-arm64-linux/lib libbb-external.a libwsdb-ref.a - tar -czf build-release/barretenberg-static-amd64-darwin.tar.gz -C build-amd64-macos/lib libbb-external.a libwsdb-ref.a - tar -czf build-release/barretenberg-static-arm64-darwin.tar.gz -C build-arm64-macos/lib libbb-external.a libwsdb-ref.a + tar -czf build-release/barretenberg-static-amd64-linux.tar.gz -C $native_build_dir/lib libbb-external.a + tar -czf build-release/barretenberg-static-arm64-linux.tar.gz -C build-arm64-linux/lib libbb-external.a + tar -czf build-release/barretenberg-static-amd64-darwin.tar.gz -C build-amd64-macos/lib libbb-external.a + tar -czf build-release/barretenberg-static-arm64-darwin.tar.gz -C build-arm64-macos/lib libbb-external.a tar -czf build-release/barretenberg-static-amd64-windows.tar.gz -C build-amd64-windows/lib libbb-external.a tar -czf build-release/barretenberg-static-arm64-ios.tar.gz -C build-arm64-ios/lib libbb-external.a tar -czf build-release/barretenberg-static-arm64-ios-sim.tar.gz -C build-arm64-ios-sim/lib libbb-external.a diff --git a/barretenberg/cpp/src/CMakeLists.txt b/barretenberg/cpp/src/CMakeLists.txt index f2bb6857a5b..a0f7fdb9eb5 100644 --- a/barretenberg/cpp/src/CMakeLists.txt +++ b/barretenberg/cpp/src/CMakeLists.txt @@ -54,7 +54,8 @@ endif() # The build tree's src/ holds generated code (e.g. barretenberg/bbapi/generated/), included by the same # paths as sources. -include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${MSGPACK_INCLUDE} ${TRACY_INCLUDE} ${LMDB_INCLUDE} ${LIBDEFLATE_INCLUDE} ${HTTPLIB_INCLUDE} ${BACKWARD_INCLUDE} ${NLOHMANN_JSON_INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/../../../ipc-runtime/cpp) +include_directories(SYSTEM ${MSGPACK_INCLUDE}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${TRACY_INCLUDE} ${LMDB_INCLUDE} ${LIBDEFLATE_INCLUDE} ${HTTPLIB_INCLUDE} ${BACKWARD_INCLUDE} ${NLOHMANN_JSON_INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/../../../ipc-runtime/cpp) # Add avm-transpiler include path when library is provided if(AVM_TRANSPILER_LIB) @@ -256,70 +257,6 @@ if(NOT WASM) ) endif() -# libwsdb-ref.a: wsdb-ref behind its FFI entry, plus the poseidon2 C bindings, for aztec-wsdb (which -# links it instead of bb-external). aztec-wsdb is separately built C++ with its own msgpack-c, and a -# static link merges same-named inline and template code across both, so only those C symbols may -# keep their names; everything else, barretenberg's msgpack and libc++ instantiations included, must -# be private to this library. -# ELF: one relocatable object in which everything else is local. lld links the objects as lazy -# members, so only what the exports reach is kept, and --force-group-allocation dissolves the COMDAT -# groups, or the final link would discard this object's copies in favour of the consumer's. -# Mach-O: ld64.lld has no -r (and zig's crashes the final link), so rename_private_symbols.sh renames -# every other symbol the objects define under a private prefix instead. -if(NOT WASM AND BB_WITH_WSDB_REF AND BB_ARCHIVE_WORLD_STATE_REFERENCE AND CMAKE_SYSTEM_NAME MATCHES "Linux|Darwin") - find_program(BB_LLVM_OBJCOPY NAMES llvm-objcopy llvm-objcopy-20) - find_program(BB_LLVM_NM NAMES llvm-nm llvm-nm-20) - # zig's lld (the zig presets' own linker) when there is one. lld links every ELF target, so the - # cross presets need no per-architecture linker. - find_program(BB_ZIG zig) - find_program(BB_LD_LLD NAMES ld.lld ld.lld-20) - if(BB_ZIG) - set(WSDB_REF_LD ${BB_ZIG} ld.lld) - elseif(BB_LD_LLD) - set(WSDB_REF_LD ${BB_LD_LLD}) - endif() - set(WSDB_REF_EXPORTS - wsdb_ref_ipc_ffi_entry wsdb_ref_ipc_ffi_alloc wsdb_ref_ipc_ffi_free - bb_poseidon2_hash bb_poseidon2_hash_pair_with_separator) - set(WSDB_REF_OBJECTS ${BB_EXTERNAL_TARGET_OBJECTS} $ - $ $) - set(WSDB_REF_DIR ${CMAKE_CURRENT_BINARY_DIR}/wsdb_ref_library) - set(WSDB_REF_ARCHIVE ${CMAKE_BINARY_DIR}/lib/libwsdb-ref.a) - if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND WSDB_REF_LD AND BB_LLVM_OBJCOPY) - # Every object is a lazy archive member (--start-lib), so only what the exports reach is linked. - file(GENERATE OUTPUT ${WSDB_REF_DIR}/objects.rsp - CONTENT "--start-lib\n$\n--end-lib\n") - list(TRANSFORM WSDB_REF_EXPORTS PREPEND "-u" OUTPUT_VARIABLE WSDB_REF_UNDEFINED) - list(TRANSFORM WSDB_REF_EXPORTS PREPEND "--keep-global-symbol=" OUTPUT_VARIABLE WSDB_REF_KEEP) - set(WSDB_REF_COMMANDS - COMMAND ${WSDB_REF_LD} -r --force-group-allocation ${WSDB_REF_UNDEFINED} - -o ${WSDB_REF_DIR}/wsdb_ref.o @${WSDB_REF_DIR}/objects.rsp - COMMAND ${BB_LLVM_OBJCOPY} ${WSDB_REF_KEEP} ${WSDB_REF_DIR}/wsdb_ref.o - COMMAND ${CMAKE_COMMAND} -E rm -f ${WSDB_REF_ARCHIVE} - COMMAND ${CMAKE_AR} qc ${WSDB_REF_ARCHIVE} ${WSDB_REF_DIR}/wsdb_ref.o) - elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND BB_LLVM_OBJCOPY AND BB_LLVM_NM) - file(GENERATE OUTPUT ${WSDB_REF_DIR}/objects.rsp CONTENT "$\n") - list(TRANSFORM WSDB_REF_EXPORTS PREPEND "_") - set(WSDB_REF_COMMANDS - COMMAND ${CMAKE_SOURCE_DIR}/scripts/rename_private_symbols.sh ${BB_LLVM_NM} ${BB_LLVM_OBJCOPY} - ${CMAKE_AR} ${WSDB_REF_ARCHIVE} ${WSDB_REF_DIR}/objects.rsp ${WSDB_REF_EXPORTS}) - endif() - if(NOT WSDB_REF_COMMANDS) - message(WARNING "Not building libwsdb-ref.a, which aztec-wsdb links: it needs llvm-objcopy, and ld.lld (or zig) on Linux or llvm-nm on macOS") - else() - add_custom_command( - OUTPUT ${WSDB_REF_ARCHIVE} - ${WSDB_REF_COMMANDS} - COMMAND ${CMAKE_RANLIB} ${WSDB_REF_ARCHIVE} - DEPENDS ${WSDB_REF_OBJECTS} ${WSDB_REF_DIR}/objects.rsp ${CMAKE_SOURCE_DIR}/scripts/rename_private_symbols.sh - COMMENT "Archiving libwsdb-ref.a with only its C entry points public" - VERBATIM - ) - # Named after its file so a preset's target list caches it (see preset_cache_paths in bootstrap.sh). - add_custom_target(libwsdb-ref.a ALL DEPENDS ${WSDB_REF_ARCHIVE}) - endif() -endif() - if(WASM) # A standard WASI reactor: wasi-sdk's reactor startup provides _initialize, and the module imports # nothing but WASI. diff --git a/barretenberg/cpp/src/barretenberg/wsdb_ref/standalone/CMakeLists.txt b/barretenberg/cpp/src/barretenberg/wsdb_ref/standalone/CMakeLists.txt new file mode 100644 index 00000000000..39423474722 --- /dev/null +++ b/barretenberg/cpp/src/barretenberg/wsdb_ref/standalone/CMakeLists.txt @@ -0,0 +1,75 @@ +cmake_minimum_required(VERSION 3.24) +# wsdb-ref (the reference world state behind its FFI entry, wsdb_ref_ipc_ffi_entry) and barretenberg's +# poseidon2 C bindings, as a static library a downstream project builds from source with its own +# toolchain: add it with FetchContent (or add_subdirectory) and link `wsdb_ref`. It compiles only the +# barretenberg sources those need, and none of barretenberg's own CMake. +# +# Its interface is C: wsdb_ref_ipc_ffi_entry/_alloc/_free and bb_poseidon2_hash{,_pair_with_separator}. +# The C++ behind it is linked with the consumer's, so both must use the same msgpack-c: this declares +# it under FetchContent's name `msgpack`, and the first declaration in a build wins. +project(wsdb_ref CXX) + +get_filename_component(bb_src "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) +get_filename_component(repo "${bb_src}/../../.." ABSOLUTE) +set(gen_root "${CMAKE_CURRENT_BINARY_DIR}/gen") +set(gen_dir "${gen_root}/barretenberg/wsdb_ref/generated") + +include(FetchContent) +# Header-only. SOURCE_SUBDIR names a directory without a CMakeLists.txt, so this only fetches. +FetchContent_Declare( + msgpack + GIT_REPOSITORY https://github.com/msgpack/msgpack-c.git + GIT_TAG cpp-7.0.0 + GIT_SHALLOW TRUE + SOURCE_SUBDIR include +) +FetchContent_MakeAvailable(msgpack) +# Only Tracy's headers: barretenberg's profiling macros expand to nothing without TRACY_ENABLE. +FetchContent_Declare( + tracy + URL https://github.com/wolfpld/tracy/archive/5d542dc09f3d9378d005092a4ad446bd405f819a.tar.gz + SOURCE_SUBDIR public/tracy +) +FetchContent_MakeAvailable(tracy) + +# The protocol constants header, regenerated as barretenberg's own configure does (atomically, so the +# two may run at once). +execute_process(COMMAND "${bb_src}/../scripts/remake-constants.sh" COMMAND_ERROR_IS_FATAL ANY) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${repo}/noir-projects/fnd/noir-protocol-circuits/crates/types/src/constants.nr") + +set(schema "${bb_src}/barretenberg/wsdb_ref/wsdb_schema.jsonc") +file(GLOB_RECURSE ipc_codegen_inputs CONFIGURE_DEPENDS ${repo}/ipc-codegen/src/*.ts + ${repo}/ipc-codegen/templates/cpp/*) +add_custom_command( + OUTPUT ${gen_dir}/wsdb_ffi.cpp ${gen_dir}/wsdb_ffi.hpp ${gen_dir}/wsdb_dispatch.hpp + ${gen_dir}/wsdb_ipc_server.hpp ${gen_dir}/wsdb_types.hpp + COMMAND node --experimental-strip-types --no-warnings ${repo}/ipc-codegen/src/generate.ts + --schema ${schema} --lang cpp --out ${gen_dir} --server --ffi --ffi-name wsdb_ref + --cpp-namespace bb::wsdb_ref --cpp-include-dir barretenberg/wsdb_ref/generated + DEPENDS ${schema} ${ipc_codegen_inputs} + COMMENT "Generating the wsdb-ref dispatch and FFI entry from wsdb_schema.jsonc" + VERBATIM) + +add_library(wsdb_ref STATIC + ${bb_src}/barretenberg/common/assert.cpp + ${bb_src}/barretenberg/common/log.cpp + ${bb_src}/barretenberg/common/logstr.cpp + ${bb_src}/barretenberg/common/throw_or_abort.cpp + ${bb_src}/barretenberg/crypto/poseidon2/poseidon2.cpp + ${bb_src}/barretenberg/crypto/poseidon2/poseidon2_capi.cpp + ${bb_src}/barretenberg/world_state_reference/merkle_tree_id.cpp + ${bb_src}/barretenberg/world_state_reference/reference_world_state.cpp + ${bb_src}/barretenberg/wsdb_ref/wsdb_ref_handlers.cpp + ${gen_dir}/wsdb_ffi.cpp) +target_compile_features(wsdb_ref PRIVATE cxx_std_20) +set_target_properties(wsdb_ref PROPERTIES POSITION_INDEPENDENT_CODE ON) +# All private: a consumer declares the C entry points itself and compiles no barretenberg header. +target_include_directories(wsdb_ref PRIVATE ${bb_src} ${gen_root} ${repo}/ipc-runtime/cpp) +target_include_directories(wsdb_ref SYSTEM PRIVATE ${msgpack_SOURCE_DIR}/include ${tracy_SOURCE_DIR}/public) +# What barretenberg's build defines for these sources. +target_compile_definitions(wsdb_ref PRIVATE MSGPACK_NO_BOOST MSGPACK_USE_STD_VARIANT_ADAPTOR NO_PAR_ALGOS) +target_compile_options(wsdb_ref PRIVATE -fconstexpr-steps=100000000 -fbracket-depth=1024) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") + target_compile_options(wsdb_ref PRIVATE -march=skylake) +endif() diff --git a/native-packages/wsdb/README.md b/native-packages/wsdb/README.md index 85f37935334..904fdab2fa2 100644 --- a/native-packages/wsdb/README.md +++ b/native-packages/wsdb/README.md @@ -6,13 +6,13 @@ client package (`@aztec-foundation/wsdb`) that launches the server binary. ## Dependencies -wsdb compiles no barretenberg header. While it lives in aztec-packages it builds against this +wsdb's own code includes no barretenberg header. While it lives in aztec-packages it builds against this checkout; only the protocol constants come from the release pinned by tag in `foundation.pin` (`cpp/CMakeLists.txt`): | Dependency | Source | |---|---| -| barretenberg's `libwsdb-ref.a`: `bb_poseidon2_hash*`, and wsdb-ref behind its FFI entry (`wsdb_ref_ipc_ffi_entry`) for the differential test. Only those C symbols are global in it, so barretenberg's C++ cannot collide with this package's | the working tree's barretenberg build, `barretenberg/cpp/build/lib/libwsdb-ref.a` (`build-` for a cross build) | +| barretenberg's wsdb-ref library: `bb_poseidon2_hash*`, and wsdb-ref behind its FFI entry (`wsdb_ref_ipc_ffi_entry`) for the differential test. wsdb calls only those C functions | built from source in this build, from the working tree's `barretenberg/cpp/src/barretenberg/wsdb_ref/standalone` | | ipc-runtime (C++, compiled in) and ipc-codegen (the generated dispatch, FFI entry and client) | the working tree | | the wire contract (`wsdb_schema.jsonc`) | the working tree, `barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc` | | protocol constants (`aztec_constants.hpp`) | `npx @aztec-foundation/constants-codegen@`, which embeds its release's `constants.nr` | @@ -25,12 +25,12 @@ aztec-wsdb serves the schema over IPC (the `aztec-wsdb` binary) and in-process t test, `wsdb_ref_diff.test.cpp`, drives aztec-wsdb and wsdb-ref through their FFI entries with the generated typed client and requires both to answer every request alike. -The lmdblib sibling package (`../lmdblib`) is built from source as part of this build. The +The lmdblib sibling package (`../lmdblib`) is built from source as part of this build too. Both it +and wsdb-ref declare the same msgpack-c, so the build has one. The TypeScript client is generated by `bootstrap.sh` from the same ipc-codegen. The release publishes aztec-wsdb for linux-x64, linux-arm64, darwin-x64 and darwin-arm64. The host binary is `build_native`; the other three are `build_cross `, zig cross builds from -`cpp/CMakePresets.json` that link barretenberg's cross build of `libwsdb-ref.a` for that platform -(`barretenberg/cpp/build-/lib/`). +`cpp/CMakePresets.json`. `foundation.pin` is the tag and nothing else; edit it to move the pin. diff --git a/native-packages/wsdb/bootstrap.sh b/native-packages/wsdb/bootstrap.sh index 67aa456d31f..f76fd45c858 100755 --- a/native-packages/wsdb/bootstrap.sh +++ b/native-packages/wsdb/bootstrap.sh @@ -10,8 +10,8 @@ WSDB_BINARY=aztec-wsdb # from once native-packages leaves aztec-packages. WSDB_SCHEMA="$ROOT/barretenberg/cpp/src/barretenberg/wsdb_ref/wsdb_schema.jsonc" -# While wsdb lives in aztec-packages it builds against this checkout's barretenberg (its static -# library), ipc-runtime and ipc-codegen (see cpp/CMakeLists.txt), so the build follows their hashes. +# While wsdb lives in aztec-packages it builds against this checkout's barretenberg (the wsdb-ref +# sources), ipc-runtime and ipc-codegen (see cpp/CMakeLists.txt), so the build follows their hashes. hash=$(hash_str $(../lmdblib/bootstrap.sh hash) $(../../barretenberg/cpp/bootstrap.sh hash) \ $(../../ipc-codegen/bootstrap.sh hash) $(cache_content_hash .rebuild_patterns)) @@ -31,7 +31,7 @@ function generate_ts_package { --package-ipc-path-args 'msgpack,run,--input,{path}' } -# Build the standalone aztec-wsdb binary against this checkout's barretenberg build. +# Build the standalone aztec-wsdb binary. function build_native { local build_dir="cpp/build" CC=$(which clang) CXX=$(which clang++) cmake -S cpp -B "$build_dir" -G Ninja >/dev/null @@ -41,8 +41,7 @@ function build_native { cp "$build_dir/bin/$WSDB_BINARY" "$target_dir/$WSDB_BINARY" } -# Cross-compile aztec-wsdb for a non-host release platform with zig (cpp/CMakePresets.json), against -# barretenberg's build of libwsdb-ref.a for that platform. +# Cross-compile aztec-wsdb for a non-host release platform with zig (cpp/CMakePresets.json). function build_cross { local platform=$1 echo_header "wsdb build_cross $platform" diff --git a/native-packages/wsdb/cpp/CMakeLists.txt b/native-packages/wsdb/cpp/CMakeLists.txt index bfc76474e6f..2f0f87dad24 100644 --- a/native-packages/wsdb/cpp/CMakeLists.txt +++ b/native-packages/wsdb/cpp/CMakeLists.txt @@ -1,11 +1,11 @@ cmake_minimum_required(VERSION 3.24) -# aztec-wsdb: the world-state DB service. Compiles no barretenberg header. What it needs from outside -# this directory (barretenberg's libwsdb-ref.a, ipc-runtime, ipc-codegen and the schema) comes from -# this aztec-packages checkout; only the protocol constants come from the release pinned by tag in -# ../foundation.pin. +# aztec-wsdb: the world-state DB service. Its own code includes no barretenberg header. What it needs +# from outside this directory (barretenberg's wsdb-ref library, ipc-runtime, ipc-codegen and the +# schema) comes from this aztec-packages checkout; only the protocol constants come from the release +# pinned by tag in ../foundation.pin. # NOTE: when native-packages moves out of aztec-packages, take all of it from the pinned release -# again: libwsdb-ref.a from the release's barretenberg-static--.tar.gz, and ipc-runtime/cpp, -# ipc-codegen and the schema from a sparse clone of aztec-packages at the tag. +# again: FetchContent wsdb-ref's standalone project, ipc-runtime/cpp, ipc-codegen and the schema from a +# sparse clone of aztec-packages at the tag. project(aztec_wsdb CXX C) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -17,21 +17,27 @@ endif() set(pkg "${CMAKE_CURRENT_SOURCE_DIR}/..") set(deps "${CMAKE_BINARY_DIR}/_deps") +# --- Toolchain ---------------------------------------------------------------------------------- +# Set before the dependencies so they are compiled the same way: libc++, and barretenberg's +# -march=skylake on x86_64. +add_compile_definitions(MSGPACK_NO_BOOST MSGPACK_USE_STD_VARIANT_ADAPTOR NO_PAR_ALGOS) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") + add_compile_options(-march=skylake) +endif() +add_compile_options(-fconstexpr-steps=100000000 -fbracket-depth=1024 $<$:-stdlib=libc++> + -Wno-deprecated-declarations -Wno-missing-field-initializers) +add_link_options(-stdlib=libc++ -static-libstdc++ -static-libgcc) + # --- Dependencies ------------------------------------------------------------------------------- include(FetchContent) -# barretenberg's libwsdb-ref.a: the poseidon2 C bindings, and wsdb-ref (the reference world state) -# behind its FFI entry, which the differential test drives. It exports only those C symbols, so -# barretenberg's C++ (its own msgpack-c among it) cannot collide with this package's. bb-cpp-native -# builds it. file(READ "${pkg}/foundation.pin" tag) string(STRIP "${tag}" tag) string(REGEX REPLACE "^v" "" version "${tag}") set(foundation "${pkg}/../..") -set(bb_archive "${foundation}/barretenberg/cpp/build/lib/libwsdb-ref.a" CACHE FILEPATH - "barretenberg's libwsdb-ref.a") -if(NOT EXISTS "${bb_archive}") - message(FATAL_ERROR "No libwsdb-ref.a at ${bb_archive}: build barretenberg/cpp first") -endif() +# barretenberg's wsdb-ref library, built from source here: the poseidon2 C bindings, and wsdb-ref (the +# reference world state) behind its FFI entry, which the differential test drives. +FetchContent_Declare(wsdb_ref SOURCE_DIR ${foundation}/barretenberg/cpp/src/barretenberg/wsdb_ref/standalone) +FetchContent_MakeAvailable(wsdb_ref) option(WSDB_BUILD_TESTS "Build wsdb_tests and wsdb_bench" ON) # The lmdblib sibling package (the lmdb C++ wrapper, with the lmdb and msgpack-c sources it fetches), # built here for this toolchain and target. @@ -40,14 +46,6 @@ set(LMDBLIB_BUILD_TESTS OFF CACHE BOOL "" FORCE) add_subdirectory(${lmdblib} ${deps}/lmdblib) FetchContent_GetProperties(msgpack) # --- Build -------------------------------------------------------------------------------------- -# Match barretenberg's archive build (-march=skylake on x86_64, libc++, Release). -add_compile_definitions(MSGPACK_NO_BOOST MSGPACK_USE_STD_VARIANT_ADAPTOR NO_PAR_ALGOS) -if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") - add_compile_options(-march=skylake) -endif() -add_compile_options(-fconstexpr-steps=100000000 -fbracket-depth=1024 -stdlib=libc++ - -Wno-deprecated-declarations -Wno-missing-field-initializers) -add_link_options(-stdlib=libc++ -static-libstdc++ -static-libgcc) include_directories(src ${lmdblib}/src ${msgpack_SOURCE_DIR}/include) set(IPC_RUNTIME_BUILD_TESTS OFF CACHE BOOL "" FORCE) @@ -87,7 +85,7 @@ file(GLOB_RECURSE sources CONFIGURE_DEPENDS src/*.cpp) file(GLOB_RECURSE tests CONFIGURE_DEPENDS src/*.test.cpp) file(GLOB_RECURSE benches CONFIGURE_DEPENDS src/*.bench.cpp) list(REMOVE_ITEM sources ${tests} ${benches}) -set(libs ${bb_archive} lmdblib ipc_runtime lmdb pthread) +set(libs wsdb_ref lmdblib ipc_runtime lmdb pthread) add_executable(aztec-wsdb ${sources}) target_link_libraries(aztec-wsdb PRIVATE ${libs} CLI11::CLI11) diff --git a/native-packages/wsdb/cpp/CMakePresets.json b/native-packages/wsdb/cpp/CMakePresets.json index cc3e58abe09..0e6f6b2df1d 100644 --- a/native-packages/wsdb/cpp/CMakePresets.json +++ b/native-packages/wsdb/cpp/CMakePresets.json @@ -21,8 +21,7 @@ "CMAKE_AR": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ar.sh", "CMAKE_RANLIB": "${sourceDir}/../../../barretenberg/cpp/scripts/zig-ranlib.sh", "CMAKE_BUILD_TYPE": "Release", - "WSDB_BUILD_TESTS": "OFF", - "bb_archive": "${sourceDir}/../../../barretenberg/cpp/build-${presetName}/lib/libwsdb-ref.a" + "WSDB_BUILD_TESTS": "OFF" } }, {