@typeberry/lib 0.8.4-7cde74e → 0.8.4-7d1719f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/lib",
3
- "version": "0.8.4-7cde74e",
3
+ "version": "0.8.4-7d1719f",
4
4
  "description": "Typeberry Library",
5
5
  "main": "./bin/lib/index.js",
6
6
  "types": "./bin/lib/index.d.ts",
@@ -216,6 +216,8 @@
216
216
  "#@typeberry/config-node/*": "./packages/jam/config-node/*",
217
217
  "#@typeberry/database": "./packages/jam/database/index.js",
218
218
  "#@typeberry/database/*": "./packages/jam/database/*",
219
+ "#@typeberry/database-fjall": "./packages/jam/database-fjall/index.js",
220
+ "#@typeberry/database-fjall/*": "./packages/jam/database-fjall/*",
219
221
  "#@typeberry/database-lmdb": "./packages/jam/database-lmdb/index.js",
220
222
  "#@typeberry/database-lmdb/*": "./packages/jam/database-lmdb/*",
221
223
  "#@typeberry/executor": "./packages/jam/executor/index.js",
@@ -0,0 +1,45 @@
1
+ import type { HeaderHash, StateRootHash } from "#@typeberry/block";
2
+ import type { ChainSpec } from "#@typeberry/config";
3
+ import { type InitStatesDb, LeafDb, type StatesDb, StateUpdateError } from "#@typeberry/database";
4
+ import type { Blake2b } from "#@typeberry/hash";
5
+ import type { ServicesUpdate, State } from "#@typeberry/state";
6
+ import { SerializedState, type StateEntries } from "#@typeberry/state-merkleization";
7
+ import { OK, Result } from "#@typeberry/utils";
8
+ /**
9
+ * Hybrid serialized-states db (fjall variant).
10
+ *
11
+ * States (leafs) are kept in-memory, but large values are persisted to fjall.
12
+ * Reads go straight to fjall, which keeps its own (bounded) block cache.
13
+ * Designed for long fuzzing, used with pruning to keep heap usage bounded.
14
+ *
15
+ * Behaviourally identical to the LMDB hybrid db; differences are mechanical:
16
+ * construction is async, and value writes are ordered + flushed explicitly
17
+ * because fjall has no transaction primitive.
18
+ */
19
+ export declare class HybridSerializedStates implements StatesDb<SerializedState<LeafDb>>, InitStatesDb<StateEntries> {
20
+ private readonly spec;
21
+ private readonly blake2b;
22
+ private readonly root;
23
+ private readonly values;
24
+ static new({ spec, blake2b, dbPath, ephemeral, }: {
25
+ spec: ChainSpec;
26
+ blake2b: Blake2b;
27
+ dbPath: string;
28
+ ephemeral?: boolean;
29
+ }): Promise<HybridSerializedStates>;
30
+ private readonly inMemStates;
31
+ private readonly valuesDb;
32
+ private constructor();
33
+ insertInitialState(headerHash: HeaderHash, entries: StateEntries): Promise<Result<OK, StateUpdateError>>;
34
+ updateAndSetState(header: HeaderHash, state: SerializedState<LeafDb>, update: Partial<State & ServicesUpdate>): Promise<Result<OK, StateUpdateError>>;
35
+ getStateRoot(state: SerializedState<LeafDb>): Promise<StateRootHash>;
36
+ getState(header: HeaderHash): SerializedState<LeafDb> | null;
37
+ markUnused(header: HeaderHash): void;
38
+ diskSizeInBytes(): number | null;
39
+ close(): Promise<void>;
40
+ /** Write new large values to fjall, then flush. */
41
+ private writeValues;
42
+ /** Read a value from fjall. */
43
+ private readValue;
44
+ }
45
+ //# sourceMappingURL=hybrid-states.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hybrid-states.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-fjall/hybrid-states.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGlE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,KAAK,YAAY,EACjB,MAAM,EACN,KAAK,QAAQ,EACb,gBAAgB,EAGjB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EACL,eAAe,EACf,KAAK,YAAY,EAGlB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAK9C;;;;;;;;;;GAUG;AACH,qBAAa,sBAAuB,YAAW,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC;IAsBxG,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;WAxBZ,GAAG,CAAC,EACf,IAAI,EACJ,OAAO,EACP,MAAM,EACN,SAAS,GACV,EAAE;QACD,IAAI,EAAE,SAAS,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAMnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyE;IAErG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IAEpC,OAAO;IASD,kBAAkB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,gBAAgB,CAAC,CAAC;IAcxG,iBAAiB,CACrB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,eAAe,CAAC,MAAM,CAAC,EAC9B,MAAM,EAAE,OAAO,CAAC,KAAK,GAAG,cAAc,CAAC,GACtC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,gBAAgB,CAAC,CAAC;IAkBlC,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IAI1E,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,IAAI;IAS5D,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAMpC,eAAe,IAAI,MAAM,GAAG,IAAI;IAI1B,KAAK;IAIX,mDAAmD;YACrC,WAAW;IAgBzB,+BAA+B;IAC/B,OAAO,CAAC,SAAS;CAOlB"}
@@ -0,0 +1,113 @@
1
+ import { HashDictionary, SortedSet } from "#@typeberry/collections";
2
+ import { LeafDb, StateUpdateError, updateLeafs, } from "#@typeberry/database";
3
+ import { Logger } from "#@typeberry/logger";
4
+ import { SerializedState, StateEntryUpdateAction, serializeStateUpdate, } from "#@typeberry/state-merkleization";
5
+ import { leafComparator } from "#@typeberry/trie";
6
+ import { OK, Result } from "#@typeberry/utils";
7
+ import { FjallRoot, toUint8Array } from "./root.js";
8
+ const logger = Logger.new(import.meta.filename, "db");
9
+ /**
10
+ * Hybrid serialized-states db (fjall variant).
11
+ *
12
+ * States (leafs) are kept in-memory, but large values are persisted to fjall.
13
+ * Reads go straight to fjall, which keeps its own (bounded) block cache.
14
+ * Designed for long fuzzing, used with pruning to keep heap usage bounded.
15
+ *
16
+ * Behaviourally identical to the LMDB hybrid db; differences are mechanical:
17
+ * construction is async, and value writes are ordered + flushed explicitly
18
+ * because fjall has no transaction primitive.
19
+ */
20
+ export class HybridSerializedStates {
21
+ spec;
22
+ blake2b;
23
+ root;
24
+ values;
25
+ static async new({ spec, blake2b, dbPath, ephemeral, }) {
26
+ const root = await FjallRoot.open(dbPath, { ephemeral });
27
+ const values = await root.partition("values");
28
+ return new HybridSerializedStates(spec, blake2b, root, values);
29
+ }
30
+ inMemStates = HashDictionary.new();
31
+ // A single shared values accessor reused by every `LeafDb` we hand out.
32
+ valuesDb;
33
+ constructor(spec, blake2b, root, values) {
34
+ this.spec = spec;
35
+ this.blake2b = blake2b;
36
+ this.root = root;
37
+ this.values = values;
38
+ this.valuesDb = { get: (key) => this.readValue(key) };
39
+ }
40
+ async insertInitialState(headerHash, entries) {
41
+ const { values, leafs } = updateLeafs(SortedSet.fromArray(leafComparator, []), this.blake2b, Array.from(entries, (x) => [StateEntryUpdateAction.Insert, x[0], x[1]]));
42
+ const res = await this.writeValues(values);
43
+ if (res.isError) {
44
+ return res;
45
+ }
46
+ this.inMemStates.set(headerHash, leafs);
47
+ return Result.ok(OK);
48
+ }
49
+ async updateAndSetState(header, state, update) {
50
+ const updatedValues = serializeStateUpdate(this.spec, this.blake2b, update);
51
+ // Clone the leaf set before mutating: the previous state keeps using its own.
52
+ const newLeafs = SortedSet.fromSortedArray(leafComparator, state.backend.leafs.array);
53
+ const { values, leafs } = updateLeafs(newLeafs, this.blake2b, updatedValues);
54
+ const res = await this.writeValues(values);
55
+ if (res.isError) {
56
+ // Leave the caller's state untouched: its new leaves would reference
57
+ // values that never reached disk.
58
+ return res;
59
+ }
60
+ // Re-create the lookup with the shared values accessor only once the new
61
+ // values are durably written.
62
+ state.updateBackend(LeafDb.fromLeaves(leafs, this.valuesDb));
63
+ this.inMemStates.set(header, leafs);
64
+ return Result.ok(OK);
65
+ }
66
+ async getStateRoot(state) {
67
+ return state.backend.getStateRoot(this.blake2b);
68
+ }
69
+ getState(header) {
70
+ const leafs = this.inMemStates.get(header);
71
+ if (leafs === undefined) {
72
+ return null;
73
+ }
74
+ const leafDb = LeafDb.fromLeaves(leafs, this.valuesDb);
75
+ return SerializedState.new(this.spec, this.blake2b, leafDb);
76
+ }
77
+ markUnused(header) {
78
+ // We only remove the state from memory - values are not pruned at all,
79
+ // but since they are stored on disk we should be safe.
80
+ this.inMemStates.delete(header);
81
+ }
82
+ diskSizeInBytes() {
83
+ return this.root.sizeInBytes();
84
+ }
85
+ async close() {
86
+ await this.root.close();
87
+ }
88
+ /** Write new large values to fjall, then flush. */
89
+ async writeValues(values) {
90
+ if (values.length === 0) {
91
+ return Result.ok(OK);
92
+ }
93
+ try {
94
+ for (const [hash, val] of values) {
95
+ await this.values.insert(hash.raw, val.raw);
96
+ }
97
+ await this.root.persist();
98
+ }
99
+ catch (e) {
100
+ logger.error `${e}`;
101
+ return Result.error(StateUpdateError.Commit, () => `Failed to commit values: ${e}`);
102
+ }
103
+ return Result.ok(OK);
104
+ }
105
+ /** Read a value from fjall. */
106
+ readValue(key) {
107
+ const val = toUint8Array(this.values.get(key.raw));
108
+ if (val === null) {
109
+ throw new Error(`Missing value at key: ${key}`);
110
+ }
111
+ return val;
112
+ }
113
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hybrid-states.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hybrid-states.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-fjall/hybrid-states.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,83 @@
1
+ import assert from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import { afterEach, before, beforeEach, describe, it } from "node:test";
4
+ import { Bytes, BytesBlob } from "#@typeberry/bytes";
5
+ import { tinyChainSpec } from "#@typeberry/config";
6
+ import { Blake2b, HASH_SIZE } from "#@typeberry/hash";
7
+ import { InMemoryState } from "#@typeberry/state";
8
+ import { StateEntries } from "#@typeberry/state-merkleization";
9
+ import { deepEqual, OK, Result } from "#@typeberry/utils";
10
+ import { HybridSerializedStates } from "./hybrid-states.js";
11
+ let blake2b;
12
+ before(async () => {
13
+ blake2b = await Blake2b.createHasher();
14
+ });
15
+ function createTempDir(suffix = "fjall-hybrid") {
16
+ return fs.mkdtempSync(`typeberry-${suffix}`);
17
+ }
18
+ describe("Fjall hybrid serialized states", () => {
19
+ const spec = tinyChainSpec;
20
+ const headerHash = Bytes.zero(HASH_SIZE).asOpaque();
21
+ let dbPath = "";
22
+ beforeEach(() => {
23
+ dbPath = createTempDir();
24
+ });
25
+ afterEach(() => {
26
+ fs.rmSync(dbPath, { recursive: true });
27
+ });
28
+ it("round-trips an initial state through the on-disk values store", async () => {
29
+ const states = await HybridSerializedStates.new({ spec, blake2b, dbPath });
30
+ try {
31
+ const empty = InMemoryState.empty(spec);
32
+ const serialized = StateEntries.serializeInMemory(spec, blake2b, empty);
33
+ const expectedRoot = serialized.getRootHash(blake2b);
34
+ const res = await states.insertInitialState(headerHash, serialized);
35
+ deepEqual(res, Result.ok(OK));
36
+ const state = states.getState(headerHash);
37
+ assert.ok(state !== null);
38
+ const stateRoot = await states.getStateRoot(state);
39
+ assert.strictEqual(`${stateRoot}`, `${expectedRoot}`);
40
+ deepEqual(InMemoryState.copyFrom(spec, state, new Map()), empty);
41
+ }
42
+ finally {
43
+ await states.close();
44
+ }
45
+ });
46
+ it("reads large values back from disk", async () => {
47
+ const states = await HybridSerializedStates.new({ spec, blake2b, dbPath });
48
+ try {
49
+ // > 32 bytes => stored in the values db (not embedded in the leaf).
50
+ const big1 = BytesBlob.blobFromString("x".repeat(100));
51
+ const big2 = BytesBlob.blobFromString("y".repeat(100));
52
+ const key1 = Bytes.fill(HASH_SIZE, 1).asOpaque();
53
+ const key2 = Bytes.fill(HASH_SIZE, 2).asOpaque();
54
+ const entries = StateEntries.fromEntriesUnsafe([
55
+ [key1, big1],
56
+ [key2, big2],
57
+ ]);
58
+ const res = await states.insertInitialState(headerHash, entries);
59
+ deepEqual(res, Result.ok(OK));
60
+ const state = states.getState(headerHash);
61
+ assert.ok(state !== null);
62
+ assert.strictEqual(`${state.backend.get(key2)}`, `${big2}`);
63
+ assert.strictEqual(`${state.backend.get(key1)}`, `${big1}`);
64
+ }
65
+ finally {
66
+ await states.close();
67
+ }
68
+ });
69
+ it("drops the leaf set on markUnused while values stay on disk", async () => {
70
+ const states = await HybridSerializedStates.new({ spec, blake2b, dbPath });
71
+ try {
72
+ const empty = InMemoryState.empty(spec);
73
+ const serialized = StateEntries.serializeInMemory(spec, blake2b, empty);
74
+ await states.insertInitialState(headerHash, serialized);
75
+ assert.ok(states.getState(headerHash) !== null);
76
+ states.markUnused(headerHash);
77
+ assert.strictEqual(states.getState(headerHash), null);
78
+ }
79
+ finally {
80
+ await states.close();
81
+ }
82
+ });
83
+ });
@@ -0,0 +1,3 @@
1
+ export * from "./hybrid-states.js";
2
+ export * from "./root.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-fjall/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from "./hybrid-states.js";
2
+ export * from "./root.js";
@@ -0,0 +1,52 @@
1
+ import { type Partition } from "@fjall-js/fjall";
2
+ export type { Partition };
3
+ /**
4
+ * Normalize a value read from fjall (a Node `Buffer`) into a plain `Uint8Array`.
5
+ */
6
+ export declare function toUint8Array(value: Buffer | null): Uint8Array | null;
7
+ export type FjallRootOptions = {
8
+ /**
9
+ * When set, durability flushes (`persist`) are skipped entirely.
10
+ *
11
+ * Only safe for throwaway databases (e.g. the fuzz target, which wipes on
12
+ * every reset). Mirrors LMDB's `noSync`.
13
+ */
14
+ ephemeral?: boolean;
15
+ };
16
+ /**
17
+ * A thin abstraction over the fjall keyspace.
18
+ *
19
+ * Unlike LMDB (a memory-mapped B-tree), fjall is an LSM-tree that reads/writes
20
+ * through regular file I/O, so its working set is bounded by an explicit block
21
+ * cache rather than the whole mmap. LMDB has been causing oom issues because
22
+ * of that.
23
+ */
24
+ export declare class FjallRoot {
25
+ private readonly keyspace;
26
+ /** Path of the underlying keyspace directory, used to report on-disk usage. */
27
+ private readonly dbPath;
28
+ private readonly options;
29
+ private constructor();
30
+ /** Open (or create) a fjall keyspace at the given path. */
31
+ static open(dbPath: string, options: FjallRootOptions): Promise<FjallRoot>;
32
+ /** Open (or create) a partition under this keyspace. */
33
+ partition(name: string): Promise<Partition>;
34
+ /**
35
+ * Flush the journal to disk so prior writes survive a crash.
36
+ *
37
+ * Call after a logically-complete unit of work (one block, one state commit).
38
+ * A no-op for ephemeral databases.
39
+ */
40
+ persist(): Promise<void>;
41
+ /**
42
+ * Apparent on-disk size of the keyspace directory, in bytes.
43
+ *
44
+ * Returns `null` if the directory cannot be walked (e.g. not yet created).
45
+ * Unlike LMDB's single `data.mdb`, a fjall keyspace is a directory of
46
+ * partition and journal files, so we sum it recursively.
47
+ */
48
+ sizeInBytes(): number | null;
49
+ /** Persist with `sync-all` and release the keyspace handle. */
50
+ close(): Promise<void>;
51
+ }
52
+ //# sourceMappingURL=root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-fjall/root.ts"],"names":[],"mappings":"AAEA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEtE,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,UAAU,GAAG,IAAI,CAKpE;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF;;;;;;;GAOG;AACH,qBAAa,SAAS;IAElB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ1B,OAAO;IAOP,2DAA2D;WAC9C,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC;IAKhF,wDAAwD;IAClD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAIjD;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAO9B;;;;;;OAMG;IACH,WAAW,IAAI,MAAM,GAAG,IAAI;IAQ5B,+DAA+D;IACzD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -0,0 +1,85 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { open } from "@fjall-js/fjall";
4
+ /**
5
+ * Normalize a value read from fjall (a Node `Buffer`) into a plain `Uint8Array`.
6
+ */
7
+ export function toUint8Array(value) {
8
+ if (value === null) {
9
+ return null;
10
+ }
11
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
12
+ }
13
+ /**
14
+ * A thin abstraction over the fjall keyspace.
15
+ *
16
+ * Unlike LMDB (a memory-mapped B-tree), fjall is an LSM-tree that reads/writes
17
+ * through regular file I/O, so its working set is bounded by an explicit block
18
+ * cache rather than the whole mmap. LMDB has been causing oom issues because
19
+ * of that.
20
+ */
21
+ export class FjallRoot {
22
+ keyspace;
23
+ dbPath;
24
+ options;
25
+ constructor(keyspace,
26
+ /** Path of the underlying keyspace directory, used to report on-disk usage. */
27
+ dbPath, options) {
28
+ this.keyspace = keyspace;
29
+ this.dbPath = dbPath;
30
+ this.options = options;
31
+ }
32
+ /** Open (or create) a fjall keyspace at the given path. */
33
+ static async open(dbPath, options) {
34
+ const keyspace = await open(dbPath);
35
+ return new FjallRoot(keyspace, dbPath, options);
36
+ }
37
+ /** Open (or create) a partition under this keyspace. */
38
+ async partition(name) {
39
+ return this.keyspace.partition(name);
40
+ }
41
+ /**
42
+ * Flush the journal to disk so prior writes survive a crash.
43
+ *
44
+ * Call after a logically-complete unit of work (one block, one state commit).
45
+ * A no-op for ephemeral databases.
46
+ */
47
+ async persist() {
48
+ if (this.options.ephemeral === true) {
49
+ return;
50
+ }
51
+ await this.keyspace.persist();
52
+ }
53
+ /**
54
+ * Apparent on-disk size of the keyspace directory, in bytes.
55
+ *
56
+ * Returns `null` if the directory cannot be walked (e.g. not yet created).
57
+ * Unlike LMDB's single `data.mdb`, a fjall keyspace is a directory of
58
+ * partition and journal files, so we sum it recursively.
59
+ */
60
+ sizeInBytes() {
61
+ try {
62
+ return dirSizeInBytes(this.dbPath);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ /** Persist with `sync-all` and release the keyspace handle. */
69
+ async close() {
70
+ await this.keyspace.close();
71
+ }
72
+ }
73
+ function dirSizeInBytes(dir) {
74
+ let total = 0;
75
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
76
+ const full = path.join(dir, entry.name);
77
+ if (entry.isDirectory()) {
78
+ total += dirSizeInBytes(full);
79
+ }
80
+ else if (entry.isFile()) {
81
+ total += fs.statSync(full).size;
82
+ }
83
+ }
84
+ return total;
85
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"main-fuzz.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-fuzz.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uBAAuB,EAAE,OAAO,CAAC;CAClC,CAAC;AAOF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;AAED,wBAAgB,cAAc;;;;EAM7B;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,uBA2GxF"}
1
+ {"version":3,"file":"main-fuzz.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-fuzz.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uBAAuB,EAAE,OAAO,CAAC;CAClC,CAAC;AAWF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;AAED,wBAAgB,cAAc;;;;EAM7B;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,uBAqHxF"}
@@ -14,6 +14,9 @@ import { mainImporter } from "./main-importer.js";
14
14
  const logger = Logger.new(import.meta.filename, "fuzztarget");
15
15
  /** Dedicated subdirectory under the configured base path that the fuzzer owns and wipes. */
16
16
  const FUZZ_DB_SUBDIR = "typeberry-fuzz-db";
17
+ const FUZZ_DB_FJALL = "fjall-hybrid";
18
+ const FUZZ_DB_LMDB = "lmdb-hybrid";
19
+ const FUZZ_DB_OPTIONS = [FUZZ_DB_FJALL, FUZZ_DB_LMDB];
17
20
  /**
18
21
  * Resolve the directory the fuzzer should use for its on-disk database, or
19
22
  * `undefined` for an in-memory database. The dedicated `FUZZ_DB_SUBDIR` is
@@ -50,6 +53,15 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
50
53
  logHostEnvironment(logger);
51
54
  const { jamNodeConfig: config } = fuzzConfig;
52
55
  const fuzzDbBase = resolveFuzzDbBase(config.node.databaseBasePath);
56
+ const rawFuzzDb = process.env.JAM_FUZZ_DB?.trim() ?? "";
57
+ // Using experimental fjall-hybrid by default, with an option to test lmdb as well.
58
+ const hybridStateBackend = rawFuzzDb === "" ? FUZZ_DB_FJALL : rawFuzzDb;
59
+ if (!isValidStateBackend(hybridStateBackend)) {
60
+ throw new Error(`JAM_FUZZ_DB must be one of: ${FUZZ_DB_OPTIONS} (got: "${rawFuzzDb}").`);
61
+ }
62
+ if (fuzzDbBase !== undefined) {
63
+ logger.info `🗄️ Fuzz persistent backend: ${hybridStateBackend}.`;
64
+ }
53
65
  let runningNode = null;
54
66
  const chainSpec = getChainSpec(config.node.flavor);
55
67
  const closeFuzzTarget = startFuzzTarget(fuzzConfig.version, fuzzConfig.socket, {
@@ -106,7 +118,7 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
106
118
  // spec only, where values are big) bounds its on-disk/page-cache size.
107
119
  // Tiny stays uncompressed since its db is small and speed matters more.
108
120
  ephemeral: isPersistent,
109
- stateBackend: isPersistent ? "hybrid" : "lmdb",
121
+ stateBackend: isPersistent ? hybridStateBackend : "lmdb",
110
122
  });
111
123
  };
112
124
  if (fuzzDbBase !== undefined) {
@@ -136,3 +148,6 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
136
148
  }
137
149
  };
138
150
  }
151
+ function isValidStateBackend(val) {
152
+ return FUZZ_DB_OPTIONS.indexOf(val) !== -1;
153
+ }
@@ -1,13 +1,16 @@
1
1
  import type { JamConfig } from "./jam-config.js";
2
2
  import type { NodeApi } from "./main.js";
3
+ export type StateBackend = "lmdb" | "lmdb-hybrid" | "fjall-hybrid";
3
4
  export type ImporterOptions = {
4
5
  initGenesisFromAncestry?: boolean;
5
6
  dummyFinalityDepth?: number;
6
7
  pruneBlocks?: boolean;
7
- /** Open the LMDB database without fsync/compression. Only safe for throwaway dbs (e.g. fuzzing). */
8
+ /** Open the database without fsync/compression. Only safe for throwaway dbs (e.g. fuzzing). */
8
9
  ephemeral?: boolean;
9
- /** Persistent backend to use when `databaseBasePath` is set. Defaults to full LMDB. */
10
- stateBackend?: "lmdb" | "hybrid";
10
+ /**
11
+ * Persistent backend to use when `databaseBasePath` is set. Defaults to full LMDB.
12
+ */
13
+ stateBackend?: StateBackend;
11
14
  };
12
15
  export declare function mainImporter(config: JamConfig, withRelPath: (v: string) => string, options?: ImporterOptions): Promise<NodeApi>;
13
16
  //# sourceMappingURL=main-importer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-importer.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-importer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oGAAoG;IACpG,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;CAClC,CAAC;AAEF,wBAAsB,YAAY,CAChC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EAClC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,OAAO,CAAC,CAoGlB"}
1
+ {"version":3,"file":"main-importer.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-importer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,aAAa,GAAG,cAAc,CAAC;AAEnE,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;OAEG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,wBAAsB,YAAY,CAChC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EAClC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,OAAO,CAAC,CAqGlB"}
@@ -39,8 +39,8 @@ export async function mainImporter(config, withRelPath, options = {}) {
39
39
  blake2b,
40
40
  workerParams,
41
41
  })
