@typeberry/lib 0.8.4 → 0.9.0-4a9f363

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 (214) hide show
  1. package/package.json +6 -4
  2. package/packages/configs/index.d.ts +30 -1
  3. package/packages/configs/index.d.ts.map +1 -1
  4. package/packages/configs/index.js +4 -2
  5. package/packages/configs/typeberry-dev-full.json +29 -0
  6. package/packages/core/bytes/bytes.d.ts +1 -0
  7. package/packages/core/bytes/bytes.d.ts.map +1 -1
  8. package/packages/core/bytes/bytes.js +8 -0
  9. package/packages/core/collections/sorted-array.d.ts +1 -1
  10. package/packages/core/collections/sorted-array.d.ts.map +1 -1
  11. package/packages/core/collections/sorted-array.js +4 -2
  12. package/packages/core/collections/sorted-array.test.js +10 -0
  13. package/packages/core/collections/sorted-set.d.ts +1 -1
  14. package/packages/core/collections/sorted-set.d.ts.map +1 -1
  15. package/packages/core/collections/sorted-set.js +6 -2
  16. package/packages/core/collections/sorted-set.test.js +24 -0
  17. package/packages/core/utils/debug.d.ts +4 -2
  18. package/packages/core/utils/debug.d.ts.map +1 -1
  19. package/packages/core/utils/debug.js +18 -13
  20. package/packages/core/utils/debug.test.js +12 -6
  21. package/packages/core/utils/index.d.ts +1 -0
  22. package/packages/core/utils/index.d.ts.map +1 -1
  23. package/packages/core/utils/index.js +1 -0
  24. package/packages/core/utils/shutdown.d.ts +18 -0
  25. package/packages/core/utils/shutdown.d.ts.map +1 -0
  26. package/packages/core/utils/shutdown.js +3 -0
  27. package/packages/core/utils/shutdown.node.d.ts +5 -0
  28. package/packages/core/utils/shutdown.node.d.ts.map +1 -0
  29. package/packages/core/utils/shutdown.node.js +76 -0
  30. package/packages/core/utils/shutdown.test.d.ts +2 -0
  31. package/packages/core/utils/shutdown.test.d.ts.map +1 -0
  32. package/packages/core/utils/shutdown.test.js +164 -0
  33. package/packages/extensions/ipc/server.d.ts.map +1 -1
  34. package/packages/extensions/ipc/server.js +7 -0
  35. package/packages/jam/config-node/node-config.d.ts +22 -3
  36. package/packages/jam/config-node/node-config.d.ts.map +1 -1
  37. package/packages/jam/config-node/node-config.js +33 -6
  38. package/packages/jam/config-node/node-config.test.js +11 -4
  39. package/packages/jam/database/index.d.ts +1 -0
  40. package/packages/jam/database/index.d.ts.map +1 -1
  41. package/packages/jam/database/index.js +1 -0
  42. package/packages/jam/database/leaf-db-update.d.ts +1 -0
  43. package/packages/jam/database/leaf-db-update.d.ts.map +1 -1
  44. package/packages/jam/database/leaf-db-update.js +22 -5
  45. package/packages/jam/database/leaf-db-update.test.d.ts +2 -0
  46. package/packages/jam/database/leaf-db-update.test.d.ts.map +1 -0
  47. package/packages/jam/database/leaf-db-update.test.js +81 -0
  48. package/packages/jam/database/serialized-states-db.d.ts +1 -0
  49. package/packages/jam/database/serialized-states-db.d.ts.map +1 -1
  50. package/packages/jam/database/serialized-states-db.js +4 -0
  51. package/packages/jam/database/states.d.ts +11 -0
  52. package/packages/jam/database/states.d.ts.map +1 -1
  53. package/packages/jam/database/states.js +3 -0
  54. package/packages/jam/database/value-refs.d.ts +117 -0
  55. package/packages/jam/database/value-refs.d.ts.map +1 -0
  56. package/packages/jam/database/value-refs.js +206 -0
  57. package/packages/jam/database/value-refs.test.d.ts +2 -0
  58. package/packages/jam/database/value-refs.test.d.ts.map +1 -0
  59. package/packages/jam/database/value-refs.test.js +165 -0
  60. package/packages/jam/database-fjall/blocks.d.ts +25 -0
  61. package/packages/jam/database-fjall/blocks.d.ts.map +1 -0
  62. package/packages/jam/database-fjall/blocks.js +86 -0
  63. package/packages/jam/database-fjall/blocks.test.d.ts +2 -0
  64. package/packages/jam/database-fjall/blocks.test.d.ts.map +1 -0
  65. package/packages/jam/database-fjall/blocks.test.js +62 -0
  66. package/packages/jam/database-fjall/hybrid-states.d.ts +96 -0
  67. package/packages/jam/database-fjall/hybrid-states.d.ts.map +1 -0
  68. package/packages/jam/database-fjall/hybrid-states.js +218 -0
  69. package/packages/jam/database-fjall/hybrid-states.test.d.ts +2 -0
  70. package/packages/jam/database-fjall/hybrid-states.test.d.ts.map +1 -0
  71. package/packages/jam/database-fjall/hybrid-states.test.js +113 -0
  72. package/packages/jam/database-fjall/index.d.ts +5 -0
  73. package/packages/jam/database-fjall/index.d.ts.map +1 -0
  74. package/packages/jam/database-fjall/index.js +4 -0
  75. package/packages/jam/database-fjall/root.d.ts +64 -0
  76. package/packages/jam/database-fjall/root.d.ts.map +1 -0
  77. package/packages/jam/database-fjall/root.js +105 -0
  78. package/packages/jam/database-fjall/states.d.ts +31 -0
  79. package/packages/jam/database-fjall/states.d.ts.map +1 -0
  80. package/packages/jam/database-fjall/states.js +99 -0
  81. package/packages/jam/database-fjall/states.test.d.ts +2 -0
  82. package/packages/jam/database-fjall/states.test.d.ts.map +1 -0
  83. package/packages/jam/database-fjall/states.test.js +150 -0
  84. package/packages/jam/database-lmdb/hybrid-states.d.ts +12 -0
  85. package/packages/jam/database-lmdb/hybrid-states.d.ts.map +1 -1
  86. package/packages/jam/database-lmdb/hybrid-states.js +45 -4
  87. package/packages/jam/database-lmdb/states.d.ts +1 -0
  88. package/packages/jam/database-lmdb/states.d.ts.map +1 -1
  89. package/packages/jam/database-lmdb/states.js +5 -0
  90. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts +18 -10
  91. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts.map +1 -1
  92. package/packages/jam/jamnp-s/tasks/ticket-distribution.js +44 -68
  93. package/packages/jam/jamnp-s/tasks/ticket-distribution.test.js +30 -8
  94. package/packages/jam/node/export.d.ts.map +1 -1
  95. package/packages/jam/node/export.js +18 -9
  96. package/packages/jam/node/main-fuzz.d.ts +4 -1
  97. package/packages/jam/node/main-fuzz.d.ts.map +1 -1
  98. package/packages/jam/node/main-fuzz.js +142 -55
  99. package/packages/jam/node/main-importer.d.ts +15 -3
  100. package/packages/jam/node/main-importer.d.ts.map +1 -1
  101. package/packages/jam/node/main-importer.js +30 -14
  102. package/packages/jam/node/main.d.ts.map +1 -1
  103. package/packages/jam/node/main.js +42 -15
  104. package/packages/jam/node/workers.d.ts +4 -4
  105. package/packages/jam/node/workers.d.ts.map +1 -1
  106. package/packages/jam/safrole/bandersnatch-vrf.d.ts +24 -4
  107. package/packages/jam/safrole/bandersnatch-vrf.d.ts.map +1 -1
  108. package/packages/jam/safrole/bandersnatch-vrf.js +63 -26
  109. package/packages/jam/safrole/bandersnatch-vrf.test.js +12 -9
  110. package/packages/jam/safrole/bandersnatch-wasm.d.ts +10 -0
  111. package/packages/jam/safrole/bandersnatch-wasm.d.ts.map +1 -1
  112. package/packages/jam/safrole/bandersnatch-wasm.js +12 -0
  113. package/packages/jam/safrole/safrole.js +5 -5
  114. package/packages/jam/safrole/safrole.test.js +13 -13
  115. package/packages/jam/ticket-pool/index.d.ts +4 -0
  116. package/packages/jam/ticket-pool/index.d.ts.map +1 -0
  117. package/packages/jam/ticket-pool/index.js +3 -0
  118. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts +30 -0
  119. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts.map +1 -0
  120. package/packages/jam/ticket-pool/pending-ticket-pool.js +56 -0
  121. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts +2 -0
  122. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts.map +1 -0
  123. package/packages/jam/ticket-pool/pending-ticket-pool.test.js +67 -0
  124. package/packages/jam/ticket-pool/ticket-validator.d.ts +47 -0
  125. package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
  126. package/packages/jam/ticket-pool/ticket-validator.js +34 -0
  127. package/packages/jam/ticket-pool/ticket-validator.test.d.ts +2 -0
  128. package/packages/jam/ticket-pool/ticket-validator.test.d.ts.map +1 -0
  129. package/packages/jam/ticket-pool/ticket-validator.test.js +35 -0
  130. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +26 -0
  131. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
  132. package/packages/jam/ticket-pool/verified-ticket-pool.js +41 -0
  133. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts +2 -0
  134. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts.map +1 -0
  135. package/packages/jam/ticket-pool/verified-ticket-pool.test.js +54 -0
  136. package/packages/workers/api/config.d.ts +2 -2
  137. package/packages/workers/api/config.d.ts.map +1 -1
  138. package/packages/workers/api/config.js +1 -1
  139. package/packages/workers/api/port.d.ts +5 -1
  140. package/packages/workers/api/port.d.ts.map +1 -1
  141. package/packages/workers/api/port.js +56 -13
  142. package/packages/workers/api/port.test.js +35 -0
  143. package/packages/workers/api-node/config.d.ts +65 -9
  144. package/packages/workers/api-node/config.d.ts.map +1 -1
  145. package/packages/workers/api-node/config.js +151 -26
  146. package/packages/workers/api-node/config.test.js +88 -2
  147. package/packages/workers/api-node/port.d.ts +3 -0
  148. package/packages/workers/api-node/port.d.ts.map +1 -1
  149. package/packages/workers/api-node/port.js +31 -2
  150. package/packages/workers/api-node/port.test.js +17 -0
  151. package/packages/workers/api-node/protocol.d.ts +3 -3
  152. package/packages/workers/api-node/protocol.d.ts.map +1 -1
  153. package/packages/workers/api-node/protocol.js +18 -12
  154. package/packages/workers/block-authorship/{generator.d.ts → block-generator.d.ts} +5 -5
  155. package/packages/workers/block-authorship/block-generator.d.ts.map +1 -0
  156. package/packages/workers/block-authorship/{generator.js → block-generator.js} +3 -3
  157. package/packages/workers/block-authorship/block-generator.test.d.ts +2 -0
  158. package/packages/workers/block-authorship/block-generator.test.d.ts.map +1 -0
  159. package/packages/workers/block-authorship/{generator.test.js → block-generator.test.js} +8 -8
  160. package/packages/workers/block-authorship/epoch-authoring-slots.d.ts +35 -0
  161. package/packages/workers/block-authorship/epoch-authoring-slots.d.ts.map +1 -0
  162. package/packages/workers/block-authorship/epoch-authoring-slots.js +86 -0
  163. package/packages/workers/block-authorship/epoch-tracker.d.ts +29 -0
  164. package/packages/workers/block-authorship/epoch-tracker.d.ts.map +1 -0
  165. package/packages/workers/block-authorship/epoch-tracker.js +80 -0
  166. package/packages/workers/block-authorship/index.d.ts.map +1 -1
  167. package/packages/workers/block-authorship/index.js +1 -1
  168. package/packages/workers/block-authorship/main.d.ts +3 -0
  169. package/packages/workers/block-authorship/main.d.ts.map +1 -1
  170. package/packages/workers/block-authorship/main.js +198 -316
  171. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts +2 -0
  172. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts.map +1 -0
  173. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.js +23 -0
  174. package/packages/workers/block-authorship/ticket-generator/index.d.ts +16 -0
  175. package/packages/workers/block-authorship/ticket-generator/index.d.ts.map +1 -0
  176. package/packages/workers/block-authorship/ticket-generator/index.js +62 -0
  177. package/packages/workers/block-authorship/ticket-generator/protocol.d.ts +50 -0
  178. package/packages/workers/block-authorship/ticket-generator/protocol.d.ts.map +1 -0
  179. package/packages/workers/block-authorship/ticket-generator/protocol.js +54 -0
  180. package/packages/workers/block-authorship/{ticket-generator.d.ts → ticket-generator/ticket-generator.d.ts} +4 -0
  181. package/packages/workers/block-authorship/ticket-generator/ticket-generator.d.ts.map +1 -0
  182. package/packages/workers/block-authorship/{ticket-generator.js → ticket-generator/ticket-generator.js} +19 -9
  183. package/packages/workers/block-authorship/ticket-generator/ticket-generator.test.d.ts.map +1 -0
  184. package/packages/workers/block-authorship/{ticket-generator.test.js → ticket-generator/ticket-generator.test.js} +13 -9
  185. package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts +36 -0
  186. package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts.map +1 -0
  187. package/packages/workers/block-authorship/ticket-generator/worker-pool.js +111 -0
  188. package/packages/workers/block-authorship/ticket-validator.d.ts +31 -0
  189. package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
  190. package/packages/workers/block-authorship/ticket-validator.js +59 -0
  191. package/packages/workers/comms-authorship-network/protocol.d.ts +14 -4
  192. package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
  193. package/packages/workers/comms-authorship-network/protocol.js +12 -6
  194. package/packages/workers/comms-authorship-network/tickets-message.d.ts +0 -14
  195. package/packages/workers/comms-authorship-network/tickets-message.d.ts.map +1 -1
  196. package/packages/workers/comms-authorship-network/tickets-message.js +0 -17
  197. package/packages/workers/importer/importer.d.ts +2 -2
  198. package/packages/workers/importer/importer.d.ts.map +1 -1
  199. package/packages/workers/importer/importer.js +5 -5
  200. package/packages/workers/importer/main.d.ts +1 -1
  201. package/packages/workers/importer/main.d.ts.map +1 -1
  202. package/packages/workers/importer/main.js +1 -1
  203. package/packages/workers/importer/stats.d.ts +1 -3
  204. package/packages/workers/importer/stats.d.ts.map +1 -1
  205. package/packages/workers/importer/stats.js +12 -12
  206. package/packages/workers/jam-network/main.d.ts.map +1 -1
  207. package/packages/workers/jam-network/main.js +26 -5
  208. package/packages/workers/block-authorship/generator.d.ts.map +0 -1
  209. package/packages/workers/block-authorship/generator.test.d.ts +0 -2
  210. package/packages/workers/block-authorship/generator.test.d.ts.map +0 -1
  211. package/packages/workers/block-authorship/ticket-generator.d.ts.map +0 -1
  212. package/packages/workers/block-authorship/ticket-generator.test.d.ts.map +0 -1
  213. /package/packages/configs/{typeberry-dev.json → typeberry-dev-tiny.json} +0 -0
  214. /package/packages/workers/block-authorship/{ticket-generator.test.d.ts → ticket-generator/ticket-generator.test.d.ts} +0 -0
