@typeberry/lib 0.9.0-959889e → 0.9.0-a723180
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 +1 -1
- package/packages/core/collections/sorted-array.d.ts +1 -1
- package/packages/core/collections/sorted-array.d.ts.map +1 -1
- package/packages/core/collections/sorted-array.js +4 -2
- package/packages/core/collections/sorted-array.test.js +10 -0
- package/packages/core/collections/sorted-set.d.ts +1 -1
- package/packages/core/collections/sorted-set.d.ts.map +1 -1
- package/packages/core/collections/sorted-set.js +6 -2
- package/packages/core/collections/sorted-set.test.js +24 -0
- package/packages/core/utils/shutdown.d.ts +3 -3
- package/packages/core/utils/shutdown.d.ts.map +1 -1
- package/packages/core/utils/shutdown.js +2 -79
- package/packages/core/utils/shutdown.node.d.ts +5 -0
- package/packages/core/utils/shutdown.node.d.ts.map +1 -0
- package/packages/core/utils/shutdown.node.js +76 -0
- package/packages/core/utils/shutdown.test.js +32 -23
- package/packages/jam/config-node/node-config.d.ts +20 -2
- package/packages/jam/config-node/node-config.d.ts.map +1 -1
- package/packages/jam/config-node/node-config.js +25 -3
- package/packages/jam/config-node/node-config.test.js +8 -1
- package/packages/jam/database/index.d.ts +1 -0
- package/packages/jam/database/index.d.ts.map +1 -1
- package/packages/jam/database/index.js +1 -0
- package/packages/jam/database/leaf-db-update.d.ts +1 -0
- package/packages/jam/database/leaf-db-update.d.ts.map +1 -1
- package/packages/jam/database/leaf-db-update.js +22 -5
- package/packages/jam/database/leaf-db-update.test.d.ts +2 -0
- package/packages/jam/database/leaf-db-update.test.d.ts.map +1 -0
- package/packages/jam/database/leaf-db-update.test.js +81 -0
- package/packages/jam/database/serialized-states-db.d.ts +4 -0
- package/packages/jam/database/serialized-states-db.d.ts.map +1 -1
- package/packages/jam/database/serialized-states-db.js +18 -2
- package/packages/jam/database/serialized-states-db.test.d.ts +2 -0
- package/packages/jam/database/serialized-states-db.test.d.ts.map +1 -0
- package/packages/jam/database/serialized-states-db.test.js +109 -0
- package/packages/jam/database/states.d.ts +11 -0
- package/packages/jam/database/states.d.ts.map +1 -1
- package/packages/jam/database/states.js +3 -0
- package/packages/jam/database/value-refs.d.ts +117 -0
- package/packages/jam/database/value-refs.d.ts.map +1 -0
- package/packages/jam/database/value-refs.js +206 -0
- package/packages/jam/database/value-refs.test.d.ts +2 -0
- package/packages/jam/database/value-refs.test.d.ts.map +1 -0
- package/packages/jam/database/value-refs.test.js +165 -0
- package/packages/jam/database-fjall/blocks.d.ts +25 -0
- package/packages/jam/database-fjall/blocks.d.ts.map +1 -0
- package/packages/jam/database-fjall/blocks.js +86 -0
- package/packages/jam/database-fjall/blocks.test.d.ts +2 -0
- package/packages/jam/database-fjall/blocks.test.d.ts.map +1 -0
- package/packages/jam/database-fjall/blocks.test.js +62 -0
- package/packages/jam/database-fjall/hybrid-states.d.ts +14 -0
- package/packages/jam/database-fjall/hybrid-states.d.ts.map +1 -1
- package/packages/jam/database-fjall/hybrid-states.js +55 -6
- package/packages/jam/database-fjall/hybrid-states.test.js +94 -2
- package/packages/jam/database-fjall/index.d.ts +2 -0
- package/packages/jam/database-fjall/index.d.ts.map +1 -1
- package/packages/jam/database-fjall/index.js +2 -0
- package/packages/jam/database-fjall/root.d.ts +14 -8
- package/packages/jam/database-fjall/root.d.ts.map +1 -1
- package/packages/jam/database-fjall/root.js +25 -9
- package/packages/jam/database-fjall/states.d.ts +31 -0
- package/packages/jam/database-fjall/states.d.ts.map +1 -0
- package/packages/jam/database-fjall/states.js +99 -0
- package/packages/jam/database-fjall/states.test.d.ts +2 -0
- package/packages/jam/database-fjall/states.test.d.ts.map +1 -0
- package/packages/jam/database-fjall/states.test.js +150 -0
- package/packages/jam/database-lmdb/hybrid-states.d.ts +12 -0
- package/packages/jam/database-lmdb/hybrid-states.d.ts.map +1 -1
- package/packages/jam/database-lmdb/hybrid-states.js +45 -4
- package/packages/jam/database-lmdb/hybrid-states.test.js +94 -2
- package/packages/jam/database-lmdb/states.d.ts +1 -0
- package/packages/jam/database-lmdb/states.d.ts.map +1 -1
- package/packages/jam/database-lmdb/states.js +5 -0
- package/packages/jam/node/export.d.ts.map +1 -1
- package/packages/jam/node/export.js +18 -9
- package/packages/jam/node/main-fuzz.d.ts.map +1 -1
- package/packages/jam/node/main-fuzz.js +85 -71
- package/packages/jam/node/main-importer.d.ts +4 -2
- package/packages/jam/node/main-importer.d.ts.map +1 -1
- package/packages/jam/node/main-importer.js +26 -12
- package/packages/jam/node/main.d.ts.map +1 -1
- package/packages/jam/node/main.js +42 -15
- package/packages/jam/node/workers.d.ts +4 -4
- package/packages/jam/node/workers.d.ts.map +1 -1
- package/packages/workers/api/config.d.ts +2 -2
- package/packages/workers/api/config.d.ts.map +1 -1
- package/packages/workers/api/config.js +1 -1
- package/packages/workers/api/port.d.ts +5 -1
- package/packages/workers/api/port.d.ts.map +1 -1
- package/packages/workers/api/port.js +56 -13
- package/packages/workers/api/port.test.js +35 -0
- package/packages/workers/api-node/config.d.ts +45 -4
- package/packages/workers/api-node/config.d.ts.map +1 -1
- package/packages/workers/api-node/config.js +127 -9
- package/packages/workers/api-node/config.test.js +52 -3
- package/packages/workers/api-node/port.d.ts +3 -0
- package/packages/workers/api-node/port.d.ts.map +1 -1
- package/packages/workers/api-node/port.js +31 -2
- package/packages/workers/api-node/port.test.js +17 -0
- package/packages/workers/api-node/protocol.d.ts +3 -3
- package/packages/workers/api-node/protocol.d.ts.map +1 -1
- package/packages/workers/api-node/protocol.js +18 -12
- package/packages/workers/block-authorship/main.js +1 -1
- package/packages/workers/importer/finality.d.ts +4 -0
- package/packages/workers/importer/finality.d.ts.map +1 -1
- package/packages/workers/importer/finality.js +10 -1
- package/packages/workers/importer/finality.test.js +6 -0
- package/packages/workers/importer/importer.d.ts.map +1 -1
- package/packages/workers/importer/importer.js +3 -0
- package/packages/workers/importer/main.d.ts +1 -1
- package/packages/workers/importer/main.d.ts.map +1 -1
- package/packages/workers/importer/main.js +1 -1
- package/packages/workers/jam-network/main.js +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"value-refs.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database/value-refs.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { Bytes } from "#@typeberry/bytes";
|
|
4
|
+
import { HASH_SIZE } from "#@typeberry/hash";
|
|
5
|
+
import { InMemoryValueRefsStore, isEmptyUpdate, ValueRefs, } from "./value-refs.js";
|
|
6
|
+
function vh(n) {
|
|
7
|
+
return Bytes.fill(HASH_SIZE, n).asOpaque();
|
|
8
|
+
}
|
|
9
|
+
function hh(n) {
|
|
10
|
+
return Bytes.fill(HASH_SIZE, n).asOpaque();
|
|
11
|
+
}
|
|
12
|
+
/** Tracks which value hashes are present in the (simulated) values DB. */
|
|
13
|
+
class TrackingValues {
|
|
14
|
+
present = new Set();
|
|
15
|
+
write(h) {
|
|
16
|
+
this.present.add(h.toString());
|
|
17
|
+
}
|
|
18
|
+
delete(h) {
|
|
19
|
+
this.present.delete(h.toString());
|
|
20
|
+
}
|
|
21
|
+
has(h) {
|
|
22
|
+
return this.present.has(h.toString());
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function setup() {
|
|
26
|
+
const store = new InMemoryValueRefsStore();
|
|
27
|
+
const refs = new ValueRefs(store);
|
|
28
|
+
const values = new TrackingValues();
|
|
29
|
+
// what every backend is expected to do with an update
|
|
30
|
+
const apply = (update) => {
|
|
31
|
+
store.apply(update);
|
|
32
|
+
for (const v of update.removeValues) {
|
|
33
|
+
values.delete(v);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return { refs, store, values, apply };
|
|
37
|
+
}
|
|
38
|
+
/** Simulate importing a block: write inserted values + record the delta. */
|
|
39
|
+
function importBlock({ refs, values, apply }, header, delta) {
|
|
40
|
+
const full = { inserted: delta.inserted ?? [], removed: delta.removed ?? [] };
|
|
41
|
+
for (const v of full.inserted) {
|
|
42
|
+
values.write(v);
|
|
43
|
+
}
|
|
44
|
+
apply(refs.onImport(header, full));
|
|
45
|
+
}
|
|
46
|
+
describe("ValueRefs", () => {
|
|
47
|
+
it("deletes a value once its only finalized reference is removed", () => {
|
|
48
|
+
const s = setup();
|
|
49
|
+
const { refs, values, apply } = s;
|
|
50
|
+
const V = vh(1);
|
|
51
|
+
importBlock(s, hh(1), { inserted: [V] });
|
|
52
|
+
apply(refs.commitFinalized([hh(1)]));
|
|
53
|
+
assert.ok(values.has(V), "kept while referenced by finalized tip");
|
|
54
|
+
importBlock(s, hh(2), { removed: [V] });
|
|
55
|
+
apply(refs.commitFinalized([hh(2)]));
|
|
56
|
+
assert.ok(!values.has(V), "deleted after last finalized reference removed");
|
|
57
|
+
});
|
|
58
|
+
it("keeps a value referenced under multiple keys until all references are gone", () => {
|
|
59
|
+
const s = setup();
|
|
60
|
+
const { refs, values, apply } = s;
|
|
61
|
+
const V = vh(1);
|
|
62
|
+
importBlock(s, hh(1), { inserted: [V] });
|
|
63
|
+
apply(refs.commitFinalized([hh(1)]));
|
|
64
|
+
importBlock(s, hh(2), { inserted: [V] });
|
|
65
|
+
apply(refs.commitFinalized([hh(2)]));
|
|
66
|
+
importBlock(s, hh(3), { removed: [V] });
|
|
67
|
+
apply(refs.commitFinalized([hh(3)]));
|
|
68
|
+
assert.ok(values.has(V), "kept while one reference remains");
|
|
69
|
+
importBlock(s, hh(4), { removed: [V] });
|
|
70
|
+
apply(refs.commitFinalized([hh(4)]));
|
|
71
|
+
assert.ok(!values.has(V), "deleted after the last reference");
|
|
72
|
+
});
|
|
73
|
+
it("keeps a value removed on the finalized chain while an unfinalized fork re-adds it", () => {
|
|
74
|
+
const s = setup();
|
|
75
|
+
const { refs, values, apply } = s;
|
|
76
|
+
const V = vh(1);
|
|
77
|
+
// genesis already references V
|
|
78
|
+
apply(refs.onInitial([V]));
|
|
79
|
+
values.write(V);
|
|
80
|
+
// finalized chain removes V
|
|
81
|
+
importBlock(s, hh(10), { removed: [V] });
|
|
82
|
+
// an unfinalized fork re-adds V
|
|
83
|
+
importBlock(s, hh(20), { inserted: [V] });
|
|
84
|
+
apply(refs.commitFinalized([hh(10)]));
|
|
85
|
+
assert.ok(values.has(V), "kept because an unfinalized fork still references V");
|
|
86
|
+
});
|
|
87
|
+
it("collects a value that lived only on a discarded fork", () => {
|
|
88
|
+
const s = setup();
|
|
89
|
+
const { refs, values, apply } = s;
|
|
90
|
+
const W = vh(2);
|
|
91
|
+
importBlock(s, hh(30), { inserted: [W] });
|
|
92
|
+
assert.ok(values.has(W));
|
|
93
|
+
const update = refs.releaseUnfinalized(hh(30));
|
|
94
|
+
apply(update);
|
|
95
|
+
assert.strictEqual(isEmptyUpdate(update), false);
|
|
96
|
+
assert.ok(!values.has(W), "dead-fork-only value is collected");
|
|
97
|
+
});
|
|
98
|
+
it("does not touch finalized references when releasing an already finalized header", () => {
|
|
99
|
+
const s = setup();
|
|
100
|
+
const { refs, values, apply } = s;
|
|
101
|
+
const V = vh(1);
|
|
102
|
+
importBlock(s, hh(1), { inserted: [V] });
|
|
103
|
+
apply(refs.commitFinalized([hh(1)]));
|
|
104
|
+
const update = refs.releaseUnfinalized(hh(1));
|
|
105
|
+
apply(update);
|
|
106
|
+
assert.strictEqual(isEmptyUpdate(update), true, "header was already finalized");
|
|
107
|
+
assert.ok(values.has(V), "finalized value is untouched");
|
|
108
|
+
});
|
|
109
|
+
it("ignores unknown headers in commitFinalized", () => {
|
|
110
|
+
const { refs } = setup();
|
|
111
|
+
const update = refs.commitFinalized([hh(99)]);
|
|
112
|
+
assert.strictEqual(isEmptyUpdate(update), true);
|
|
113
|
+
});
|
|
114
|
+
it("ignores a duplicate import of the same header", () => {
|
|
115
|
+
const s = setup();
|
|
116
|
+
const { refs, values, apply } = s;
|
|
117
|
+
const V = vh(1);
|
|
118
|
+
importBlock(s, hh(1), { inserted: [V] });
|
|
119
|
+
const second = refs.onImport(hh(1), { inserted: [V], removed: [] });
|
|
120
|
+
assert.strictEqual(isEmptyUpdate(second), true, "duplicate import is a no-op");
|
|
121
|
+
// had the duplicate bumped `pending` twice, releasing the fork would leave
|
|
122
|
+
// a dangling count and the value would never be collected
|
|
123
|
+
apply(refs.releaseUnfinalized(hh(1)));
|
|
124
|
+
assert.ok(!values.has(V));
|
|
125
|
+
});
|
|
126
|
+
it("counts a header only once within a single commitFinalized call", () => {
|
|
127
|
+
const s = setup();
|
|
128
|
+
const { refs, values, apply } = s;
|
|
129
|
+
const V = vh(1);
|
|
130
|
+
importBlock(s, hh(1), { inserted: [V] });
|
|
131
|
+
apply(refs.commitFinalized([hh(1), hh(1)]));
|
|
132
|
+
importBlock(s, hh(2), { removed: [V] });
|
|
133
|
+
apply(refs.commitFinalized([hh(2)]));
|
|
134
|
+
assert.ok(!values.has(V), "a double-counted finalized reference would keep the value alive");
|
|
135
|
+
});
|
|
136
|
+
it("does not remove a value re-referenced later within the same batch", () => {
|
|
137
|
+
const s = setup();
|
|
138
|
+
const { refs, values, apply } = s;
|
|
139
|
+
const V = vh(1);
|
|
140
|
+
apply(refs.onInitial([V]));
|
|
141
|
+
values.write(V);
|
|
142
|
+
// one block removes V, its descendant re-adds it; both finalize at once
|
|
143
|
+
importBlock(s, hh(1), { removed: [V] });
|
|
144
|
+
importBlock(s, hh(2), { inserted: [V] });
|
|
145
|
+
const update = refs.commitFinalized([hh(1), hh(2)]);
|
|
146
|
+
apply(update);
|
|
147
|
+
assert.strictEqual(update.removeValues.length, 0);
|
|
148
|
+
assert.ok(values.has(V), "the re-added reference keeps the value alive");
|
|
149
|
+
});
|
|
150
|
+
it("emits absolute counts so re-applying an update is harmless", () => {
|
|
151
|
+
const s = setup();
|
|
152
|
+
const { refs, store, values, apply } = s;
|
|
153
|
+
const V = vh(1);
|
|
154
|
+
importBlock(s, hh(1), { inserted: [V] });
|
|
155
|
+
const update = refs.commitFinalized([hh(1)]);
|
|
156
|
+
apply(update);
|
|
157
|
+
// crash-replay: same batch applied again
|
|
158
|
+
apply(update);
|
|
159
|
+
assert.strictEqual(store.getFinalizedCount(V), 1);
|
|
160
|
+
assert.strictEqual(store.getPendingCount(V), 0);
|
|
161
|
+
importBlock(s, hh(2), { removed: [V] });
|
|
162
|
+
apply(refs.commitFinalized([hh(2)]));
|
|
163
|
+
assert.ok(!values.has(V), "refcounting still exact after replay");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type BlockView, type ExtrinsicView, type HeaderHash, type HeaderView, type StateRootHash } from "#@typeberry/block";
|
|
2
|
+
import type { ChainSpec } from "#@typeberry/config";
|
|
3
|
+
import type { BlocksDb } from "#@typeberry/database/blocks.js";
|
|
4
|
+
import { type WithHash } from "#@typeberry/hash";
|
|
5
|
+
import { type FjallRoot } from "./root.js";
|
|
6
|
+
/** fjall-backed block storage. */
|
|
7
|
+
export declare class FjallBlocks implements BlocksDb {
|
|
8
|
+
private readonly chainSpec;
|
|
9
|
+
private readonly root;
|
|
10
|
+
private readonly headers;
|
|
11
|
+
private readonly extrinsics;
|
|
12
|
+
private readonly postStateRoots;
|
|
13
|
+
static open(chainSpec: ChainSpec, root: FjallRoot): Promise<FjallBlocks>;
|
|
14
|
+
private constructor();
|
|
15
|
+
setPostStateRoot(hash: HeaderHash, postStateRoot: StateRootHash): Promise<void>;
|
|
16
|
+
getPostStateRoot(hash: HeaderHash): StateRootHash | null;
|
|
17
|
+
insertBlock(block: WithHash<HeaderHash, BlockView>): Promise<void>;
|
|
18
|
+
setBestHeaderHash(hash: HeaderHash): Promise<void>;
|
|
19
|
+
getBestHeaderHash(): HeaderHash;
|
|
20
|
+
getHeader(hash: HeaderHash): HeaderView | null;
|
|
21
|
+
getExtrinsic(hash: HeaderHash): ExtrinsicView | null;
|
|
22
|
+
markUnused(hash: HeaderHash): void;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=blocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-fjall/blocks.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EAEd,KAAK,aAAa,EAElB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3D,OAAO,EAAuB,KAAK,SAAS,EAAgC,MAAM,WAAW,CAAC;AAK9F,kCAAkC;AAClC,qBAAa,WAAY,YAAW,QAAQ;IAWxC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,cAAc;WAdpB,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IAS9E,OAAO;IAQD,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,GAAG,IAAI;IAQlD,WAAW,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlE,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxD,iBAAiB,IAAI,UAAU;IAQ/B,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI;IAQ9C,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa,GAAG,IAAI;IAQpD,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAQ5B,KAAK;CACZ"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Extrinsic, Header, } from "#@typeberry/block";
|
|
2
|
+
import { Bytes } from "#@typeberry/bytes";
|
|
3
|
+
import { Decoder } from "#@typeberry/codec";
|
|
4
|
+
import { HASH_SIZE } from "#@typeberry/hash";
|
|
5
|
+
import { Logger } from "#@typeberry/logger";
|
|
6
|
+
import { toUint8Array } from "./root.js";
|
|
7
|
+
const BEST_BLOCK = new TextEncoder().encode("best hash and posterior state root");
|
|
8
|
+
const logger = Logger.new(import.meta.filename, "db");
|
|
9
|
+
/** fjall-backed block storage. */
|
|
10
|
+
export class FjallBlocks {
|
|
11
|
+
chainSpec;
|
|
12
|
+
root;
|
|
13
|
+
headers;
|
|
14
|
+
extrinsics;
|
|
15
|
+
postStateRoots;
|
|
16
|
+
static async open(chainSpec, root) {
|
|
17
|
+
const [headers, extrinsics, postStateRoots] = await Promise.all([
|
|
18
|
+
root.partition("headers"),
|
|
19
|
+
root.partition("extrinsics"),
|
|
20
|
+
root.partition("postStateRoots"),
|
|
21
|
+
]);
|
|
22
|
+
return new FjallBlocks(chainSpec, root, headers, extrinsics, postStateRoots);
|
|
23
|
+
}
|
|
24
|
+
constructor(chainSpec, root, headers, extrinsics, postStateRoots) {
|
|
25
|
+
this.chainSpec = chainSpec;
|
|
26
|
+
this.root = root;
|
|
27
|
+
this.headers = headers;
|
|
28
|
+
this.extrinsics = extrinsics;
|
|
29
|
+
this.postStateRoots = postStateRoots;
|
|
30
|
+
}
|
|
31
|
+
async setPostStateRoot(hash, postStateRoot) {
|
|
32
|
+
await writable(this.postStateRoots, this.root).insert(hash.raw, postStateRoot.raw);
|
|
33
|
+
}
|
|
34
|
+
getPostStateRoot(hash) {
|
|
35
|
+
const postStateRoot = toUint8Array(this.postStateRoots.get(hash.raw));
|
|
36
|
+
if (postStateRoot === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return Bytes.fromBlob(postStateRoot, HASH_SIZE).asOpaque();
|
|
40
|
+
}
|
|
41
|
+
async insertBlock(block) {
|
|
42
|
+
await Promise.all([
|
|
43
|
+
writable(this.headers, this.root).insert(block.hash.raw, block.data.header.view().encoded().raw),
|
|
44
|
+
writable(this.extrinsics, this.root).insert(block.hash.raw, block.data.extrinsic.view().encoded().raw),
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
async setBestHeaderHash(hash) {
|
|
48
|
+
await writable(this.headers, this.root).insert(BEST_BLOCK, hash.raw);
|
|
49
|
+
await this.root.persist();
|
|
50
|
+
}
|
|
51
|
+
getBestHeaderHash() {
|
|
52
|
+
const bestHeaderHash = toUint8Array(this.headers.get(BEST_BLOCK));
|
|
53
|
+
if (bestHeaderHash === null) {
|
|
54
|
+
return Bytes.zero(HASH_SIZE).asOpaque();
|
|
55
|
+
}
|
|
56
|
+
return Bytes.fromBlob(bestHeaderHash, HASH_SIZE).asOpaque();
|
|
57
|
+
}
|
|
58
|
+
getHeader(hash) {
|
|
59
|
+
const data = toUint8Array(this.headers.get(hash.raw));
|
|
60
|
+
if (data === null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return Decoder.decodeObject(Header.Codec.View, data, this.chainSpec);
|
|
64
|
+
}
|
|
65
|
+
getExtrinsic(hash) {
|
|
66
|
+
const data = toUint8Array(this.extrinsics.get(hash.raw));
|
|
67
|
+
if (data === null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return Decoder.decodeObject(Extrinsic.Codec.View, data, this.chainSpec);
|
|
71
|
+
}
|
|
72
|
+
markUnused(hash) {
|
|
73
|
+
void Promise.all([
|
|
74
|
+
writable(this.headers, this.root).remove(hash.raw),
|
|
75
|
+
writable(this.extrinsics, this.root).remove(hash.raw),
|
|
76
|
+
writable(this.postStateRoots, this.root).remove(hash.raw),
|
|
77
|
+
]).catch((e) => logger.warn `Failed to prune block ${hash}: ${e}`);
|
|
78
|
+
}
|
|
79
|
+
async close() { }
|
|
80
|
+
}
|
|
81
|
+
function writable(partition, root) {
|
|
82
|
+
if (root.readOnly) {
|
|
83
|
+
throw new Error("Cannot write through a read-only fjall partition.");
|
|
84
|
+
}
|
|
85
|
+
return partition;
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocks.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-fjall/blocks.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { testBlockView } from "#@typeberry/block/test-helpers.js";
|
|
5
|
+
import { Bytes } from "#@typeberry/bytes";
|
|
6
|
+
import { tinyChainSpec } from "#@typeberry/config";
|
|
7
|
+
import { Blake2b, HASH_SIZE, WithHash } from "#@typeberry/hash";
|
|
8
|
+
import { FjallBlocks } from "./blocks.js";
|
|
9
|
+
import { FjallRoot } from "./root.js";
|
|
10
|
+
let blake2b;
|
|
11
|
+
before(async () => {
|
|
12
|
+
blake2b = await Blake2b.createHasher();
|
|
13
|
+
});
|
|
14
|
+
describe("Fjall blocks database", () => {
|
|
15
|
+
let tmpDir = "";
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = fs.mkdtempSync("typeberry-fjall-blocks-");
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
it("sets and retrieves the best header hash", async () => {
|
|
23
|
+
const root = await FjallRoot.open(tmpDir, { ephemeral: true });
|
|
24
|
+
const blocks = await FjallBlocks.open(tinyChainSpec, root);
|
|
25
|
+
try {
|
|
26
|
+
await blocks.setBestHeaderHash(Bytes.fill(HASH_SIZE, 5).asOpaque());
|
|
27
|
+
assert.strictEqual(blocks.getBestHeaderHash().toString(), "0x0505050505050505050505050505050505050505050505050505050505050505");
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await blocks.close();
|
|
31
|
+
await root.close();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
it("sets and retrieves post state roots", async () => {
|
|
35
|
+
const root = await FjallRoot.open(tmpDir, { ephemeral: true });
|
|
36
|
+
const blocks = await FjallBlocks.open(tinyChainSpec, root);
|
|
37
|
+
try {
|
|
38
|
+
await blocks.setPostStateRoot(Bytes.fill(HASH_SIZE, 5).asOpaque(), Bytes.fill(HASH_SIZE, 10).asOpaque());
|
|
39
|
+
assert.strictEqual(blocks.getPostStateRoot(Bytes.fill(HASH_SIZE, 1).asOpaque())?.toString(), undefined);
|
|
40
|
+
assert.strictEqual(blocks.getPostStateRoot(Bytes.fill(HASH_SIZE, 5).asOpaque())?.toString(), "0x0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a");
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await blocks.close();
|
|
44
|
+
await root.close();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it("stores and retrieves a block", async () => {
|
|
48
|
+
const root = await FjallRoot.open(tmpDir, { ephemeral: true });
|
|
49
|
+
const blocks = await FjallBlocks.open(tinyChainSpec, root);
|
|
50
|
+
try {
|
|
51
|
+
const block = testBlockView();
|
|
52
|
+
const headerHash = blake2b.hashBytes(block.header.view().encoded()).asOpaque();
|
|
53
|
+
await blocks.insertBlock(WithHash.new(headerHash, block));
|
|
54
|
+
assert.deepStrictEqual(blocks.getHeader(headerHash)?.materialize(), block.header.materialize());
|
|
55
|
+
assert.deepStrictEqual(blocks.getExtrinsic(headerHash)?.materialize(), block.extrinsic.materialize());
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
await blocks.close();
|
|
59
|
+
await root.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -39,6 +39,14 @@ export declare class FjallValuesSession {
|
|
|
39
39
|
*
|
|
40
40
|
* Construction is async, and value writes are flushed explicitly, because fjall
|
|
41
41
|
* has no transaction primitive.
|
|
42
|
+
*
|
|
43
|
+
* Values that no longer belong to any surviving state are removed from fjall,
|
|
44
|
+
* decided by in-memory refcounting (`ValueRefs`) driven by the importer's
|
|
45
|
+
* finality signal. Counts are not persisted: this db cannot resume from disk
|
|
46
|
+
* anyway (the leaf sets live in memory), so values left over by a previous run
|
|
47
|
+
* are never collected. An instance backed by a shared session (fuzz reset
|
|
48
|
+
* reuse) only ever prunes values it inserted itself, since its refcounts start
|
|
49
|
+
* empty - values left behind by earlier resets stay untouched.
|
|
42
50
|
*/
|
|
43
51
|
export declare class HybridSerializedStates implements StatesDb<SerializedState<LeafDb>>, InitStatesDb<StateEntries> {
|
|
44
52
|
private readonly spec;
|
|
@@ -66,13 +74,19 @@ export declare class HybridSerializedStates implements StatesDb<SerializedState<
|
|
|
66
74
|
private readonly valuesDb;
|
|
67
75
|
/** Shared content-addressed values partition (owned by `session`). */
|
|
68
76
|
private readonly values;
|
|
77
|
+
private readonly refsStore;
|
|
78
|
+
private readonly refs;
|
|
79
|
+
private pendingCleanup;
|
|
69
80
|
private constructor();
|
|
70
81
|
insertInitialState(headerHash: HeaderHash, entries: StateEntries): Promise<Result<OK, StateUpdateError>>;
|
|
71
82
|
updateAndSetState(header: HeaderHash, state: SerializedState<LeafDb>, update: Partial<State & ServicesUpdate>): Promise<Result<OK, StateUpdateError>>;
|
|
72
83
|
getStateRoot(state: SerializedState<LeafDb>): Promise<StateRootHash>;
|
|
73
84
|
getState(header: HeaderHash): SerializedState<LeafDb> | null;
|
|
85
|
+
commitFinalized(headers: HeaderHash[]): void;
|
|
74
86
|
markUnused(header: HeaderHash): void;
|
|
75
87
|
diskSizeInBytes(): number | null;
|
|
88
|
+
/** Apply a refcounting update and remove values that lost their last reference. */
|
|
89
|
+
private applyRefs;
|
|
76
90
|
close(): Promise<void>;
|
|
77
91
|
/** Write new large values to fjall in a single batch, then flush. */
|
|
78
92
|
private writeValues;
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"hybrid-states.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-fjall/hybrid-states.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HashDictionary, SortedSet } from "#@typeberry/collections";
|
|
2
|
-
import { LeafDb, StateUpdateError, updateLeafs, } from "#@typeberry/database";
|
|
2
|
+
import { InMemoryValueRefsStore, LeafDb, StateUpdateError, updateLeafs, ValueRefs, } from "#@typeberry/database";
|
|
3
3
|
import { Logger } from "#@typeberry/logger";
|
|
4
4
|
import { SerializedState, StateEntryUpdateAction, serializeStateUpdate, } from "#@typeberry/state-merkleization";
|
|
5
5
|
import { leafComparator } from "#@typeberry/trie";
|
|
@@ -27,9 +27,18 @@ export class FjallValuesSession {
|
|
|
27
27
|
}
|
|
28
28
|
/** Open (or create) the keyspace at `dbPath` and its `values` partition. */
|
|
29
29
|
static async open(dbPath, options = {}) {
|
|
30
|
+
if (options.readOnly === true) {
|
|
31
|
+
throw new Error("FjallValuesSession requires a writable keyspace.");
|
|
32
|
+
}
|
|
30
33
|
const root = await FjallRoot.open(dbPath, options);
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
try {
|
|
35
|
+
const values = await root.writablePartition("values");
|
|
36
|
+
return new FjallValuesSession(root, values);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
await root.close();
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
/** Flush the journal to disk (a no-op for ephemeral keyspaces). */
|
|
35
44
|
async persist() {
|
|
@@ -53,6 +62,14 @@ export class FjallValuesSession {
|
|
|
53
62
|
*
|
|
54
63
|
* Construction is async, and value writes are flushed explicitly, because fjall
|
|
55
64
|
* has no transaction primitive.
|
|
65
|
+
*
|
|
66
|
+
* Values that no longer belong to any surviving state are removed from fjall,
|
|
67
|
+
* decided by in-memory refcounting (`ValueRefs`) driven by the importer's
|
|
68
|
+
* finality signal. Counts are not persisted: this db cannot resume from disk
|
|
69
|
+
* anyway (the leaf sets live in memory), so values left over by a previous run
|
|
70
|
+
* are never collected. An instance backed by a shared session (fuzz reset
|
|
71
|
+
* reuse) only ever prunes values it inserted itself, since its refcounts start
|
|
72
|
+
* empty - values left behind by earlier resets stay untouched.
|
|
56
73
|
*/
|
|
57
74
|
export class HybridSerializedStates {
|
|
58
75
|
spec;
|
|
@@ -80,6 +97,10 @@ export class HybridSerializedStates {
|
|
|
80
97
|
valuesDb;
|
|
81
98
|
/** Shared content-addressed values partition (owned by `session`). */
|
|
82
99
|
values;
|
|
100
|
+
refsStore = new InMemoryValueRefsStore();
|
|
101
|
+
refs = new ValueRefs(this.refsStore);
|
|
102
|
+
// Queue of not-yet-committed value removals, awaited on close.
|
|
103
|
+
pendingCleanup = Promise.resolve();
|
|
83
104
|
constructor(spec, blake2b, session,
|
|
84
105
|
/** Whether `close()` should close the underlying session. */
|
|
85
106
|
ownsSession) {
|
|
@@ -97,13 +118,14 @@ export class HybridSerializedStates {
|
|
|
97
118
|
return res;
|
|
98
119
|
}
|
|
99
120
|
this.inMemStates.set(headerHash, leafs);
|
|
121
|
+
this.applyRefs(this.refs.onInitial(values.map((v) => v[0])));
|
|
100
122
|
return Result.ok(OK);
|
|
101
123
|
}
|
|
102
124
|
async updateAndSetState(header, state, update) {
|
|
103
125
|
const updatedValues = serializeStateUpdate(this.spec, this.blake2b, update);
|
|
104
126
|
// Clone the leaf set before mutating: the previous state keeps using its own.
|
|
105
127
|
const newLeafs = SortedSet.fromSortedArray(leafComparator, state.backend.leafs.array);
|
|
106
|
-
const { values, leafs } = updateLeafs(newLeafs, this.blake2b, updatedValues);
|
|
128
|
+
const { values, removed, leafs } = updateLeafs(newLeafs, this.blake2b, updatedValues);
|
|
107
129
|
const res = await this.writeValues(values);
|
|
108
130
|
if (res.isError) {
|
|
109
131
|
// Leave the caller's state untouched: its new leaves would reference
|
|
@@ -114,6 +136,7 @@ export class HybridSerializedStates {
|
|
|
114
136
|
// values are durably written.
|
|
115
137
|
state.updateBackend(LeafDb.fromLeaves(leafs, this.valuesDb));
|
|
116
138
|
this.inMemStates.set(header, leafs);
|
|
139
|
+
this.applyRefs(this.refs.onImport(header, { inserted: values.map((v) => v[0]), removed }));
|
|
117
140
|
return Result.ok(OK);
|
|
118
141
|
}
|
|
119
142
|
async getStateRoot(state) {
|
|
@@ -127,15 +150,36 @@ export class HybridSerializedStates {
|
|
|
127
150
|
const leafDb = LeafDb.fromLeaves(leafs, this.valuesDb);
|
|
128
151
|
return SerializedState.new(this.spec, this.blake2b, leafDb);
|
|
129
152
|
}
|
|
153
|
+
commitFinalized(headers) {
|
|
154
|
+
this.applyRefs(this.refs.commitFinalized(headers));
|
|
155
|
+
}
|
|
130
156
|
markUnused(header) {
|
|
131
|
-
//
|
|
132
|
-
//
|
|
157
|
+
// Release the speculative references first (a no-op for finalized states,
|
|
158
|
+
// whose deltas were already consumed by `commitFinalized`).
|
|
159
|
+
this.applyRefs(this.refs.releaseUnfinalized(header));
|
|
133
160
|
this.inMemStates.delete(header);
|
|
134
161
|
}
|
|
135
162
|
diskSizeInBytes() {
|
|
136
163
|
return this.session.sizeInBytes();
|
|
137
164
|
}
|
|
165
|
+
/** Apply a refcounting update and remove values that lost their last reference. */
|
|
166
|
+
applyRefs(update) {
|
|
167
|
+
this.refsStore.apply(update);
|
|
168
|
+
if (update.removeValues.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Queued, not awaited: a failed removal only leaks a value. No explicit
|
|
172
|
+
// persist - removals are flushed together with the next value write.
|
|
173
|
+
this.pendingCleanup = this.pendingCleanup
|
|
174
|
+
.then(() => Promise.all(update.removeValues.map((v) => this.values.remove(v.raw))))
|
|
175
|
+
.catch((e) => {
|
|
176
|
+
logger.error `Failed to remove unreferenced values: ${e}`;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
138
179
|
async close() {
|
|
180
|
+
// Finish any queued value removals before tearing down (or releasing) the
|
|
181
|
+
// session - they operate on the shared `values` partition.
|
|
182
|
+
await this.pendingCleanup;
|
|
139
183
|
// Instances backed by a shared session (fuzz reset reuse) keep the keyspace
|
|
140
184
|
// open for the next reset. The session owner closes it once.
|
|
141
185
|
if (this.ownsSession) {
|
|
@@ -148,6 +192,11 @@ export class HybridSerializedStates {
|
|
|
148
192
|
return Result.ok(OK);
|
|
149
193
|
}
|
|
150
194
|
try {
|
|
195
|
+
// Flush queued removals first: a pending cleanup might be about to delete
|
|
196
|
+
// a content hash we are re-inserting now. Writing behind it keeps the
|
|
197
|
+
// re-inserted value on disk (removals are only queued at refcount 0, so
|
|
198
|
+
// none can be queued for a value still referenced).
|
|
199
|
+
await this.pendingCleanup;
|
|
151
200
|
const entries = values.map(([hash, val]) => ({ key: hash.raw, value: val.raw }));
|
|
152
201
|
await this.values.insertBatch(entries);
|
|
153
202
|
await this.session.persist();
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { afterEach, before, beforeEach, describe, it } from "node:test";
|
|
4
|
+
import { tryAsServiceId } from "#@typeberry/block";
|
|
4
5
|
import { Bytes, BytesBlob } from "#@typeberry/bytes";
|
|
5
6
|
import { tinyChainSpec } from "#@typeberry/config";
|
|
6
7
|
import { Blake2b, HASH_SIZE } from "#@typeberry/hash";
|
|
7
|
-
import { InMemoryState } from "#@typeberry/state";
|
|
8
|
+
import { InMemoryState, StorageItem, UpdateStorage, } from "#@typeberry/state";
|
|
9
|
+
import { testState } from "#@typeberry/state/test.utils.js";
|
|
8
10
|
import { StateEntries } from "#@typeberry/state-merkleization";
|
|
9
|
-
import { deepEqual, OK, Result } from "#@typeberry/utils";
|
|
11
|
+
import { asOpaqueType, deepEqual, OK, Result } from "#@typeberry/utils";
|
|
10
12
|
import { FjallValuesSession, HybridSerializedStates } from "./hybrid-states.js";
|
|
11
13
|
let blake2b;
|
|
12
14
|
before(async () => {
|
|
@@ -111,3 +113,93 @@ describe("Fjall hybrid serialized states", () => {
|
|
|
111
113
|
}
|
|
112
114
|
});
|
|
113
115
|
});
|
|
116
|
+
function hh(n) {
|
|
117
|
+
return Bytes.fill(HASH_SIZE, n).asOpaque();
|
|
118
|
+
}
|
|
119
|
+
const storageKey = asOpaqueType(BytesBlob.blobFromString("test-key"));
|
|
120
|
+
/** A state update writing a single large (non-embedded) value under `storageKey`. */
|
|
121
|
+
function storageUpdate(value) {
|
|
122
|
+
const item = StorageItem.create({ key: storageKey, value: BytesBlob.blobFromString(value) });
|
|
123
|
+
return {
|
|
124
|
+
storage: new Map([[tryAsServiceId(0), [UpdateStorage.set({ storage: item })]]]),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// > 32 bytes => stored in the values db (not embedded in the leaf).
|
|
128
|
+
const BIG_1 = "a".repeat(100);
|
|
129
|
+
const BIG_2 = "b".repeat(100);
|
|
130
|
+
/** `true` if every value referenced by the state can still be resolved. */
|
|
131
|
+
function canReadFully(state) {
|
|
132
|
+
if (state === null) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
state.backend.intoStateEntries();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Value removals are queued, so poll for the expected outcome. */
|
|
144
|
+
async function eventually(check, what, timeoutMs = 5_000) {
|
|
145
|
+
const start = Date.now();
|
|
146
|
+
while (!check()) {
|
|
147
|
+
if (Date.now() - start > timeoutMs) {
|
|
148
|
+
assert.fail(`Timed out waiting for: ${what}`);
|
|
149
|
+
}
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
describe("Fjall hybrid serialized states value refcounting", () => {
|
|
154
|
+
const spec = tinyChainSpec;
|
|
155
|
+
let dbPath = "";
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
dbPath = createTempDir();
|
|
158
|
+
});
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
fs.rmSync(dbPath, { recursive: true });
|
|
161
|
+
});
|
|
162
|
+
it("removes a replaced value from disk once the replacement finalizes", async () => {
|
|
163
|
+
const states = await HybridSerializedStates.new({ spec, blake2b, dbPath });
|
|
164
|
+
try {
|
|
165
|
+
await states.insertInitialState(hh(0), StateEntries.serializeInMemory(spec, blake2b, testState()));
|
|
166
|
+
const s0 = states.getState(hh(0));
|
|
167
|
+
assert.ok(s0 !== null);
|
|
168
|
+
await states.updateAndSetState(hh(1), s0, storageUpdate(BIG_1));
|
|
169
|
+
// a handle to the post-1 state, surviving the pruning below
|
|
170
|
+
const stale1 = states.getState(hh(1));
|
|
171
|
+
const s1 = states.getState(hh(1));
|
|
172
|
+
assert.ok(s1 !== null);
|
|
173
|
+
await states.updateAndSetState(hh(2), s1, storageUpdate(BIG_2));
|
|
174
|
+
states.commitFinalized([hh(1)]);
|
|
175
|
+
assert.ok(canReadFully(stale1), "still referenced by the finalized tip");
|
|
176
|
+
states.commitFinalized([hh(2)]);
|
|
177
|
+
await eventually(() => !canReadFully(stale1), "replaced value removed from fjall");
|
|
178
|
+
assert.ok(canReadFully(states.getState(hh(2))), "the new finalized tip stays fully readable");
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await states.close();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
it("collects values of a pruned dead fork and keeps the surviving chain intact", async () => {
|
|
185
|
+
const states = await HybridSerializedStates.new({ spec, blake2b, dbPath });
|
|
186
|
+
try {
|
|
187
|
+
await states.insertInitialState(hh(0), StateEntries.serializeInMemory(spec, blake2b, testState()));
|
|
188
|
+
const s0 = states.getState(hh(0));
|
|
189
|
+
assert.ok(s0 !== null);
|
|
190
|
+
await states.updateAndSetState(hh(1), s0, storageUpdate(BIG_1));
|
|
191
|
+
// a dead fork on top of genesis, inserting a different value
|
|
192
|
+
const fork = states.getState(hh(0));
|
|
193
|
+
assert.ok(fork !== null);
|
|
194
|
+
await states.updateAndSetState(hh(0xaa), fork, storageUpdate(BIG_2));
|
|
195
|
+
const staleFork = states.getState(hh(0xaa));
|
|
196
|
+
states.markUnused(hh(0xaa));
|
|
197
|
+
assert.strictEqual(states.getState(hh(0xaa)), null);
|
|
198
|
+
await eventually(() => !canReadFully(staleFork), "fork-only value removed from fjall");
|
|
199
|
+
assert.ok(canReadFully(states.getState(hh(1))), "surviving chain is unaffected");
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
await states.close();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -1 +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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-fjall/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|