@typeberry/lib 0.10.0-9e3af22 → 0.11.0-411c828

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.
Files changed (63) hide show
  1. package/package.json +1 -3
  2. package/packages/jam/config-node/node-config.d.ts +1 -11
  3. package/packages/jam/config-node/node-config.d.ts.map +1 -1
  4. package/packages/jam/config-node/node-config.js +3 -25
  5. package/packages/jam/config-node/node-config.test.js +3 -7
  6. package/packages/jam/database/states.d.ts +1 -8
  7. package/packages/jam/database/states.d.ts.map +1 -1
  8. package/packages/jam/database/value-refs.d.ts +4 -5
  9. package/packages/jam/database/value-refs.d.ts.map +1 -1
  10. package/packages/jam/database-fjall/blocks.d.ts +1 -0
  11. package/packages/jam/database-fjall/blocks.d.ts.map +1 -1
  12. package/packages/jam/database-fjall/blocks.js +8 -3
  13. package/packages/jam/database-fjall/hybrid-states.d.ts +10 -4
  14. package/packages/jam/database-fjall/hybrid-states.d.ts.map +1 -1
  15. package/packages/jam/database-fjall/hybrid-states.js +22 -6
  16. package/packages/jam/database-fjall/root.d.ts +2 -0
  17. package/packages/jam/database-fjall/root.d.ts.map +1 -1
  18. package/packages/jam/database-fjall/root.js +8 -1
  19. package/packages/jam/database-fjall/root.test.d.ts +2 -0
  20. package/packages/jam/database-fjall/root.test.d.ts.map +1 -0
  21. package/packages/jam/database-fjall/root.test.js +23 -0
  22. package/packages/jam/database-fjall/states.d.ts +1 -0
  23. package/packages/jam/database-fjall/states.d.ts.map +1 -1
  24. package/packages/jam/database-fjall/states.js +6 -3
  25. package/packages/jam/node/export.d.ts.map +1 -1
  26. package/packages/jam/node/export.js +8 -17
  27. package/packages/jam/node/main-fuzz.d.ts.map +1 -1
  28. package/packages/jam/node/main-fuzz.js +51 -46
  29. package/packages/jam/node/main-importer.d.ts +6 -11
  30. package/packages/jam/node/main-importer.d.ts.map +1 -1
  31. package/packages/jam/node/main-importer.js +14 -28
  32. package/packages/jam/node/main.d.ts.map +1 -1
  33. package/packages/jam/node/main.js +6 -16
  34. package/packages/jam/rpc/test/e2e-server.d.ts.map +1 -1
  35. package/packages/jam/rpc/test/e2e-server.js +3 -5
  36. package/packages/jam/rpc/test/e2e.js +1 -1
  37. package/packages/jam/state-merkleization/index.d.ts +1 -1
  38. package/packages/jam/state-merkleization/index.js +1 -1
  39. package/packages/workers/api-node/config.d.ts +14 -46
  40. package/packages/workers/api-node/config.d.ts.map +1 -1
  41. package/packages/workers/api-node/config.js +31 -96
  42. package/packages/workers/api-node/config.test.js +82 -63
  43. package/packages/jam/database-lmdb/blocks.d.ts +0 -24
  44. package/packages/jam/database-lmdb/blocks.d.ts.map +0 -1
  45. package/packages/jam/database-lmdb/blocks.js +0 -82
  46. package/packages/jam/database-lmdb/hybrid-states.d.ts +0 -56
  47. package/packages/jam/database-lmdb/hybrid-states.d.ts.map +0 -1
  48. package/packages/jam/database-lmdb/hybrid-states.js +0 -149
  49. package/packages/jam/database-lmdb/hybrid-states.test.d.ts +0 -2
  50. package/packages/jam/database-lmdb/hybrid-states.test.d.ts.map +0 -1
  51. package/packages/jam/database-lmdb/hybrid-states.test.js +0 -180
  52. package/packages/jam/database-lmdb/index.d.ts +0 -5
  53. package/packages/jam/database-lmdb/index.d.ts.map +0 -1
  54. package/packages/jam/database-lmdb/index.js +0 -4
  55. package/packages/jam/database-lmdb/root.d.ts +0 -26
  56. package/packages/jam/database-lmdb/root.d.ts.map +0 -1
  57. package/packages/jam/database-lmdb/root.js +0 -53
  58. package/packages/jam/database-lmdb/states.d.ts +0 -76
  59. package/packages/jam/database-lmdb/states.d.ts.map +0 -1
  60. package/packages/jam/database-lmdb/states.js +0 -145
  61. package/packages/jam/database-lmdb/states.test.d.ts +0 -2
  62. package/packages/jam/database-lmdb/states.test.d.ts.map +0 -1
  63. package/packages/jam/database-lmdb/states.test.js +0 -202