@@ -0,0 +1,150 @@
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 { tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "#@typeberry/block";
5
+ import { Bytes, BytesBlob } from "#@typeberry/bytes";
6
+ import { tinyChainSpec } from "#@typeberry/config";
7
+ import { Blake2b, HASH_SIZE } from "#@typeberry/hash";
8
+ import { tryAsU32, tryAsU64 } from "#@typeberry/numbers";
9
+ import { InMemoryState, LookupHistoryItem, PrivilegedServices, ServiceAccountInfo, tryAsLookupHistorySlots, tryAsPerCore, UpdateService, } from "#@typeberry/state";
10
+ import { testState } from "#@typeberry/state/test.utils.js";
11
+ import { StateEntries } from "#@typeberry/state-merkleization";
12
+ import { deepEqual, OK, Result } from "#@typeberry/utils";
13
+ import { FjallRoot } from "./root.js";
14
+ import { FjallStates } from "./states.js";
15
+ let blake2b;
16
+ before(async () => {
17
+ blake2b = await Blake2b.createHasher();
18
+ });
19
+ describe("Fjall states database", () => {
20
+ const headerHash = Bytes.zero(HASH_SIZE).asOpaque();
21
+ const spec = tinyChainSpec;
22
+ let tmpDir = "";
23
+ beforeEach(() => {
24
+ tmpDir = fs.mkdtempSync("typeberry-fjall-states-");
25
+ });
26
+ afterEach(() => {
27
+ fs.rmSync(tmpDir, { recursive: true, force: true });
28
+ });
29
+ it("imports and reads an empty state", async () => {
30
+ const root = await FjallRoot.open(tmpDir, { ephemeral: true });
31
+ const states = await FjallStates.open(spec, blake2b, root);
32
+ try {
33
+ const emptyState = InMemoryState.empty(spec);
34
+ const serialized = StateEntries.serializeInMemory(spec, blake2b, emptyState);
35
+ const emptyRoot = serialized.getRootHash(blake2b);
36
+ deepEqual(await states.insertInitialState(headerHash, serialized), Result.ok(OK));
37
+ const newState = states.getState(headerHash);
38
+ assert.ok(newState !== null);
39
+ assert.strictEqual(`${await states.getStateRoot(newState)}`, `${emptyRoot}`);
40
+ deepEqual(InMemoryState.copyFrom(spec, newState, new Map()), emptyState);
41
+ }
42
+ finally {
43
+ await states.close();
44
+ await root.close();
45
+ }
46
+ });
47
+ it("reads large values back from disk", async () => {
48
+ const root = await FjallRoot.open(tmpDir, { ephemeral: true });
49
+ const states = await FjallStates.open(spec, blake2b, root);
50
+ try {
51
+ const big1 = BytesBlob.blobFromString("x".repeat(100));
52
+ const big2 = BytesBlob.blobFromString("y".repeat(100));
53
+ const key1 = Bytes.fill(HASH_SIZE, 1).asOpaque();
54
+ const key2 = Bytes.fill(HASH_SIZE, 2).asOpaque();
55
+ const entries = StateEntries.fromEntriesUnsafe([
56
+ [key1, big1],
57
+ [key2, big2],
58
+ ]);
59
+ deepEqual(await states.insertInitialState(headerHash, entries), 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
+ await root.close();
68
+ }
69
+ });
70
+ it("updates state", async () => {
71
+ const root = await FjallRoot.open(tmpDir, { ephemeral: true });
72
+ const states = await FjallStates.open(spec, blake2b, root);
73
+ try {
74
+ const state = InMemoryState.empty(spec);
75
+ await states.insertInitialState(headerHash, StateEntries.serializeInMemory(spec, blake2b, state));
76
+ const newState = states.getState(headerHash);
77
+ assert.ok(newState !== null);
78
+ const headerHash2 = Bytes.fill(HASH_SIZE, 2).asOpaque();
79
+ const lookupHistory = LookupHistoryItem.new(Bytes.fill(HASH_SIZE, 0xff).asOpaque(), tryAsU32(5), tryAsLookupHistorySlots([]));
80
+ const stateUpdate = {
81
+ timeslot: tryAsTimeSlot(15),
82
+ privilegedServices: PrivilegedServices.create({
83
+ manager: tryAsServiceId(1),
84
+ assigners: tryAsPerCore(new Array(spec.coresCount).fill(tryAsServiceId(2)), spec),
85
+ delegator: tryAsServiceId(3),
86
+ registrar: tryAsServiceId(4),
87
+ autoAccumulateServices: new Map(),
88
+ }),
89
+ updated: new Map([
90
+ [
91
+ tryAsServiceId(1),
92
+ UpdateService.create({
93
+ serviceInfo: ServiceAccountInfo.create({
94
+ codeHash: Bytes.zero(HASH_SIZE).asOpaque(),
95
+ balance: tryAsU64(1_000_000),
96
+ accumulateMinGas: tryAsServiceGas(10_000),
97
+ onTransferMinGas: tryAsServiceGas(5_000),
98
+ storageUtilisationBytes: tryAsU64(1_000),
99
+ gratisStorage: tryAsU64(0),
100
+ storageUtilisationCount: tryAsU32(1),
101
+ created: tryAsTimeSlot(0),
102
+ lastAccumulation: tryAsTimeSlot(0),
103
+ parentService: tryAsServiceId(0),
104
+ }),
105
+ lookupHistory,
106
+ }),
107
+ ],
108
+ ]),
109
+ };
110
+ deepEqual(state.applyUpdate(stateUpdate), Result.ok(OK));
111
+ deepEqual(await states.updateAndSetState(headerHash2, newState, stateUpdate), Result.ok(OK));
112
+ const updatedState = states.getState(headerHash2);
113
+ assert.ok(updatedState !== null);
114
+ deepEqual(InMemoryState.copyFrom(spec, updatedState, new Map([
115
+ [
116
+ tryAsServiceId(1),
117
+ {
118
+ storageKeys: [],
119
+ preimages: [],
120
+ lookupHistory: [{ hash: lookupHistory.hash, length: lookupHistory.length }],
121
+ },
122
+ ],
123
+ ])), state);
124
+ assert.strictEqual(`${await states.getStateRoot(updatedState)}`, `${StateEntries.serializeInMemory(spec, blake2b, state).getRootHash(blake2b)}`);
125
+ }
126
+ finally {
127
+ await states.close();
128
+ await root.close();
129
+ }
130
+ });
131
+ it("imports a more complex state", async () => {
132
+ const root = await FjallRoot.open(tmpDir, { ephemeral: true });
133
+ const states = await FjallStates.open(spec, blake2b, root);
134
+ try {
135
+ const initialState = testState();
136
+ const initialService = initialState.services.get(tryAsServiceId(0));
137
+ assert.ok(initialService !== undefined);
138
+ const serialized = StateEntries.serializeInMemory(spec, blake2b, initialState);
139
+ deepEqual(await states.insertInitialState(headerHash, serialized), Result.ok(OK));
140
+ const newState = states.getState(headerHash);
141
+ assert.ok(newState !== null);
142
+ assert.strictEqual(`${await states.getStateRoot(newState)}`, `${serialized.getRootHash(blake2b)}`);
143
+ deepEqual(InMemoryState.copyFrom(spec, newState, new Map([[initialService.serviceId, initialService.getEntries()]])), initialState);
144
+ }
145
+ finally {
146
+ await states.close();
147
+ await root.close();
148
+ }
149
+ });
150
+ });
@@ -12,6 +12,12 @@ import { OK, Result } from "#@typeberry/utils";
12
12
  * Reads go straight to lmdb, which keeps its own page cache.
13
13
  * NOTE: this DB is designed for long fuzzing and to be used with pruning to
14
14
  * keep the heap usage bounded.
15
+ *
16
+ * Values that no longer belong to any surviving state are removed from lmdb,
17
+ * decided by in-memory refcounting (`ValueRefs`) driven by the importer's
18
+ * finality signal. Counts are not persisted: this db cannot resume from disk
19
+ * anyway (the leaf sets live in memory), so values left over by a previous
20
+ * run are never collected.
15
21
  */
16
22
  export declare class HybridSerializedStates implements StatesDb<SerializedState<LeafDb>>, InitStatesDb<StateEntries> {
17
23
  private readonly spec;
@@ -20,6 +26,9 @@ export declare class HybridSerializedStates implements StatesDb<SerializedState<
20
26
  private readonly inMemStates;
21
27
  private readonly lmdbValues;
22
28
  private readonly valuesDb;
29
+ private readonly refsStore;
30
+ private readonly refs;
31
+ private pendingCleanup;
23
32
  static new({ spec, blake2b, dbPath, readOnly, ephemeral, compression, }: {
24
33
  spec: ChainSpec;
25
34
  blake2b: Blake2b;
@@ -33,7 +42,10 @@ export declare class HybridSerializedStates implements StatesDb<SerializedState<
33
42
  updateAndSetState(header: HeaderHash, state: SerializedState<LeafDb>, update: Partial<State & ServicesUpdate>): Promise<Result<OK, StateUpdateError>>;
34
43
  getStateRoot(state: SerializedState<LeafDb>): Promise<StateRootHash>;
35
44
  getState(header: HeaderHash): SerializedState<LeafDb> | null;
45
+ commitFinalized(headers: HeaderHash[]): void;
36
46
  markUnused(header: HeaderHash): void;
47
+ /** Apply a refcounting update and remove values that lost their last reference. */
48
+ private applyRefs;
37
49
  diskSizeInBytes(): number | null;
38
50
  close(): Promise<void>;
39
51
  /** Write new large values to LMDB in one transaction. */
@@ -1 +1 @@
1
- {"version":3,"file":"hybrid-states.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-lmdb/hybrid-states.ts"],"names":[],"mappings":"AACA,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;AAC/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;AAG9C;;;;;;;GAOG;AACH,qBAAa,sBAAuB,YAAW,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC;IA0BxG,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IA3BvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyE;IACrG,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IAEnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IAEpC,MAAM,CAAC,GAAG,CAAC,EACT,IAAI,EACJ,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,WAAW,GACZ,EAAE;QACD,IAAI,EAAE,SAAS,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB;IAKD,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;IAKX,yDAAyD;YAC3C,WAAW;IAgBzB,8BAA8B;IAC9B,OAAO,CAAC,SAAS;CAOlB"}
1
+ {"version":3,"file":"hybrid-states.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/database-lmdb/hybrid-states.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGlE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,KAAK,YAAY,EAEjB,MAAM,EACN,KAAK,QAAQ,EACb,gBAAgB,EAKjB,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;;;;;;;;;;;;;GAaG;AACH,qBAAa,sBAAuB,YAAW,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,EAAE,YAAY,CAAC,YAAY,CAAC;IA8BxG,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IA/BvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyE;IACrG,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IAEnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAC1D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiC;IAEtD,OAAO,CAAC,cAAc,CAAuC;IAE7D,MAAM,CAAC,GAAG,CAAC,EACT,IAAI,EACJ,OAAO,EACP,MAAM,EACN,QAAQ,EACR,SAAS,EACT,WAAW,GACZ,EAAE;QACD,IAAI,EAAE,SAAS,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB;IAKD,OAAO;IASD,kBAAkB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,gBAAgB,CAAC,CAAC;IAexG,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;IAmBlC,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,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI;IAI5C,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAOpC,mFAAmF;IACnF,OAAO,CAAC,SAAS;IAmBjB,eAAe,IAAI,MAAM,GAAG,IAAI;IAI1B,KAAK;IAMX,yDAAyD;YAC3C,WAAW;IAqBzB,8BAA8B;IAC9B,OAAO,CAAC,SAAS;CAOlB"}
@@ -1,9 +1,11 @@
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
+ import { Logger } from "#@typeberry/logger";
3
4
  import { SerializedState, StateEntryUpdateAction, serializeStateUpdate, } from "#@typeberry/state-merkleization";
4
5
  import { leafComparator } from "#@typeberry/trie";
5
6
  import { OK, Result } from "#@typeberry/utils";
6
7
  import { LmdbRoot } from "./root.js";
8
+ const logger = Logger.new(import.meta.filename, "db");
7
9
  /**
8
10
  * Hybrid serialized-states db.
9
11
  *
@@ -11,6 +13,12 @@ import { LmdbRoot } from "./root.js";
11
13
  * Reads go straight to lmdb, which keeps its own page cache.
12
14
  * NOTE: this DB is designed for long fuzzing and to be used with pruning to
13
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.
14
22
  */
15
23
  export class HybridSerializedStates {
16
24
  spec;
@@ -20,6 +28,10 @@ export class HybridSerializedStates {
20
28
  lmdbValues;
21
29
  // A single shared values accessor reused by every `LeafDb` we hand out.
22
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();
23
35
  static new({ spec, blake2b, dbPath, readOnly, ephemeral, compression, }) {
24
36
  const root = LmdbRoot.new(dbPath, { readOnly, ephemeral, compression });
25
37
  return new HybridSerializedStates(spec, blake2b, root);
@@ -38,13 +50,14 @@ export class HybridSerializedStates {
38
50
  return res;
39
51
  }
40
52
  this.inMemStates.set(headerHash, leafs);
53
+ this.applyRefs(this.refs.onInitial(values.map((v) => v[0])));
41
54
  return Result.ok(OK);
42
55
  }
43
56
  async updateAndSetState(header, state, update) {
44
57
  const updatedValues = serializeStateUpdate(this.spec, this.blake2b, update);
45
58
  // Clone the leaf set before mutating: the previous state keeps using its own.
46
59
  const newLeafs = SortedSet.fromSortedArray(leafComparator, state.backend.leafs.array);
47
- const { values, leafs } = updateLeafs(newLeafs, this.blake2b, updatedValues);
60
+ const { values, removed, leafs } = updateLeafs(newLeafs, this.blake2b, updatedValues);
48
61
  const res = await this.writeValues(values);
49
62
  if (res.isError) {
50
63
  // Leave the caller's state untouched: its new leaves would reference
@@ -55,6 +68,7 @@ export class HybridSerializedStates {
55
68
  // values are durably written.
56
69
  state.updateBackend(LeafDb.fromLeaves(leafs, this.valuesDb));
57
70
  this.inMemStates.set(header, leafs);
71
+ this.applyRefs(this.refs.onImport(header, { inserted: values.map((v) => v[0]), removed }));
58
72
  return Result.ok(OK);
59
73
  }
60
74
  async getStateRoot(state) {
@@ -68,15 +82,37 @@ export class HybridSerializedStates {
68
82
  const leafDb = LeafDb.fromLeaves(leafs, this.valuesDb);
69
83
  return SerializedState.new(this.spec, this.blake2b, leafDb);
70
84
  }
85
+ commitFinalized(headers) {
86
+ this.applyRefs(this.refs.commitFinalized(headers));
87
+ }
71
88
  markUnused(header) {
72
- // We only remove the state from memory - values are not pruned at all,
73
- // but since they are stored on disk we should be safe.
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));
74
92
  this.inMemStates.delete(header);
75
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
+ }
76
111
  diskSizeInBytes() {
77
112
  return this.root.sizeInBytes();
78
113
  }
79
114
  async close() {
115
+ await this.pendingCleanup;
80
116
  await this.lmdbValues.close();
81
117
  await this.root.close();
82
118
  }
@@ -86,6 +122,11 @@ export class HybridSerializedStates {
86
122
  return Result.ok(OK);
87
123
  }
88
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;
89
130
  await this.lmdbValues.transaction(() => {
90
131
  for (const [hash, val] of values) {
91
132
  this.lmdbValues.put(hash.raw, val.raw);
@@ -68,6 +68,7 @@ export declare class LmdbStates implements StatesDb<SerializedState<LeafDb>>, In
68
68
  updateAndSetState(headerHash: HeaderHash, state: SerializedState<LeafDb>, update: Partial<State & ServicesUpdate>): Promise<Result<OK, StateUpdateError>>;
69
69
  getStateRoot(state: SerializedState<LeafDb>): Promise<StateRootHash>;
70
70
  getState(root: HeaderHash): SerializedState<LeafDb> | null;
71
+ commitFinalized(_headers: HeaderHash[]): void;
71
72
  markUnused(header: HeaderHash): void;
72
73
  diskSizeInBytes(): number | null;
73
74
  close(): Promise<void>;
@@ -1 +1 @@
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,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAIpC,eAAe,IAAI,MAAM,GAAG,IAAI;IAI1B,KAAK;CAGZ"}
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"}
@@ -128,6 +128,11 @@ export class LmdbStates {
128
128
  }
129
129
  return SerializedState.new(this.spec, this.blake2b, leafDbResult.ok);
130
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
+ }
131
136
  markUnused(header) {
132
137
  this.states.removeSync(header.raw);
133
138
  }
@@ -1,6 +1,7 @@
1
1
  import type { Epoch } from "#@typeberry/block";
2
2
  import type { SignedTicket } from "#@typeberry/block/tickets.js";
3
3
  import type { ChainSpec } from "#@typeberry/config";
4
+ import { type TicketValidator } from "#@typeberry/ticket-pool";
4
5
  import type { Connections } from "../peers.js";
5
6
  import type { StreamManager } from "../stream-manager.js";
6
7
  /**
@@ -9,33 +10,40 @@ import type { StreamManager } from "../stream-manager.js";
9
10
  * Uses CE-132 (proxy-to-all) for direct broadcast to all peers.
10
11
  * Implements a maintain pattern similar to SyncTask: tickets are collected
11
12
  * and periodically distributed to peers that haven't received them yet.
13
+ *
14
+ * Incoming tickets from peers are first run through a {@link TicketValidator};
15
+ * only validated tickets are added to the redistribution pool. The default
16
+ * validator denies everything, so callers must wire a real one via
17
+ * {@link setTicketValidator} before any networked ticket can be redistributed.
12
18
  */
13
19
  export declare class TicketDistributionTask {
14
20
  private readonly streamManager;
15
21
  private readonly connections;
16
22
  static start(streamManager: StreamManager, connections: Connections, chainSpec: ChainSpec): TicketDistributionTask;
17
- /** Pending tickets waiting to be distributed to peers */
18
- private pendingTickets;
19
- /** Current epoch being tracked (cleared when epoch changes) */
20
- private currentEpoch;
23
+ private readonly pool;
24
+ private validator;
21
25
  private constructor();
22
26
  /**
23
27
  * Should be called periodically to distribute pending tickets to connected peers.
24
28
  */
25
29
  maintainDistribution(): void;
26
30
  /**
27
- * Add a ticket to the pending queue for distribution.
31
+ * Add a ticket to the redistribution pool.
28
32
  * Clears pending tickets when epoch changes.
29
33
  * Deduplicates tickets based on signature.
30
34
  */
31
35
  addTicket(epochIndex: Epoch, ticket: SignedTicket): void;
32
- private onTicketReceivedCallback;
33
36
  /**
34
- * Register a callback that validates a received ticket.
35
- * The ticket is only added to the redistribution pool if the callback returns `true`.
36
- * This prevents redistribution of invalid tickets (e.g. those with a tampered `attempt` field).
37
+ * Replace the redistribution pool for the given epoch with the supplied tickets.
38
+ * Used when the authorship worker dumps the authoritative pool on an epoch boundary.
39
+ */
40
+ replacePool(epochIndex: Epoch, tickets: readonly SignedTicket[]): void;
41
+ /**
42
+ * Register the validator that decides whether tickets received from peers should be
43
+ * accepted (and therefore redistributed). The default is {@link DenyTicketsValidator},
44
+ * so the caller must install a real validator for any peer ticket to make it through.
37
45
  */
38
- setOnTicketReceived(cb: (epochIndex: Epoch, ticket: SignedTicket) => Promise<boolean>): void;
46
+ setTicketValidator(validator: TicketValidator): void;
39
47
  private onTicketReceived;
40
48
  }
41
49
  //# sourceMappingURL=ticket-distribution.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ticket-distribution.d.ts","sourceRoot":"","sources":["../../../../../../packages/jam/jamnp-s/tasks/ticket-distribution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAe1D;;;;;;GAMG;AACH,qBAAa,sBAAsB;IAuB/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAvB9B,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS;IAgBzF,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAA0D;IAChF,+DAA+D;IAC/D,OAAO,CAAC,YAAY,CAAsB;IAE1C,OAAO;IAKP;;OAEG;IACH,oBAAoB;IAkDpB;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY;IAsCjD,OAAO,CAAC,wBAAwB,CAAgF;IAEhH;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC;IAIrF,OAAO,CAAC,gBAAgB;CAsBzB"}
1
+ {"version":3,"file":"ticket-distribution.d.ts","sourceRoot":"","sources":["../../../../../../packages/jam/jamnp-s/tasks/ticket-distribution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAA2C,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEvG,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAe1D;;;;;;;;;;;GAWG;AACH,qBAAa,sBAAsB;IAqB/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW;IArB9B,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS;IAgBzF,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAChD,OAAO,CAAC,SAAS,CAA+C;IAEhE,OAAO;IAKP;;OAEG;IACH,oBAAoB;IAgDpB;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY;IAIjD;;;OAGG;IACH,WAAW,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE;IAI/D;;;;OAIG;IACH,kBAAkB,CAAC,SAAS,EAAE,eAAe;IAI7C,OAAO,CAAC,gBAAgB;CAkBzB"}
@@ -1,4 +1,5 @@
1
1
  import { Logger } from "#@typeberry/logger";
2
+ import { DenyTicketsValidator, PendingTicketPool } from "#@typeberry/ticket-pool";
2
3
  import { OK } from "#@typeberry/utils";
3
4
  import { ce131 } from "../protocol/index.js";
4
5
  const logger = Logger.new(import.meta.filename, "net:tickets");
@@ -12,6 +13,11 @@ const TICKET_AUX = {
12
13
  * Uses CE-132 (proxy-to-all) for direct broadcast to all peers.
13
14
  * Implements a maintain pattern similar to SyncTask: tickets are collected
14
15
  * and periodically distributed to peers that haven't received them yet.
16
+ *
17
+ * Incoming tickets from peers are first run through a {@link TicketValidator};
18
+ * only validated tickets are added to the redistribution pool. The default
19
+ * validator denies everything, so callers must wire a real one via
20
+ * {@link setTicketValidator} before any networked ticket can be redistributed.
15
21
  */
16
22
  export class TicketDistributionTask {
17
23
  streamManager;
@@ -26,10 +32,8 @@ export class TicketDistributionTask {
26
32
  streamManager.registerOutgoingHandlers(ce131.ClientHandler.new(chainSpec, ce131.STREAM_KIND_PROXY_TO_ALL));
27
33
  return task;
28
34
  }
29
- /** Pending tickets waiting to be distributed to peers */
30
- pendingTickets = [];
31
- /** Current epoch being tracked (cleared when epoch changes) */
32
- currentEpoch = null;
35
+ pool = new PendingTicketPool();
36
+ validator = new DenyTicketsValidator();
33
37
  constructor(streamManager, connections) {
34
38
  this.streamManager = streamManager;
35
39
  this.connections = connections;
@@ -38,14 +42,13 @@ export class TicketDistributionTask {
38
42
  * Should be called periodically to distribute pending tickets to connected peers.
39
43
  */
40
44
  maintainDistribution() {
41
- if (this.currentEpoch === null) {
42
- return; // No tickets to distribute yet
45
+ const currentEpoch = this.pool.currentEpoch;
46
+ if (currentEpoch === null) {
47
+ return;
43
48
  }
44
- /** `this` is mutable and TS can't narrow this.currentEpoch inside the callback closure */
45
- const currentEpoch = this.currentEpoch;
46
- // Iterate through all pending tickets
47
- for (let ticketIdx = 0; ticketIdx < this.pendingTickets.length; ticketIdx++) {
48
- const { epochIndex, ticket } = this.pendingTickets[ticketIdx];
49
+ const tickets = this.pool.getTickets();
50
+ for (let ticketIdx = 0; ticketIdx < tickets.length; ticketIdx++) {
51
+ const { epochIndex, ticket } = tickets[ticketIdx];
49
52
  // Try to send to each connected peer
50
53
  for (const peerInfo of this.connections.getConnectedPeers()) {
51
54
  this.connections.withAuxData(peerInfo.peerId, TICKET_AUX, (maybeAux) => {
@@ -77,72 +80,45 @@ export class TicketDistributionTask {
77
80
  }
78
81
  }
79
82
  /**
80
- * Add a ticket to the pending queue for distribution.
83
+ * Add a ticket to the redistribution pool.
81
84
  * Clears pending tickets when epoch changes.
82
85
  * Deduplicates tickets based on signature.
83
86
  */
84
87
  addTicket(epochIndex, ticket) {
85
- // Drop tickets for older epochs (can happen when a delayed validation callback completes
86
- // after the epoch has already advanced — accepting it would roll back currentEpoch).
87
- if (this.currentEpoch !== null && epochIndex < this.currentEpoch) {
88
- return;
89
- }
90
- // Epoch advanced — clear old tickets
91
- if (this.currentEpoch !== null && epochIndex > this.currentEpoch) {
92
- logger.log `[addTicket] Epoch changed from ${this.currentEpoch} to ${epochIndex}, clearing ${this.pendingTickets.length} old tickets`;
93
- this.pendingTickets = [];
94
- // Note: We don't need to clear aux data for all peers here.
95
- // The aux data contains the epoch, so maintainDistribution will lazily
96
- // reset it when it detects an epoch mismatch. This handles both connected
97
- // and disconnected peers correctly.
98
- }
99
- this.currentEpoch = epochIndex;
100
- /**
101
- * Deduplicate: check if a ticket with the same signature already exists
102
- *
103
- * Here we are risking "poisoning" the local pendingTickets - i.e:
104
- * 1. The adversary sees a signature and swaps the ticket attempt to something different.
105
- * 2. This creates an invalid ticket, but prevents a valid ticket with the same signature from being included and distributed.
106
- *
107
- * TODO [MaSi]: The poisoning risk should be fixed during implementation of ticket validation.
108
- */
109
- const isDuplicate = this.pendingTickets.some((pending) => pending.epochIndex === epochIndex && pending.ticket.signature.isEqualTo(ticket.signature));
110
- if (!isDuplicate) {
111
- this.pendingTickets.push({ epochIndex, ticket });
112
- logger.info `[addTicket] Added ticket for epoch ${epochIndex}, total: ${this.pendingTickets.length}`;
113
- }
88
+ this.pool.addTicket(epochIndex, ticket);
114
89
  }
115
- onTicketReceivedCallback = null;
116
90
  /**
117
- * Register a callback that validates a received ticket.
118
- * The ticket is only added to the redistribution pool if the callback returns `true`.
119
- * This prevents redistribution of invalid tickets (e.g. those with a tampered `attempt` field).
91
+ * Replace the redistribution pool for the given epoch with the supplied tickets.
92
+ * Used when the authorship worker dumps the authoritative pool on an epoch boundary.
120
93
  */
121
- setOnTicketReceived(cb) {
122
- this.onTicketReceivedCallback = cb;
94
+ replacePool(epochIndex, tickets) {
95
+ this.pool.replace(epochIndex, tickets);
96
+ }
97
+ /**
98
+ * Register the validator that decides whether tickets received from peers should be
99
+ * accepted (and therefore redistributed). The default is {@link DenyTicketsValidator},
100
+ * so the caller must install a real validator for any peer ticket to make it through.
101
+ */
102
+ setTicketValidator(validator) {
103
+ this.validator = validator;
123
104
  }
124
105
  onTicketReceived(epochIndex, ticket) {
125
106
  logger.trace `Received ticket for epoch ${epochIndex}, attempt ${ticket.attempt}`;
126
- if (this.onTicketReceivedCallback !== null) {
127
- // Validate first; only redistribute if valid to avoid spreading tampered tickets.
128
- // Wrap with Promise.resolve().then() to catch both sync throws and async rejections.
129
- const cb = this.onTicketReceivedCallback;
130
- Promise.resolve()
131
- .then(() => cb(epochIndex, ticket))
132
- .then((isValid) => {
133
- if (isValid) {
134
- this.addTicket(epochIndex, ticket);
135
- }
136
- else {
137
- logger.warn `Dropping invalid ticket for epoch ${epochIndex} (validation failed)`;
138
- }
139
- })
140
- .catch((error) => {
141
- logger.error `Error validating ticket for epoch ${epochIndex}, attempt ${ticket.attempt}: ${error}`;
142
- });
143
- }
144
- else {
145
- this.addTicket(epochIndex, ticket);
146
- }
107
+ const validator = this.validator;
108
+ // Wrap with Promise.resolve().then() so a synchronous throw inside the validator
109
+ // funnels into the same .catch() as an async rejection.
110
+ Promise.resolve()
111
+ .then(() => validator.validate(epochIndex, [ticket]))
112
+ .then((result) => {
113
+ if (result.isOk) {
114
+ this.addTicket(epochIndex, ticket);
115
+ }
116
+ else {
117
+ logger.trace `Dropping ticket for epoch ${epochIndex}: ${result.error}`;
118
+ }
119
+ })
120
+ .catch((error) => {
121
+ logger.error `Error validating ticket for epoch ${epochIndex}, attempt ${ticket.attempt}: ${error}`;
122
+ });
147
123
  }
148
124
  }