42
- : dbBackend === "hybrid"
43
- ? HybridWorkerConfig.new({
42
+ : dbBackend === "lmdb-hybrid" || dbBackend === "fjall-hybrid"
43
+ ? await HybridWorkerConfig.new({
44
44
  nodeName,
45
45
  chainSpec,
46
46
  blake2b,
@@ -48,6 +48,7 @@ export async function mainImporter(config, withRelPath, options = {}) {
48
48
  workerParams,
49
49
  ephemeral,
50
50
  compression,
51
+ backend: dbBackend === "lmdb-hybrid" ? "lmdb" : "fjall",
51
52
  })
52
53
  : LmdbWorkerConfig.new({
53
54
  nodeName,
@@ -71,13 +71,19 @@ export declare class InMemWorkerConfig<T = undefined> implements WorkerConfig<T,
71
71
  readonly: boolean;
72
72
  }): RootDb<BlocksDb, SerializedStatesDb>;
73
73
  }
74
+ /** Persistent values store backing the hybrid config. */
75
+ export type HybridBackend = "lmdb" | "fjall";
74
76
  /**
75
77
  * Hybrid worker config for the fuzz target: in-memory blocks and leaf sets,
76
- * but large values persisted to LMDB.
78
+ * but large values persisted to disk (LMDB or fjall, selected by `backend`).
79
+ *
80
+ * The fjall backend is opt-in so its performance can be compared against LMDB
81
+ * before committing to it. fjall opens its keyspace asynchronously, hence the
82
+ * async `new`.
77
83
  *
78
84
  * Like `InMemWorkerConfig`, the blocks and leaf sets are shared across the
79
85
  * open/close/reopen dance that genesis init performs, so `openDatabase`
80
- * returns the same instances and a no-op close. The LMDB root is opened once
86
+ * returns the same instances and a no-op close. The values store is opened once
81
87
  * here and closed by `HybridSerializedStates.close()` at importer teardown.
82
88
  *
83
89
  * In-process only: it holds shared mutable state (the in-memory leaf
@@ -92,7 +98,8 @@ export declare class HybridWorkerConfig<T = undefined> implements WorkerConfig<T
92
98
  readonly dbPath: string;
93
99
  readonly ephemeral: boolean;
94
100
  readonly compression: boolean;
95
- static new<T>({ nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression, }: {
101
+ private readonly states;
102
+ static new<T>({ nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression, backend, }: {
96
103
  nodeName: string;
97
104
  chainSpec: ChainSpec;
98
105
  workerParams: T;
@@ -100,9 +107,9 @@ export declare class HybridWorkerConfig<T = undefined> implements WorkerConfig<T
100
107
  dbPath: string;
101
108
  ephemeral?: boolean;
102
109
  compression?: boolean;
103
- }): HybridWorkerConfig<T>;
110
+ backend?: HybridBackend;
111
+ }): Promise<HybridWorkerConfig<T>>;
104
112
  private readonly blocks;
105
- private readonly states;
106
113
  private constructor();
107
114
  openDatabase(_options?: {
108
115
  readonly: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/api-node/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/api-node/config.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { Decoder, Encoder } from "#@typeberry/codec";
2
2
  import { ChainSpec } from "#@typeberry/config";
3
3
  import { InMemoryBlocks, InMemorySerializedStates, } from "#@typeberry/database";
4
- import { HybridSerializedStates, LmdbBlocks, LmdbRoot, LmdbStates } from "#@typeberry/database-lmdb";
4
+ import { HybridSerializedStates as FjallHybridSerializedStates } from "#@typeberry/database-fjall";
5
+ import { LmdbBlocks, HybridSerializedStates as LmdbHybridSerializedStates, LmdbRoot, LmdbStates, } from "#@typeberry/database-lmdb";
5
6
  import { Blake2b } from "#@typeberry/hash";
6
7
  import { ThreadPort } from "./port.js";
7
8
  /** A worker config that's usable in node.js and uses LMDB database backend. */
@@ -110,11 +111,15 @@ export class InMemWorkerConfig {
110
111
  }
111
112
  /**
112
113
  * Hybrid worker config for the fuzz target: in-memory blocks and leaf sets,
113
- * but large values persisted to LMDB.
114
+ * but large values persisted to disk (LMDB or fjall, selected by `backend`).
115
+ *
116
+ * The fjall backend is opt-in so its performance can be compared against LMDB
117
+ * before committing to it. fjall opens its keyspace asynchronously, hence the
118
+ * async `new`.
114
119
  *
115
120
  * Like `InMemWorkerConfig`, the blocks and leaf sets are shared across the
116
121
  * open/close/reopen dance that genesis init performs, so `openDatabase`
117
- * returns the same instances and a no-op close. The LMDB root is opened once
122
+ * returns the same instances and a no-op close. The values store is opened once
118
123
  * here and closed by `HybridSerializedStates.close()` at importer teardown.
119
124
  *
120
125
  * In-process only: it holds shared mutable state (the in-memory leaf
@@ -129,12 +134,17 @@ export class HybridWorkerConfig {
129
134
  dbPath;
130
135
  ephemeral;
131
136
  compression;
132
- static new({ nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral = false, compression = true, }) {
133
- return new HybridWorkerConfig(nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression);
137
+ states;
138
+ static async new({ nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral = false, compression = true, backend = "lmdb", }) {
139
+ // fjall opens its keyspace asynchronously; LMDB is synchronous. Either way
140
+ // the values store is created once here and shared across reopen.
141
+ const states = backend === "fjall"
142
+ ? await FjallHybridSerializedStates.new({ spec: chainSpec, blake2b, dbPath, ephemeral })
143
+ : LmdbHybridSerializedStates.new({ spec: chainSpec, blake2b, dbPath, ephemeral, compression, readOnly: false });
144
+ return new HybridWorkerConfig(nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression, states);
134
145
  }
135
146
  blocks;
136
- states;
137
- constructor(nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression = true) {
147
+ constructor(nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression, states) {
138
148
  this.nodeName = nodeName;
139
149
  this.chainSpec = chainSpec;
140
150
  this.workerParams = workerParams;
@@ -142,22 +152,15 @@ export class HybridWorkerConfig {
142
152
  this.dbPath = dbPath;
143
153
  this.ephemeral = ephemeral;
144
154
  this.compression = compression;
155
+ this.states = states;
145
156
  this.blocks = InMemoryBlocks.new();
146
- this.states = HybridSerializedStates.new({
147
- spec: this.chainSpec,
148
- blake2b: this.blake2b,
149
- dbPath: this.dbPath,
150
- ephemeral: this.ephemeral,
151
- compression: this.compression,
152
- readOnly: false,
153
- });
154
157
  }
155
158
  openDatabase(_options = { readonly: true }) {
156
159
  return {
157
160
  getBlocksDb: () => this.blocks,
158
161
  getStatesDb: () => this.states,
159
- // Leaf sets and blocks live in memory; the LMDB values store is closed
160
- // via states.close() at importer teardown, so this is a no-op.
162
+ // Leaf sets and blocks live in memory; the values store is closed via
163
+ // states.close() at importer teardown, so this is a no-op.
161
164
  close: async () => { },
162
165
  };
163
166
  }
@@ -1,11 +1,12 @@
1
1
  import assert from "node:assert";
2
+ import * as fs from "node:fs";
2
3
  import { describe, it } from "node:test";
3
4
  import { MessageChannel } from "node:worker_threads";
4
5
  import { codec } from "#@typeberry/codec";
5
6
  import { tinyChainSpec } from "#@typeberry/config";
6
7
  import { Blake2b } from "#@typeberry/hash";
7
8
  import { tryAsU32 } from "#@typeberry/numbers";
8
- import { configTransferList, LmdbWorkerConfig } from "./config.js";
9
+ import { configTransferList, HybridWorkerConfig, LmdbWorkerConfig } from "./config.js";
9
10
  import { ThreadPort } from "./port.js";
10
11
  const spec = tinyChainSpec;
11
12
  describe("LmdbWorkerConfig transfer list", () => {
@@ -39,3 +40,39 @@ describe("LmdbWorkerConfig transfer list", () => {
39
40
  }
40
41
  });
41
42
  });
43
+ describe("HybridWorkerConfig", () => {
44
+ // Both persistent backends must construct asynchronously and hand out a
45
+ // working db. fjall is the experimental backend we want to benchmark.
46
+ for (const backend of ["lmdb", "fjall"]) {
47
+ it(`constructs and opens a ${backend}-backed hybrid db`, async () => {
48
+ const blake2b = await Blake2b.createHasher();
49
+ const dbPath = fs.mkdtempSync(`typeberry-hybrid-${backend}-`);
50
+ try {
51
+ const config = await HybridWorkerConfig.new({
52
+ nodeName: "node",
53
+ chainSpec: spec,
54
+ workerParams: undefined,
55
+ blake2b,
56
+ dbPath,
57
+ ephemeral: true,
58
+ backend,
59
+ });
60
+ const db = config.openDatabase({ readonly: false });
61
+ const states = db.getStatesDb();
62
+ try {
63
+ assert.notStrictEqual(db.getBlocksDb(), undefined);
64
+ assert.notStrictEqual(states, undefined);
65
+ }
66
+ finally {
67
+ // The values store owns the on-disk resources (the no-op db.close()
68
+ // does not), so close it explicitly to release the fjall keyspace.
69
+ await states.close();
70
+ await db.close();
71
+ }
72
+ }
73
+ finally {
74
+ fs.rmSync(dbPath, { recursive: true, force: true });
75
+ }
76
+ });
77
+ }
78
+ });