@@ -1,149 +0,0 @@
1
- import { HashDictionary, SortedSet } from "#@typeberry/collections";
2
- import { InMemoryValueRefsStore, LeafDb, StateUpdateError, updateLeafs, ValueRefs, } 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 { LmdbRoot } from "./root.js";
8
- const logger = Logger.new(import.meta.filename, "db");
9
- /**
10
- * Hybrid serialized-states db.
11
- *
12
- * States (leafs) are kept in-memory, but large values are persisted to lmdb.
13
- * Reads go straight to lmdb, which keeps its own page cache.
14
- * NOTE: this DB is designed for long fuzzing and to be used with pruning to
15
- * keep the heap usage bounded.
16
- *
17
- * Values that no longer belong to any surviving state are removed from lmdb,
18
- * decided by in-memory refcounting (`ValueRefs`) driven by the importer's
19
- * finality signal. Counts are not persisted: this db cannot resume from disk
20
- * anyway (the leaf sets live in memory), so values left over by a previous
21
- * run are never collected.
22
- */
23
- export class HybridSerializedStates {
24
- spec;
25
- blake2b;
26
- root;
27
- inMemStates = HashDictionary.new();
28
- lmdbValues;
29
- // A single shared values accessor reused by every `LeafDb` we hand out.
30
- valuesDb;
31
- refsStore = new InMemoryValueRefsStore();
32
- refs = new ValueRefs(this.refsStore);
33
- // Queue of not-yet-committed value removals, awaited on close.
34
- pendingCleanup = Promise.resolve();
35
- static new({ spec, blake2b, dbPath, readOnly, ephemeral, compression, }) {
36
- const root = LmdbRoot.new(dbPath, { readOnly, ephemeral, compression });
37
- return new HybridSerializedStates(spec, blake2b, root);
38
- }
39
- constructor(spec, blake2b, root) {
40
- this.spec = spec;
41
- this.blake2b = blake2b;
42
- this.root = root;
43
- this.lmdbValues = this.root.subDb("values");
44
- this.valuesDb = { get: (key) => this.readValue(key) };
45
- }
46
- async insertInitialState(headerHash, entries) {
47
- const { values, leafs } = updateLeafs(SortedSet.fromArray(leafComparator, []), this.blake2b, Array.from(entries, (x) => [StateEntryUpdateAction.Insert, x[0], x[1]]));
48
- const res = await this.writeValues(values);
49
- if (res.isError) {
50
- return res;
51
- }
52
- this.inMemStates.set(headerHash, leafs);
53
- this.applyRefs(this.refs.onInitial(values.map((v) => v[0])));
54
- return Result.ok(OK);
55
- }
56
- async updateAndSetState(header, state, update) {
57
- const updatedValues = serializeStateUpdate(this.spec, this.blake2b, update);
58
- // Clone the leaf set before mutating: the previous state keeps using its own.
59
- const newLeafs = SortedSet.fromSortedArray(leafComparator, state.backend.leafs.array);
60
- const { values, removed, leafs } = updateLeafs(newLeafs, this.blake2b, updatedValues);
61
- const res = await this.writeValues(values);
62
- if (res.isError) {
63
- // Leave the caller's state untouched: its new leaves would reference
64
- // values that never reached disk.
65
- return res;
66
- }
67
- // Re-create the lookup with the shared values accessor only once the new
68
- // values are durably written.
69
- state.updateBackend(LeafDb.fromLeaves(leafs, this.valuesDb));
70
- this.inMemStates.set(header, leafs);
71
- this.applyRefs(this.refs.onImport(header, { inserted: values.map((v) => v[0]), removed }));
72
- return Result.ok(OK);
73
- }
74
- async getStateRoot(state) {
75
- return state.backend.getStateRoot(this.blake2b);
76
- }
77
- getState(header) {
78
- const leafs = this.inMemStates.get(header);
79
- if (leafs === undefined) {
80
- return null;
81
- }
82
- const leafDb = LeafDb.fromLeaves(leafs, this.valuesDb);
83
- return SerializedState.new(this.spec, this.blake2b, leafDb);
84
- }
85
- commitFinalized(headers) {
86
- this.applyRefs(this.refs.commitFinalized(headers));
87
- }
88
- markUnused(header) {
89
- // Release the speculative references first (a no-op for finalized states,
90
- // whose deltas were already consumed by `commitFinalized`).
91
- this.applyRefs(this.refs.releaseUnfinalized(header));
92
- this.inMemStates.delete(header);
93
- }
94
- /** Apply a refcounting update and remove values that lost their last reference. */
95
- applyRefs(update) {
96
- this.refsStore.apply(update);
97
- if (update.removeValues.length === 0) {
98
- return;
99
- }
100
- // Queued, not awaited: a failed removal only leaks a value.
101
- this.pendingCleanup = this.pendingCleanup
102
- .then(() => this.lmdbValues.transaction(() => {
103
- for (const v of update.removeValues) {
104
- this.lmdbValues.remove(v.raw);
105
- }
106
- }))
107
- .catch((e) => {
108
- logger.error `Failed to remove unreferenced values: ${e}`;
109
- });
110
- }
111
- diskSizeInBytes() {
112
- return this.root.sizeInBytes();
113
- }
114
- async close() {
115
- await this.pendingCleanup;
116
- await this.lmdbValues.close();
117
- await this.root.close();
118
- }
119
- /** Write new large values to LMDB in one transaction. */
120
- async writeValues(values) {
121
- if (values.length === 0) {
122
- return Result.ok(OK);
123
- }
124
- try {
125
- // Flush queued removals first: a pending cleanup might be about to delete
126
- // a content hash we are re-inserting now. Writing behind it keeps the
127
- // re-inserted value on disk (removals are only queued at refcount 0, so
128
- // none can be queued for a value still referenced).
129
- await this.pendingCleanup;
130
- await this.lmdbValues.transaction(() => {
131
- for (const [hash, val] of values) {
132
- this.lmdbValues.put(hash.raw, val.raw);
133
- }
134
- });
135
- }
136
- catch (e) {
137
- return Result.error(StateUpdateError.Commit, () => `Failed to commit values: ${e}`);
138
- }
139
- return Result.ok(OK);
140
- }
141
- /** Read a value from LMDB. */
142
- readValue(key) {
143
- const val = this.lmdbValues.get(key.raw);
144
- if (val === undefined) {
145
- throw new Error(`Missing value at key: ${key}`);
146
- }
147
- return val;
148
- }
149
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=hybrid-states.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"hybrid-states.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-lmdb/hybrid-states.test.ts"],"names":[],"mappings":""}
@@ -1,180 +0,0 @@
1
- // packages/jam/database-lmdb/hybrid-states.test.ts
2
- import assert from "node:assert";
3
- import * as fs from "node:fs";
4
- import { afterEach, before, beforeEach, describe, it } from "node:test";
5
- import { tryAsServiceId } from "#@typeberry/block";
6
- import { Bytes, BytesBlob } from "#@typeberry/bytes";
7
- import { tinyChainSpec } from "#@typeberry/config";
8
- import { Blake2b, HASH_SIZE } from "#@typeberry/hash";
9
- import { InMemoryState, StorageItem, UpdateStorage, } from "#@typeberry/state";
10
- import { testState } from "#@typeberry/state/test.utils.js";
11
- import { StateEntries } from "#@typeberry/state-merkleization";
12
- import { asOpaqueType, deepEqual, OK, Result } from "#@typeberry/utils";
13
- import { HybridSerializedStates } from "./hybrid-states.js";
14
- let blake2b;
15
- before(async () => {
16
- blake2b = await Blake2b.createHasher();
17
- });
18
- function createTempDir(suffix = "hybrid") {
19
- return fs.mkdtempSync(`typeberry-${suffix}`);
20
- }
21
- describe("Hybrid serialized states", () => {
22
- const spec = tinyChainSpec;
23
- const headerHash = Bytes.zero(HASH_SIZE).asOpaque();
24
- let dbPath = "";
25
- beforeEach(() => {
26
- dbPath = createTempDir();
27
- });
28
- afterEach(() => {
29
- fs.rmSync(dbPath, { recursive: true });
30
- });
31
- it("round-trips an initial state through the on-disk values store", async () => {
32
- const states = HybridSerializedStates.new({
33
- spec,
34
- blake2b,
35
- dbPath,
36
- });
37
- try {
38
- const empty = InMemoryState.empty(spec);
39
- const serialized = StateEntries.serializeInMemory(spec, blake2b, empty);
40
- const expectedRoot = serialized.getRootHash(blake2b);
41
- const res = await states.insertInitialState(headerHash, serialized);
42
- deepEqual(res, Result.ok(OK));
43
- const state = states.getState(headerHash);
44
- assert.ok(state !== null);
45
- const stateRoot = await states.getStateRoot(state);
46
- assert.strictEqual(`${stateRoot}`, `${expectedRoot}`);
47
- deepEqual(InMemoryState.copyFrom(spec, state, new Map()), empty);
48
- }
49
- finally {
50
- await states.close();
51
- }
52
- });
53
- it("reads large values back from disk", async () => {
54
- const states = HybridSerializedStates.new({ spec, blake2b, dbPath });
55
- try {
56
- // > 32 bytes => stored in the values db (not embedded in the leaf).
57
- const big1 = BytesBlob.blobFromString("x".repeat(100));
58
- const big2 = BytesBlob.blobFromString("y".repeat(100));
59
- const key1 = Bytes.fill(HASH_SIZE, 1).asOpaque();
60
- const key2 = Bytes.fill(HASH_SIZE, 2).asOpaque();
61
- const entries = StateEntries.fromEntriesUnsafe([
62
- [key1, big1],
63
- [key2, big2],
64
- ]);
65
- const res = await states.insertInitialState(headerHash, entries);
66
- deepEqual(res, Result.ok(OK));
67
- const state = states.getState(headerHash);
68
- assert.ok(state !== null);
69
- assert.strictEqual(`${state.backend.get(key2)}`, `${big2}`);
70
- assert.strictEqual(`${state.backend.get(key1)}`, `${big1}`);
71
- }
72
- finally {
73
- await states.close();
74
- }
75
- });
76
- it("drops the leaf set on markUnused while values stay on disk", async () => {
77
- const states = HybridSerializedStates.new({ spec, blake2b, dbPath });
78
- try {
79
- const empty = InMemoryState.empty(spec);
80
- const serialized = StateEntries.serializeInMemory(spec, blake2b, empty);
81
- await states.insertInitialState(headerHash, serialized);
82
- assert.ok(states.getState(headerHash) !== null);
83
- states.markUnused(headerHash);
84
- assert.strictEqual(states.getState(headerHash), null);
85
- }
86
- finally {
87
- await states.close();
88
- }
89
- });
90
- });
91
- function hh(n) {
92
- return Bytes.fill(HASH_SIZE, n).asOpaque();
93
- }
94
- const storageKey = asOpaqueType(BytesBlob.blobFromString("test-key"));
95
- /** A state update writing a single large (non-embedded) value under `storageKey`. */
96
- function storageUpdate(value) {
97
- const item = StorageItem.create({ key: storageKey, value: BytesBlob.blobFromString(value) });
98
- return {
99
- storage: new Map([[tryAsServiceId(0), [UpdateStorage.set({ storage: item })]]]),
100
- };
101
- }
102
- // > 32 bytes => stored in the values db (not embedded in the leaf).
103
- const BIG_1 = "a".repeat(100);
104
- const BIG_2 = "b".repeat(100);
105
- /** `true` if every value referenced by the state can still be resolved. */
106
- function canReadFully(state) {
107
- if (state === null) {
108
- return false;
109
- }
110
- try {
111
- state.backend.intoStateEntries();
112
- return true;
113
- }
114
- catch {
115
- return false;
116
- }
117
- }
118
- /** Value removals are queued, so poll for the expected outcome. */
119
- async function eventually(check, what, timeoutMs = 5_000) {
120
- const start = Date.now();
121
- while (!check()) {
122
- if (Date.now() - start > timeoutMs) {
123
- assert.fail(`Timed out waiting for: ${what}`);
124
- }
125
- await new Promise((resolve) => setTimeout(resolve, 10));
126
- }
127
- }
128
- describe("Hybrid serialized states value refcounting", () => {
129
- const spec = tinyChainSpec;
130
- let dbPath = "";
131
- beforeEach(() => {
132
- dbPath = createTempDir();
133
- });
134
- afterEach(() => {
135
- fs.rmSync(dbPath, { recursive: true });
136
- });
137
- it("removes a replaced value from disk once the replacement finalizes", async () => {
138
- const states = HybridSerializedStates.new({ spec, blake2b, dbPath });
139
- try {
140
- await states.insertInitialState(hh(0), StateEntries.serializeInMemory(spec, blake2b, testState()));
141
- const s0 = states.getState(hh(0));
142
- assert.ok(s0 !== null);
143
- await states.updateAndSetState(hh(1), s0, storageUpdate(BIG_1));
144
- // a handle to the post-1 state, surviving the pruning below
145
- const stale1 = states.getState(hh(1));
146
- const s1 = states.getState(hh(1));
147
- assert.ok(s1 !== null);
148
- await states.updateAndSetState(hh(2), s1, storageUpdate(BIG_2));
149
- states.commitFinalized([hh(1)]);
150
- assert.ok(canReadFully(stale1), "still referenced by the finalized tip");
151
- states.commitFinalized([hh(2)]);
152
- await eventually(() => !canReadFully(stale1), "replaced value removed from lmdb");
153
- assert.ok(canReadFully(states.getState(hh(2))), "the new finalized tip stays fully readable");
154
- }
155
- finally {
156
- await states.close();
157
- }
158
- });
159
- it("collects values of a pruned dead fork and keeps the surviving chain intact", async () => {
160
- const states = HybridSerializedStates.new({ spec, blake2b, dbPath });
161
- try {
162
- await states.insertInitialState(hh(0), StateEntries.serializeInMemory(spec, blake2b, testState()));
163
- const s0 = states.getState(hh(0));
164
- assert.ok(s0 !== null);
165
- await states.updateAndSetState(hh(1), s0, storageUpdate(BIG_1));
166
- // a dead fork on top of genesis, inserting a different value
167
- const fork = states.getState(hh(0));
168
- assert.ok(fork !== null);
169
- await states.updateAndSetState(hh(0xaa), fork, storageUpdate(BIG_2));
170
- const staleFork = states.getState(hh(0xaa));
171
- states.markUnused(hh(0xaa));
172
- assert.strictEqual(states.getState(hh(0xaa)), null);
173
- await eventually(() => !canReadFully(staleFork), "fork-only value removed from lmdb");
174
- assert.ok(canReadFully(states.getState(hh(1))), "surviving chain is unaffected");
175
- }
176
- finally {
177
- await states.close();
178
- }
179
- });
180
- });
@@ -1,5 +0,0 @@
1
- export * from "./blocks.js";
2
- export * from "./hybrid-states.js";
3
- export * from "./root.js";
4
- export * from "./states.js";
5
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-lmdb/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
@@ -1,4 +0,0 @@
1
- export * from "./blocks.js";
2
- export * from "./hybrid-states.js";
3
- export * from "./root.js";
4
- export * from "./states.js";
@@ -1,26 +0,0 @@
1
- import * as lmdb from "lmdb";
2
- export type SubDb = lmdb.Database<Uint8Array, lmdb.Key>;
3
- export type LmdbRootOptions = {
4
- readOnly?: boolean;
5
- ephemeral?: boolean;
6
- compression?: boolean;
7
- };
8
- /** A thin abstraction over lmdb database interface. */
9
- export declare class LmdbRoot {
10
- readonly db: lmdb.RootDatabase<Uint8Array, lmdb.Key>;
11
- /** Path of the underlying LMDB data file, used to report on-disk usage. */
12
- private readonly dataFilePath;
13
- static new(dbPath: string, options: LmdbRootOptions): LmdbRoot;
14
- private constructor();
15
- /** Open a sub-database under the same path. */
16
- subDb(name: string): SubDb;
17
- /**
18
- * Apparent on-disk size of the LMDB data file, in bytes.
19
- *
20
- * Returns `null` if the file cannot be `stat`-ed (e.g. not yet created).
21
- */
22
- sizeInBytes(): number | null;
23
- /** Close the database and all sub-databases. */
24
- close(): Promise<void>;
25
- }
26
- //# sourceMappingURL=root.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-lmdb/root.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B,MAAM,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,uDAAuD;AACvD,qBAAa,QAAQ;IACnB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACrD,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe;IAInD,OAAO;IAwBP,+CAA+C;IAC/C,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK;IAI1B;;;;OAIG;IACH,WAAW,IAAI,MAAM,GAAG,IAAI;IAQ5B,gDAAgD;IAC1C,KAAK;CAGZ"}
@@ -1,53 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as lmdb from "lmdb";
3
- /** A thin abstraction over lmdb database interface. */
4
- export class LmdbRoot {
5
- db;
6
- /** Path of the underlying LMDB data file, used to report on-disk usage. */
7
- dataFilePath;
8
- static new(dbPath, options) {
9
- return new LmdbRoot(dbPath, options);
10
- }
11
- constructor(dbPath, { readOnly, ephemeral, compression }) {
12
- // `lmdb.open` treats an extension-less path as a directory and stores the
13
- // environment in `<dbPath>/data.mdb` (next to `lock.mdb`).
14
- this.dataFilePath = `${dbPath}/data.mdb`;
15
- const isEphemeral = ephemeral ?? false;
16
- this.db = lmdb.open(dbPath, {
17
- // experimental options
18
- noMemInit: true,
19
- remapChunks: true,
20
- eventTurnBatching: false,
21
- // For ephemeral databases (e.g. the fuzz target, which wipes on every reset)
22
- // durability is pointless, so we skip fsync and skip compressing the large
23
- // per-block leaf blobs. Both are pure overhead there and dominate the cost.
24
- // This trades disk space (uncompressed) and crash-durability for speed.
25
- compression: compression ?? !isEphemeral,
26
- noSync: isEphemeral,
27
- keyEncoding: "binary",
28
- encoding: "binary",
29
- readOnly,
30
- });
31
- }
32
- /** Open a sub-database under the same path. */
33
- subDb(name) {
34
- return this.db.openDB({ name });
35
- }
36
- /**
37
- * Apparent on-disk size of the LMDB data file, in bytes.
38
- *
39
- * Returns `null` if the file cannot be `stat`-ed (e.g. not yet created).
40
- */
41
- sizeInBytes() {
42
- try {
43
- return fs.statSync(this.dataFilePath).size;
44
- }
45
- catch {
46
- return null;
47
- }
48
- }
49
- /** Close the database and all sub-databases. */
50
- async close() {
51
- await this.db.close();
52
- }
53
- }
@@ -1,76 +0,0 @@
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 type { StateEntries } from "#@typeberry/state-merkleization";
7
- import { SerializedState } from "#@typeberry/state-merkleization";
8
- import { OK, Result } from "#@typeberry/utils";
9
- import type { LmdbRoot } from "./root.js";
10
- /**
11
- * LMDB-backed state storage.
12
- *
13
- * Assumptions:
14
- * 1. For each block we end up with some posterior state.
15
- * 2. Majority of state entries will stay the same, however there are some that always change.
16
- * 3. We rarely need `stateRoot`, only needed to verify that we importing a
17
- * block on the correct state.
18
- * 4. We rarely need intermediate trie nodes, only needed for answering warp-sync (CE129)
19
- * and producing state proofs.
20
- *
21
- * The trie for JAM is designed so that every node takes exactly 64 bytes. Values
22
- * that don't fit directly into a trie node (larger than 32bytes) are hash-referenced
23
- * there.
24
- * Thanks to this, we can estimate that for each and every state, all of the leaf nodes
25
- * "should not take a lot". Let's assume that we have:
26
- * 1. 999 services
27
- * 2. Each service has 50 preimages, so also roughly 50 preimage lookups.
28
- * 3. On top of that we have 899 storage entries (arbitrary size)
29
- * 4. That gives us roughly 1000 trie leaves for each service (50 + 50 + 899 + 1(info))
30
- * 5. We have 15 always-present storage entries (let's round it up to 1k)
31
- *
32
- * Hence in total we should have 999 * 1000 + 15 ~ 1M leaves.
33
- * So for every state, all of the leaves should occupy roughly `64B * 1M = 64MB`.
34
- * Large values that do not change, are automatically deduplicated via hash-references.
35
- *
36
- * It seems very sensible then to:
37
- * 1. Just store trie leaves for each state.
38
- * 2. Prune old (finalized) states (otherwise 24h of states = 14400 blocks * 64MB ~ 1TB)
39
- * 3. Recompute the trie in-memory only if needed.
40
- *
41
- * Obviously from just the leaf nodes we can easily obtain the entire state.
42
- *
43
- * That's the approach we are implementing here (for now without pruning:)).
44
- *
45
- * Since when referencing some state we always think of it in terms of a posterior
46
- * state produced by some block, we use `HeaderHash` to index the leaf collections.
47
- *
48
- * The state root (if needed) can be easily recomputed by merkelizing the leaves.
49
- *
50
- * We might need to implement some reference counting for values, or at least
51
- * know the last state that is referencing them, so that they can be purged from
52
- * the `values` database when they are not needed any more.
53
- *
54
- * TODO [ToDr] To implement when actually needed:
55
- * - [ ] state pruning on finality / pre-defined window (warp threshold?)
56
- * - [ ] removing unused values
57
- */
58
- export declare class LmdbStates implements StatesDb<SerializedState<LeafDb>>, InitStatesDb<StateEntries> {
59
- private readonly spec;
60
- private readonly blake2b;
61
- private readonly root;
62
- private readonly states;
63
- private readonly values;
64
- static new(spec: ChainSpec, blake2b: Blake2b, root: LmdbRoot): LmdbStates;
65
- private constructor();
66
- insertInitialState(headerHash: HeaderHash, serializedState: StateEntries): Promise<Result<OK, StateUpdateError>>;
67
- private updateAndCommit;
68
- updateAndSetState(headerHash: HeaderHash, state: SerializedState<LeafDb>, update: Partial<State & ServicesUpdate>): Promise<Result<OK, StateUpdateError>>;
69
- getStateRoot(state: SerializedState<LeafDb>): Promise<StateRootHash>;
70
- getState(root: HeaderHash): SerializedState<LeafDb> | null;
71
- commitFinalized(_headers: HeaderHash[]): void;
72
- markUnused(header: HeaderHash): void;
73
- diskSizeInBytes(): number | null;
74
- close(): Promise<void>;
75
- }
76
- //# sourceMappingURL=states.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"states.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-lmdb/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,EAAE,KAAK,YAAY,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,gBAAgB,EAAe,MAAM,qBAAqB,CAAC;AAC9G,OAAO,KAAK,EAAE,OAAO,EAAiB,MAAM,iBAAiB,CAAC;AAE9D,OAAO,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAY,MAAM,gCAAgC,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAgD,MAAM,gCAAgC,CAAC;AAG/G,OAAO,EAAE,EAAE,EAAE,MAAM,EAAkB,MAAM,kBAAkB,CAAC;AAC9D,OAAO,KAAK,EAAE,QAAQ,EAAS,MAAM,WAAW,CAAC;AAGjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,qBAAa,UAAW,YAAW,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC;IAS5F,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAVvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAE/B,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ;IAI5D,OAAO;IASD,kBAAkB,CACtB,UAAU,EAAE,UAAU,EACtB,eAAe,EAAE,YAAY,GAC5B,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,gBAAgB,CAAC,CAAC;YAQ1B,eAAe;IA0BvB,iBAAiB,CACrB,UAAU,EAAE,UAAU,EACtB,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;IAWlC,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IAI1E,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,IAAI;IAsB1D,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,IAAI;IAM7C,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAIpC,eAAe,IAAI,MAAM,GAAG,IAAI;IAI1B,KAAK;CAGZ"}
@@ -1,145 +0,0 @@
1
- import { BytesBlob } from "#@typeberry/bytes";
2
- import { SortedSet } from "#@typeberry/collections";
3
- import { LeafDb, StateUpdateError, updateLeafs } from "#@typeberry/database";
4
- import { Logger } from "#@typeberry/logger";
5
- import { SerializedState, StateEntryUpdateAction, serializeStateUpdate } from "#@typeberry/state-merkleization";
6
- import { leafComparator } from "#@typeberry/trie";
7
- import { OK, Result, resultToString } from "#@typeberry/utils";
8
- const logger = Logger.new(import.meta.filename, "db");
9
- /**
10
- * LMDB-backed state storage.
11
- *
12
- * Assumptions:
13
- * 1. For each block we end up with some posterior state.
14
- * 2. Majority of state entries will stay the same, however there are some that always change.
15
- * 3. We rarely need `stateRoot`, only needed to verify that we importing a
16
- * block on the correct state.
17
- * 4. We rarely need intermediate trie nodes, only needed for answering warp-sync (CE129)
18
- * and producing state proofs.
19
- *
20
- * The trie for JAM is designed so that every node takes exactly 64 bytes. Values
21
- * that don't fit directly into a trie node (larger than 32bytes) are hash-referenced
22
- * there.
23
- * Thanks to this, we can estimate that for each and every state, all of the leaf nodes
24
- * "should not take a lot". Let's assume that we have:
25
- * 1. 999 services
26
- * 2. Each service has 50 preimages, so also roughly 50 preimage lookups.
27
- * 3. On top of that we have 899 storage entries (arbitrary size)
28
- * 4. That gives us roughly 1000 trie leaves for each service (50 + 50 + 899 + 1(info))
29
- * 5. We have 15 always-present storage entries (let's round it up to 1k)
30
- *
31
- * Hence in total we should have 999 * 1000 + 15 ~ 1M leaves.
32
- * So for every state, all of the leaves should occupy roughly `64B * 1M = 64MB`.
33
- * Large values that do not change, are automatically deduplicated via hash-references.
34
- *
35
- * It seems very sensible then to:
36
- * 1. Just store trie leaves for each state.
37
- * 2. Prune old (finalized) states (otherwise 24h of states = 14400 blocks * 64MB ~ 1TB)
38
- * 3. Recompute the trie in-memory only if needed.
39
- *
40
- * Obviously from just the leaf nodes we can easily obtain the entire state.
41
- *
42
- * That's the approach we are implementing here (for now without pruning:)).
43
- *
44
- * Since when referencing some state we always think of it in terms of a posterior
45
- * state produced by some block, we use `HeaderHash` to index the leaf collections.
46
- *
47
- * The state root (if needed) can be easily recomputed by merkelizing the leaves.
48
- *
49
- * We might need to implement some reference counting for values, or at least
50
- * know the last state that is referencing them, so that they can be purged from
51
- * the `values` database when they are not needed any more.
52
- *
53
- * TODO [ToDr] To implement when actually needed:
54
- * - [ ] state pruning on finality / pre-defined window (warp threshold?)
55
- * - [ ] removing unused values
56
- */
57
- export class LmdbStates {
58
- spec;
59
- blake2b;
60
- root;
61
- states;
62
- values;
63
- static new(spec, blake2b, root) {
64
- return new LmdbStates(spec, blake2b, root);
65
- }
66
- constructor(spec, blake2b, root) {
67
- this.spec = spec;
68
- this.blake2b = blake2b;
69
- this.root = root;
70
- this.states = this.root.subDb("states");
71
- this.values = this.root.subDb("values");
72
- }
73
- async insertInitialState(headerHash, serializedState) {
74
- return await this.updateAndCommit(headerHash, SortedSet.fromArray(leafComparator, []), Array.from(serializedState, (x) => [StateEntryUpdateAction.Insert, x[0], x[1]]));
75
- }
76
- async updateAndCommit(headerHash, leafs, data) {
77
- const { values } = updateLeafs(leafs, this.blake2b, data);
78
- // TODO [ToDr] could be optimized to already have leaves written to one big chunk
79
- // (we could pre-allocate one buffer for all the leafs)
80
- const stateLeafs = BytesBlob.blobFromParts(leafs.array.map((x) => x.node.raw));
81
- // now we have the leaves and the values, so let's write it down to the DB.
82
- const statesWrite = this.states.put(headerHash.raw, stateLeafs.raw);
83
- const valuesWrite = this.values.transaction(() => {
84
- for (const [hash, val] of values) {
85
- this.values.put(hash.raw, val.raw);
86
- }
87
- });
88
- try {
89
- await Promise.all([valuesWrite, statesWrite]);
90
- }
91
- catch (e) {
92
- logger.error `${e}`;
93
- return Result.error(StateUpdateError.Commit, () => `Failed to commit state update: ${e}`);
94
- }
95
- return Result.ok(OK);
96
- }
97
- async updateAndSetState(headerHash, state, update) {
98
- const updatedValues = serializeStateUpdate(this.spec, this.blake2b, update);
99
- // and finally we insert new values and store leaves in the DB.
100
- const res = await this.updateAndCommit(headerHash, state.backend.leafs, updatedValues);
101
- if (res.isOk) {
102
- // update the internal backend with new leaves.
103
- state.updateBackend(LeafDb.fromLeaves(state.backend.leafs, state.backend.db));
104
- }
105
- return res;
106
- }
107
- async getStateRoot(state) {
108
- return state.backend.getStateRoot(this.blake2b);
109
- }
110
- getState(root) {
111
- const leafNodes = this.states.get(root.raw);
112
- // we don't have that particular state.
113
- if (leafNodes === undefined) {
114
- return null;
115
- }
116
- const values = this.values;
117
- const leafDbResult = LeafDb.fromLeavesBlob(BytesBlob.blobFrom(leafNodes), {
118
- get(key) {
119
- const val = values.get(key.raw);
120
- if (val === undefined) {
121
- throw new Error(`Missing required value: ${key} in the DB`);
122
- }
123
- return val;
124
- },
125
- });
126
- if (leafDbResult.isError) {
127
- throw new Error(`Inconsistent DB. Invalid leaf nodes for ${root}: ${resultToString(leafDbResult)}`);
128
- }
129
- return SerializedState.new(this.spec, this.blake2b, leafDbResult.ok);
130
- }
131
- commitFinalized(_headers) {
132
- // Values are never pruned here. This db survives restarts, so refcounting
133
- // would need counts persisted (and crash-consistent) alongside the values.
134
- // See the TODO above - not implemented until actually needed.
135
- }
136
- markUnused(header) {
137
- this.states.removeSync(header.raw);
138
- }
139
- diskSizeInBytes() {
140
- return this.root.sizeInBytes();
141
- }
142
- async close() {
143
- await Promise.all([this.states.close(), this.values.close()]);
144
- }
145
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=states.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"states.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-lmdb/states.test.ts"],"names":[],"mappings":""}